Browse Source

Merge #1260: Allow specifiying options for taker/schedule API endpoint

3f358ed Add options to scheduler API endpoint (Daniel)
master
Adam Gibson 4 years ago
parent
commit
d16f16e879
No known key found for this signature in database
GPG Key ID: 141001A1AF77F20B
  1. 54
      docs/api/wallet-rpc.yaml
  2. 96
      jmclient/jmclient/wallet_rpc.py

54
docs/api/wallet-rpc.yaml

@ -610,11 +610,63 @@ components:
required: required:
- destinations - destinations
properties: properties:
destinations: destination_addresses:
type: array type: array
items: items:
type: string type: string
example: "bcrt1qujp2x2fv437493sm25gfjycns7d39exjnpptzw" 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: StartMakerRequest:
type: object type: object
required: required:

96
jmclient/jmclient/wallet_rpc.py

@ -92,6 +92,11 @@ class TransactionFailed(Exception):
class NotEnoughCoinsForMaker(Exception): class NotEnoughCoinsForMaker(Exception):
pass 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 # raised when we cannot read data from our
# yigen-statement csv file: # yigen-statement csv file:
class YieldGeneratorDataUnreadable(Exception): class YieldGeneratorDataUnreadable(Exception):
@ -333,6 +338,12 @@ class JMWalletDaemon(Service):
request.setResponseCode(409) request.setResponseCode(409)
return self.err(request, "Maker could not start, no confirmed coins.") 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) @app.handle_errors(YieldGeneratorDataUnreadable)
def yieldgenerator_report_unavailable(self, request, failure): def yieldgenerator_report_unavailable(self, request, failure):
request.setResponseCode(404) request.setResponseCode(404)
@ -375,21 +386,48 @@ class JMWalletDaemon(Service):
# startup, so any failure to exist here is a logic error: # startup, so any failure to exist here is a logic error:
self.wss_factory.valid_token = self.cookie 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 """ given a request object, retrieve values corresponding
to keys keys in a dict, assuming they were encoded using JSON. to keys in a dict, assuming they were encoded using JSON.
If *any* of the keys are not present, return False, else If *any* of the required_keys are not present or required_keys
returns a dict of those key-value pairs. 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) assert isinstance(request.content, BytesIO)
# we swallow any formatting failure here: # we swallow any formatting failure here:
try: try:
json_data = json.loads(request.content.read().decode( json_data = json.loads(request.content.read().decode(
"utf-8")) "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: except:
return False 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): def initialize_wallet_service(self, request, wallet, wallet_name, **kwargs):
""" Called only when the wallet has loaded correctly, so """ Called only when the wallet has loaded correctly, so
authorization is passed, so set cookie for this wallet authorization is passed, so set cookie for this wallet
@ -1063,6 +1101,10 @@ class JMWalletDaemon(Service):
def start_tumbler(self, request, walletname): def start_tumbler(self, request, walletname):
self.check_cookie(request) 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"]: if not self.services["wallet"]:
raise NoWalletFound() raise NoWalletFound()
if not self.wallet_name == walletname: if not self.wallet_name == walletname:
@ -1070,13 +1112,12 @@ class JMWalletDaemon(Service):
# -- Options parsing ----------------------------------------------- # -- Options parsing -----------------------------------------------
# Start with default tumbler options from the tumbler CLI.
(options, args) = get_tumbler_parser().parse_args([]) (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. request_data = self.get_POST_body(request, ["destination_addresses"],
# For now, all other options are hardcoded to the defaults of ["tumbler_options"])
# the tumbler.py script.
request_data = self.get_POST_body(request, ["destination_addresses"])
if not request_data: if not request_data:
raise InvalidRequestFormat() raise InvalidRequestFormat()
@ -1086,6 +1127,14 @@ class JMWalletDaemon(Service):
if not success: if not success:
raise InvalidRequestFormat() 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. # Setting max_cj_fee based on global config.
# We won't respect it being set via tumbler_options for now. # We won't respect it being set via tumbler_options for now.
def dummy_user_callback(rel, abs): def dummy_user_callback(rel, abs):
@ -1095,18 +1144,34 @@ class JMWalletDaemon(Service):
None, None,
user_callback=dummy_user_callback) 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 ------------------------------------------- # -- Schedule generation -------------------------------------------
# Always generates a new schedule. No restart support for now. # Always generates a new schedule. No restart support for now.
schedule = get_tumble_schedule(self.tumbler_options, schedule = get_tumble_schedule(tumbler_options,
destaddrs, destaddrs,
self.services["wallet"].get_balance_by_mixdepth()) self.services["wallet"].get_balance_by_mixdepth())
logsdir = os.path.join(os.path.dirname(jm_single().config_location), logsdir = os.path.join(os.path.dirname(jm_single().config_location),
"logs") "logs")
sfile = os.path.join(logsdir, self.tumbler_options['schedulefile']) sfile = os.path.join(logsdir, tumbler_options['schedulefile'])
with open(sfile, "wb") as f: with open(sfile, "wb") as f:
f.write(schedule_to_text(schedule)) f.write(schedule_to_text(schedule))
@ -1127,6 +1192,8 @@ class JMWalletDaemon(Service):
if not self.activate_coinjoin_state(CJ_TAKER_RUNNING): if not self.activate_coinjoin_state(CJ_TAKER_RUNNING):
raise ServiceAlreadyStarted() raise ServiceAlreadyStarted()
self.tumbler_options = tumbler_options
self.taker = Taker(self.services["wallet"], self.taker = Taker(self.services["wallet"],
schedule, schedule,
max_cj_fee=max_cj_fee, max_cj_fee=max_cj_fee,
@ -1156,10 +1223,9 @@ class JMWalletDaemon(Service):
if not self.wallet_name == walletname: if not self.wallet_name == walletname:
raise InvalidRequestFormat() 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) return make_jmwalletd_response(request, status=404)
logsdir = os.path.join(os.path.dirname(jm_single().config_location), "logs") 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, self.tumbler_options['schedulefile'])
res, schedule = get_schedule(sfile) res, schedule = get_schedule(sfile)

Loading…
Cancel
Save