From a2d6f40250518a133bb6f9d01f551b7216c7929d Mon Sep 17 00:00:00 2001 From: Daniel <10026790+dnlggr@users.noreply.github.com> Date: Tue, 19 Apr 2022 15:55:52 +0200 Subject: [PATCH 1/3] Add rpc endpoint for tumbler --- jmclient/jmclient/wallet_rpc.py | 150 ++++++++++++++++++++++++++++++-- 1 file changed, 141 insertions(+), 9 deletions(-) diff --git a/jmclient/jmclient/wallet_rpc.py b/jmclient/jmclient/wallet_rpc.py index 3efe09a..8cc5ea1 100644 --- a/jmclient/jmclient/wallet_rpc.py +++ b/jmclient/jmclient/wallet_rpc.py @@ -10,6 +10,7 @@ from twisted.application.service import Service from autobahn.twisted.websocket import listenWS from klein import Klein import jwt +import pprint from jmbitcoin import human_readable_transaction from jmclient import Taker, jm_single, \ @@ -20,7 +21,11 @@ from jmclient import Taker, jm_single, \ create_wallet, get_max_cj_fee_values, \ StorageError, StoragePasswordError, JmwalletdWebSocketServerFactory, \ JmwalletdWebSocketServerProtocol, RetryableStorageError, \ - SegwitWalletFidelityBonds, wallet_gettimelockaddress, NotEnoughFundsException + SegwitWalletFidelityBonds, wallet_gettimelockaddress, \ + NotEnoughFundsException, get_tumble_log, get_tumble_schedule, \ + get_schedule, get_tumbler_parser, schedule_to_text, \ + tumbler_filter_orders_callback, tumbler_taker_finished_update, \ + validate_address from jmbase.support import get_log, utxostr_to_utxo jlog = get_log() @@ -158,6 +163,10 @@ class JMWalletDaemon(Service): # keep track of client side connections so they # can be shut down cleanly: self.coinjoin_connection = None + # Options for generating a tumble schedule / running the tumbler. + # Doubles as flag for indicating whether we're currently running + # a tumble schedule. + self.tumbler_options = None def get_client_factory(self): return JMClientProtocolFactory(self.taker) @@ -424,14 +433,26 @@ class JMWalletDaemon(Service): token=self.cookie) def taker_finished(self, res, fromtx=False, waittime=0.0, txdetails=None): - # This is a slimmed down version compared with what is seen in - # the CLI code, since that code encompasses schedules with multiple - # entries; for now, the RPC only supports single joins. - # TODO this may be updated. - # It is also different in that the event loop must not shut down - # when processing finishes. - - # reset our state on completion, we are no longer coinjoining: + if not self.tumbler_options: + # We were doing a single coinjoin -- stop taker. + self.stop_taker(res) + else: + # We're running the tumbler. + 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) + + if not fromtx: + # The tumbling schedule's final transaction is done. + self.stop_taker(res) + self.tumbler_options = None + elif fromtx != "unconfirmed": + # A non-final transaction in the tumbling schedule is done -- continue schedule after wait time. + reactor.callLater(waittime*60, self.clientfactory.getClient().clientStart) + + def stop_taker(self, res): self.taker = None if not res: @@ -464,6 +485,9 @@ class JMWalletDaemon(Service): """ return True + def filter_orders_callback_tumbler(self, orders_fees, cjamount): + return tumbler_filter_orders_callback(orders_fees, cjamount, self.taker) + def check_daemon_ready(self): # daemon must be up before coinjoins start. daemon_serving_host, daemon_serving_port = get_daemon_serving_params() @@ -1023,3 +1047,111 @@ class JMWalletDaemon(Service): raise InvalidRequestFormat() seedphrase, _ = self.services["wallet"].get_mnemonic_words() return make_jmwalletd_response(request, seedphrase=seedphrase) + + @app.route('/wallet//taker/schedule', methods=['POST']) + def start_tumbler(self, request, walletname): + self.check_cookie(request) + + if not self.services["wallet"]: + raise NoWalletFound() + if not self.wallet_name == walletname: + raise InvalidRequestFormat() + + # -- Options parsing ----------------------------------------------- + + (options, args) = get_tumbler_parser().parse_args([]) + self.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"]) + if not request_data: + raise InvalidRequestFormat() + + destaddrs = request_data["destination_addresses"] + for daddr in destaddrs: + success, _ = validate_address(daddr) + if not success: + raise InvalidRequestFormat() + + # 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): + raise ConfigNotPresent() + + max_cj_fee = get_max_cj_fee_values(jm_single().config, + None, + user_callback=dummy_user_callback) + + jm_single().mincjamount = self.tumbler_options['mincjamount'] + + # -- Schedule generation ------------------------------------------- + + # Always generates a new schedule. No restart support for now. + schedule = get_tumble_schedule(self.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']) + 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)) + + # -- Running the Taker --------------------------------------------- + + # For simplicity, we're not doing any fee estimation for now. + # We might want to add fee estimation (see scripts/tumbler.py) to + # prevent users from overspending on fees when tumbling with small + # amounts. + + if not self.activate_coinjoin_state(CJ_TAKER_RUNNING): + raise ServiceAlreadyStarted() + + self.taker = Taker(self.services["wallet"], + schedule, + max_cj_fee=max_cj_fee, + order_chooser=self.tumbler_options['order_choose_fn'], + callbacks=(self.filter_orders_callback_tumbler, None, + self.taker_finished), + tdestaddrs=destaddrs) + self.clientfactory = self.get_client_factory() + + self.taker.testflag = True + + dhost, dport = self.check_daemon_ready() + + _, self.coinjoin_connection = start_reactor(dhost, + dport, + self.clientfactory, + rs=False) + + return make_jmwalletd_response(request, status=202, schedule=schedule) + + @app.route('/wallet//taker/schedule', methods=['GET']) + def get_tumbler_schedule(self, request, walletname): + self.check_cookie(request) + + if not self.services["wallet"]: + raise NoWalletFound() + if not self.wallet_name == walletname: + raise InvalidRequestFormat() + + if not self.tumbler_options: + 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) + + if not res: + return make_jmwalletd_response(request, status=500) + + return make_jmwalletd_response(request, schedule=schedule) From 43b4eca8ec28fdb8e59704c54cf9f4b8c9cd1970 Mon Sep 17 00:00:00 2001 From: Daniel <10026790+dnlggr@users.noreply.github.com> Date: Wed, 20 Apr 2022 11:16:18 +0200 Subject: [PATCH 2/3] Add api docs --- docs/api/wallet-rpc.yaml | 89 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 88 insertions(+), 1 deletion(-) diff --git a/docs/api/wallet-rpc.yaml b/docs/api/wallet-rpc.yaml index 56cf11d..e666755 100644 --- a/docs/api/wallet-rpc.yaml +++ b/docs/api/wallet-rpc.yaml @@ -343,6 +343,61 @@ paths: $ref: '#/components/responses/409-NoConfig' '503': $ref: '#/components/responses/503-ServiceUnavailable' + /wallet/{walletname}/taker/schedule: + post: + security: + - bearerAuth: [] + summary: create and run a schedule of transactions + operationId: runschedule + description: Creates and then starts a schedule of transactions. + parameters: + - name: walletname + in: path + description: name of wallet including .jmdat + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/RunScheduleRequest' + description: taker side schedule parameters + responses: + '202': + $ref: "#/components/responses/RunSchedule-202-OK" + '400': + $ref: '#/components/responses/400-BadRequest' + '401': + $ref: '#/components/responses/401-Unauthorized' + '404': + $ref: '#/components/responses/404-NotFound' + '409': + $ref: '#/components/responses/409-NoConfig' + '503': + $ref: '#/components/responses/503-ServiceUnavailable' + get: + security: + - bearerAuth: [] + summary: get the schedule that is currently running + operationId: getschedule + description: Get the current transaction schedule if one is running. + parameters: + - name: walletname + in: path + description: name of the wallet including .jmdat + required: true + schema: + type: string + responses: + '200': + $ref: "#/components/responses/GetSchedule-200-OK" + '400': + $ref: '#/components/responses/400-BadRequest' + '401': + $ref: "#/components/responses/401-Unauthorized" + '404': + $ref: '#/components/responses/404-NotFound' /wallet/{walletname}/taker/stop: get: security: @@ -545,7 +600,16 @@ components: destination: type: string example: "bcrt1qujp2x2fv437493sm25gfjycns7d39exjnpptzw" - + RunScheduleRequest: + type: object + required: + - destinations + properties: + destinations: + type: array + items: + type: string + example: "bcrt1qujp2x2fv437493sm25gfjycns7d39exjnpptzw" StartMakerRequest: type: object required: @@ -762,6 +826,17 @@ components: properties: seedphrase: type: string + GetScheduleResponse: + type: object + required: + - schedule + properties: + schedule: + type: array + items: + oneOf: + - type: string + - type: integer LockWalletResponse: type: object required: @@ -902,6 +977,18 @@ components: application/json: schema: $ref: "#/components/schemas/FreezeResponse" + RunSchedule-202-OK: + description: "schedule started successfully" + content: + application/json: + schema: + $ref: "#/components/schemas/GetScheduleResponse" + GetSchedule-200-OK: + description: "schedule retrieved successfully" + content: + application/json: + schema: + $ref: "#/components/schemas/GetScheduleResponse" YieldGenReport-200-OK: description: "get list of coinjoins taken part in as maker (across all wallets)" content: From 1d5728f898ae903ce2bbeff0f9fae42f310e7ad1 Mon Sep 17 00:00:00 2001 From: Daniel <10026790+dnlggr@users.noreply.github.com> Date: Tue, 3 May 2022 13:43:00 +0200 Subject: [PATCH 3/3] Fix duplicated log issue --- jmclient/jmclient/wallet_rpc.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/jmclient/jmclient/wallet_rpc.py b/jmclient/jmclient/wallet_rpc.py index 8cc5ea1..048806c 100644 --- a/jmclient/jmclient/wallet_rpc.py +++ b/jmclient/jmclient/wallet_rpc.py @@ -167,6 +167,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) @@ -438,11 +439,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. @@ -1099,10 +1101,12 @@ class JMWalletDaemon(Service): 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 ---------------------------------------------