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)