Browse Source

Add options to scheduler API endpoint

master
Daniel 4 years ago
parent
commit
3f358edf8f
No known key found for this signature in database
GPG Key ID: 3FB015F7E1AA3AFB
  1. 54
      docs/api/wallet-rpc.yaml
  2. 112
      jmclient/jmclient/wallet_rpc.py

54
docs/api/wallet-rpc.yaml

@ -605,11 +605,63 @@ components:
required:
- destinations
properties:
destinations:
destination_addresses:
type: array
items:
type: string
example: "bcrt1qujp2x2fv437493sm25gfjycns7d39exjnpptzw"
tumbler_options:
type: object
properties:
addrcount:
type: integer
minmakercount:
type: integer
makercountrange:
type: array
items:
type: number
minItems: 2
maxItems: 2
example: [9, 1]
mixdepthcount:
type: integer
mintxcount:
type: integer
txcountparams:
type: array
items:
type: number
minItems: 2
maxItems: 2
example: [2, 1]
timelambda:
type: number
stage1_timelambda_increase:
type: number
liquiditywait:
type: integer
waittime:
type: number
mixdepthsrc:
type: integer
restart:
type: boolean
schedulefile:
type: string
mincjamount:
type: integer
amtmixdepths:
type: integer
rounding_chance:
type: number
rounding_sigfig_weights:
type: array
items:
type: number
minItems: 5
maxItems: 5
example: [55, 15, 25, 65, 40]
StartMakerRequest:
type: object
required:

112
jmclient/jmclient/wallet_rpc.py

@ -92,6 +92,11 @@ class TransactionFailed(Exception):
class NotEnoughCoinsForMaker(Exception):
pass
# raised when we tried to start the tumbler,
# but the wallet was empty/not enough.
class NotEnoughCoinsForTumbler(Exception):
pass
# raised when we cannot read data from our
# yigen-statement csv file:
class YieldGeneratorDataUnreadable(Exception):
@ -167,6 +172,7 @@ class JMWalletDaemon(Service):
# Doubles as flag for indicating whether we're currently running
# a tumble schedule.
self.tumbler_options = None
self.tumble_log = None
def get_client_factory(self):
return JMClientProtocolFactory(self.taker)
@ -332,6 +338,12 @@ class JMWalletDaemon(Service):
request.setResponseCode(409)
return self.err(request, "Maker could not start, no confirmed coins.")
@app.handle_errors(NotEnoughCoinsForTumbler)
def not_enough_coins_tumbler(self, request, failure):
# as above, 409 may not be ideal
request.setResponseCode(409)
return self.err(request, "Tumbler could not start, no confirmed coins.")
@app.handle_errors(YieldGeneratorDataUnreadable)
def yieldgenerator_report_unavailable(self, request, failure):
request.setResponseCode(404)
@ -369,21 +381,48 @@ class JMWalletDaemon(Service):
# startup, so any failure to exist here is a logic error:
self.wss_factory.valid_token = self.cookie
def get_POST_body(self, request, keys):
def get_POST_body(self, request, required_keys, optional_keys=None):
""" given a request object, retrieve values corresponding
to keys keys in a dict, assuming they were encoded using JSON.
If *any* of the keys are not present, return False, else
returns a dict of those key-value pairs.
to keys in a dict, assuming they were encoded using JSON.
If *any* of the required_keys are not present or required_keys
and optional_keys clash, return False, else returns a dict of those
key-value pairs and any of the optional key-value pairs.
"""
assert isinstance(request.content, BytesIO)
# we swallow any formatting failure here:
try:
json_data = json.loads(request.content.read().decode(
"utf-8"))
return {k: json_data[k] for k in keys}
required_body = {k: json_data[k] for k in required_keys}
if optional_keys is not None:
optional_body = {k: json_data[k] for k in optional_keys if k in json_data}
for key in optional_body:
if not key in required_body:
required_body[key] = optional_body[key]
else:
return False
return required_body
except:
return False
def parse_tumbler_options_from_json(self, tumbler_options_json):
parsed_tumbler_options = {}
for key, val in tumbler_options_json.items():
if isinstance(val, list):
# Convert JSON lists to tuples.
parsed_tumbler_options[key] = tuple(val)
elif key == 'order_choose_fn':
# Todo: No support for custom order choose function via API yet.
pass
else:
parsed_tumbler_options[key] = val
return parsed_tumbler_options
def initialize_wallet_service(self, request, wallet, wallet_name, **kwargs):
""" Called only when the wallet has loaded correctly, so
authorization is passed, so set cookie for this wallet
@ -438,11 +477,12 @@ class JMWalletDaemon(Service):
self.stop_taker(res)
else:
# We're running the tumbler.
assert self.tumble_log is not None
logsdir = os.path.join(os.path.dirname(jm_single().config_location), "logs")
tumble_log = get_tumble_log(logsdir)
sfile = os.path.join(logsdir, self.tumbler_options['schedulefile'])
tumbler_taker_finished_update(self.taker, sfile, tumble_log, self.tumbler_options, res, fromtx, waittime, txdetails)
tumbler_taker_finished_update(self.taker, sfile, self.tumble_log, self.tumbler_options, res, fromtx, waittime, txdetails)
if not fromtx:
# The tumbling schedule's final transaction is done.
@ -1052,6 +1092,10 @@ class JMWalletDaemon(Service):
def start_tumbler(self, request, walletname):
self.check_cookie(request)
if self.coinjoin_state is not CJ_NOT_RUNNING or self.tumbler_options is not None:
# Tumbler or taker seems to be running already.
return make_jmwalletd_response(request, status=409)
if not self.services["wallet"]:
raise NoWalletFound()
if not self.wallet_name == walletname:
@ -1059,13 +1103,12 @@ class JMWalletDaemon(Service):
# -- Options parsing -----------------------------------------------
# Start with default tumbler options from the tumbler CLI.
(options, args) = get_tumbler_parser().parse_args([])
self.tumbler_options = vars(options)
tumbler_options = vars(options)
# At the moment only the destination addresses can be set.
# For now, all other options are hardcoded to the defaults of
# the tumbler.py script.
request_data = self.get_POST_body(request, ["destination_addresses"])
request_data = self.get_POST_body(request, ["destination_addresses"],
["tumbler_options"])
if not request_data:
raise InvalidRequestFormat()
@ -1075,6 +1118,14 @@ class JMWalletDaemon(Service):
if not success:
raise InvalidRequestFormat()
if "tumbler_options" in request_data:
requested_tumbler_options = self.parse_tumbler_options_from_json(
request_data["tumbler_options"])
for k in tumbler_options:
if k in requested_tumbler_options:
tumbler_options[k] = requested_tumbler_options[k]
# Setting max_cj_fee based on global config.
# We won't respect it being set via tumbler_options for now.
def dummy_user_callback(rel, abs):
@ -1084,25 +1135,43 @@ class JMWalletDaemon(Service):
None,
user_callback=dummy_user_callback)
jm_single().mincjamount = self.tumbler_options['mincjamount']
jm_single().mincjamount = tumbler_options['mincjamount']
# -- Check wallet balance ------------------------------------------
max_mix_depth = tumbler_options['mixdepthsrc'] + tumbler_options['mixdepthcount']
if tumbler_options['amtmixdepths'] > max_mix_depth:
max_mix_depth = tumbler_options['amtmixdepths']
max_mix_to_tumble = min(tumbler_options['mixdepthsrc'] + tumbler_options['mixdepthcount'], max_mix_depth)
total_tumble_amount = int(0)
for i in range(tumbler_options['mixdepthsrc'], max_mix_to_tumble):
total_tumble_amount += self.services["wallet"].get_balance_by_mixdepth(verbose=False, minconfs=1)[i]
if total_tumble_amount == 0:
raise NotEnoughCoinsForTumbler()
# -- Schedule generation -------------------------------------------
# Always generates a new schedule. No restart support for now.
schedule = get_tumble_schedule(self.tumbler_options,
schedule = get_tumble_schedule(tumbler_options,
destaddrs,
self.services["wallet"].get_balance_by_mixdepth())
logsdir = os.path.join(os.path.dirname(jm_single().config_location),
"logs")
sfile = os.path.join(logsdir, self.tumbler_options['schedulefile'])
sfile = os.path.join(logsdir, tumbler_options['schedulefile'])
with open(sfile, "wb") as f:
f.write(schedule_to_text(schedule))
tumble_log = get_tumble_log(logsdir)
tumble_log.info("TUMBLE STARTING")
tumble_log.info("With this schedule: ")
tumble_log.info(pprint.pformat(schedule))
if self.tumble_log is None:
self.tumble_log = get_tumble_log(logsdir)
self.tumble_log.info("TUMBLE STARTING")
self.tumble_log.info("With this schedule: ")
self.tumble_log.info(pprint.pformat(schedule))
# -- Running the Taker ---------------------------------------------
@ -1114,6 +1183,8 @@ class JMWalletDaemon(Service):
if not self.activate_coinjoin_state(CJ_TAKER_RUNNING):
raise ServiceAlreadyStarted()
self.tumbler_options = tumbler_options
self.taker = Taker(self.services["wallet"],
schedule,
max_cj_fee=max_cj_fee,
@ -1143,10 +1214,9 @@ class JMWalletDaemon(Service):
if not self.wallet_name == walletname:
raise InvalidRequestFormat()
if not self.tumbler_options:
if not self.tumbler_options or not self.coinjoin_state == CJ_TAKER_RUNNING:
return make_jmwalletd_response(request, status=404)
logsdir = os.path.join(os.path.dirname(jm_single().config_location), "logs")
sfile = os.path.join(logsdir, self.tumbler_options['schedulefile'])
res, schedule = get_schedule(sfile)

Loading…
Cancel
Save