diff --git a/jmclient/jmclient/__init__.py b/jmclient/jmclient/__init__.py index 26dd57b..1d60918 100644 --- a/jmclient/jmclient/__init__.py +++ b/jmclient/jmclient/__init__.py @@ -34,6 +34,8 @@ from .schedule import (get_schedule, get_tumble_schedule, schedule_to_text, tweak_tumble_schedule, human_readable_schedule_entry, schedule_to_text) from .commitment_utils import get_utxo_info, validate_utxo_data, quit +from .tumble_support import (tumbler_taker_finished_update, restart_waiter, + get_tumble_log) # Set default logging handler to avoid "No handler found" warnings. try: diff --git a/jmclient/jmclient/taker.py b/jmclient/jmclient/taker.py index 06f1eda..f130d83 100644 --- a/jmclient/jmclient/taker.py +++ b/jmclient/jmclient/taker.py @@ -30,14 +30,46 @@ class Taker(object): order_chooser=weighted_order_choose, sign_method=None, callbacks=None): - """Schedule must be a list of tuples: [(mixdepth,cjamount,N, destaddr),..] + """Schedule must be a list of tuples: (see sample_schedule_for_testnet + for explanation of syntax, also schedule.py module in this directory), which will be a sequence of joins to do. - callbacks: - 1.filter orders callback: called to allow the client to decide whether - to accept the proposed offers. - 2.taker info callback: called to allow the client to read updates - 3.on finished callback: called on completion, either of the whole schedule - or early if a transactoin fails. + Callbacks: + External callers set the 3 callbacks for filtering orders, + sending info messages to client, and action on completion. + "None" is allowable for taker_info_callback, defaults to log msg. + Callback function definitions: + ===================== + filter_orders_callback + ===================== + args: + 1. orders_fees - a list of two items 1. orders dict 2 total cjfee + 2. cjamount - coinjoin amount in satoshis + returns: + False - offers rejected OR + True - offers accepted OR + 'retry' - offers not accepted but try again + ======================= + on_finished_callback + ======================= + args: + 1. res - True means tx successful, False means tx unsucessful + 2. fromtx - True means not the final transaction, False means final + (end of schedule), 'unconfirmed' means tx seen on the network only. + 3. waittime - passed in minutes, time to wait after confirmation before + continuing to next tx (thus, only used if fromtx is True). + 4. txdetails - a tuple (txd, txid) - only to be used when fromtx + is 'unconfirmed', used for monitoring. + returns: + None + ======================== + taker_info_callback + ======================== + args: + 1. type - one of 'ABORT' or 'INFO', the former signals the client that + processing of this transaction is aborted, the latter is only an update. + 2. message - an information message. + returns: + None """ self.aborted = False self.wallet = wallet @@ -50,27 +82,10 @@ class Taker(object): #allow custom wallet-based clients to use their own signing code; #currently only setting "wallet" is allowed, calls wallet.sign_tx(tx) self.sign_method = sign_method - #External callers set the 3 callbacks for filtering orders, - #sending info messages to client, and action on completion. - #"None" is allowable for taker_info_callback, defaults to log msg. - #"None" is allowable for filter_orders_callback, in which case offers - #are automatically accepted (bar insane fees). - #"None" is *not* allowable for taker_finished_callback, as it controls - #process flow after tx finished. - """Signature of filter_orders_callback: - args: orders_fees, cjamount - returns: boolean representing accept/reject - """ self.filter_orders_callback = callbacks[0] self.taker_info_callback = callbacks[1] if not self.taker_info_callback: self.taker_info_callback = self.default_taker_info_callback - """Signature of on_finished_callback: - args: res: True/False to flag success - from_tx: indicating whether all txs finished, or more to do - waittime: how long to wait before continuing to next. - returns: None - """ self.on_finished_callback = callbacks[2] def default_taker_info_callback(self, infotype, msg): diff --git a/jmclient/jmclient/tumble_support.py b/jmclient/jmclient/tumble_support.py new file mode 100644 index 0000000..c484a36 --- /dev/null +++ b/jmclient/jmclient/tumble_support.py @@ -0,0 +1,159 @@ +from __future__ import absolute_import, print_function +from jmclient import schedule_to_text, human_readable_schedule_entry +import logging +import pprint +import os +import time +from .configure import get_log, jm_single, validate_address +from .schedule import human_readable_schedule_entry, tweak_tumble_schedule + +log = get_log() + +""" +Utility functions for tumbler-style takers; +Currently re-used by CLI script tumbler.py and joinmarket-qt +""" + +def get_tumble_log(logsdir): + tumble_log = logging.getLogger('tumbler') + tumble_log.setLevel(logging.DEBUG) + logFormatter = logging.Formatter( + ('%(asctime)s %(message)s')) + fileHandler = logging.FileHandler(os.path.join(logsdir, 'TUMBLE.log')) + fileHandler.setFormatter(logFormatter) + tumble_log.addHandler(fileHandler) + return tumble_log + +def restart_waiter(txid): + """Given a txid, wait for confirmation by polling the blockchain + interface instance. Note that this is currently blocking, which is + fine for the CLI for now, but should be re-done using twisted/thread TODO. + """ + ctr = 0 + log.info("Waiting for confirmation of last transaction: " + str(txid)) + while True: + time.sleep(10) + ctr += 1 + if not (ctr % 12): + log.debug("Still waiting for confirmation of last transaction ...") + res = jm_single().bc_interface.query_utxo_set(txid, includeconf=True) + if not res[0]: + continue + if res[0]['confirms'] > 0: + break + log.info("The previous transaction is now in a block; continuing.") + +def unconf_update(taker, schedulefile, tumble_log, addtolog=False): + """Provide a Taker object, a schedulefile path for the current + schedule, a logging instance for TUMBLE.log, and a parameter + for whether to update TUMBLE.log. + Makes the necessary state updates explained below, including to + the wallet. + Note that this is re-used for confirmation with addtolog=False, + to avoid a repeated entry in the log. + """ + #on taker side, cache index update is only required after tx + #push, to avoid potential of address reuse in case of a crash, + #because addresses are not public until broadcast (whereas for makers, + #they are public *during* negotiation). So updating the cache here + #is sufficient + taker.wallet.update_cache_index() + + #We persist the fact that the transaction is complete to the + #schedule file. Note that if a tweak to the schedule occurred, + #it only affects future (non-complete) transactions, so the final + #full record should always be accurate; but TUMBLE.log should be + #used for checking what actually happened. + completion_flag = 1 if not addtolog else taker.txid + taker.schedule[taker.schedule_index][5] = completion_flag + with open(schedulefile, "wb") as f: + f.write(schedule_to_text(taker.schedule)) + + if addtolog: + tumble_log.info("Completed successfully this entry:") + #the log output depends on if it's to INTERNAL + hrdestn = None + if taker.schedule[taker.schedule_index][3] in ["INTERNAL", "addrask"]: + hrdestn = taker.my_cj_addr + #Whether sweep or not, the amt is not in satoshis; use taker data + hramt = taker.cjamount + tumble_log.info(human_readable_schedule_entry( + taker.schedule[taker.schedule_index], hramt, hrdestn)) + tumble_log.info("Txid was: " + taker.txid) + +def tumbler_taker_finished_update(taker, schedulefile, tumble_log, options, + res, fromtx=False, waittime=0.0, txdetails=None): + """on_finished_callback processing for tumbler. + Note that this is *not* the full callback, but provides common + processing across command line and other GUI versions. + """ + + if fromtx == "unconfirmed": + #unconfirmed event means transaction has been propagated, + #we update state to prevent accidentally re-creating it in + #any crash/restart condition + unconf_update(taker, schedulefile, tumble_log, True) + return + + if fromtx: + if res: + #this has no effect except in the rare case that confirmation + #is immediate; also it does not repeat the log entry. + unconf_update(taker, schedulefile, tumble_log, False) + #note that Qt does not yet support 'addrask', so this is only + #for command line script TODO + if taker.schedule[taker.schedule_index+1][3] == 'addrask': + jm_single().debug_silence[0] = True + print('\n'.join(['=' * 60] * 3)) + print('Tumbler requires more addresses to stop amount correlation') + print('Obtain a new destination address from your bitcoin recipient') + print(' for example click the button that gives a new deposit address') + print('\n'.join(['=' * 60] * 1)) + while True: + destaddr = raw_input('insert new address: ') + addr_valid, errormsg = validate_address(destaddr) + if addr_valid: + break + print( + 'Address ' + destaddr + ' invalid. ' + errormsg + ' try again') + jm_single().debug_silence[0] = False + taker.schedule[taker.schedule_index+1][3] = destaddr + + waiting_message = "Waiting for: " + str(waittime) + " minutes." + tumble_log.info(waiting_message) + log.info(waiting_message) + txd, txid = txdetails + taker.wallet.remove_old_utxos(txd) + taker.wallet.add_new_utxos(txd, txid) + else: + #a transaction failed; tumbler is aggressive in trying to + #complete; we tweak the schedule from this point in the mixdepth, + #then try again: + tumble_log.info("Transaction attempt failed, tweaking schedule" + " and trying again.") + tumble_log.info("The paramaters of the failed attempt: ") + tumble_log.info(str(taker.schedule[taker.schedule_index])) + log.info("Schedule entry: " + str( + taker.schedule[taker.schedule_index]) + \ + " failed after timeout, trying again") + taker.schedule_index -= 1 + taker.schedule = tweak_tumble_schedule(options, taker.schedule, + taker.schedule_index) + tumble_log.info("We tweaked the schedule, the new schedule is:") + tumble_log.info(pprint.pformat(taker.schedule)) + else: + if not res: + failure_msg = "Did not complete successfully, shutting down" + tumble_log.info(failure_msg) + log.info(failure_msg) + else: + log.info("All transactions completed correctly") + tumble_log.info("Completed successfully the last entry:") + #Whether sweep or not, the amt is not in satoshis; use taker data + hramt = taker.cjamount + tumble_log.info(human_readable_schedule_entry( + taker.schedule[taker.schedule_index], hramt)) + #copy of above, TODO refactor out + taker.schedule[taker.schedule_index][5] = 1 + with open(schedulefile, "wb") as f: + f.write(schedule_to_text(taker.schedule)) diff --git a/scripts/joinmarket-qt.py b/scripts/joinmarket-qt.py index 2cb3a4f..c0a08df 100644 --- a/scripts/joinmarket-qt.py +++ b/scripts/joinmarket-qt.py @@ -50,7 +50,10 @@ from jmclient import (load_program_config, get_network, Wallet, JMTakerClientProtocolFactory, WalletError, start_reactor, get_schedule, get_tumble_schedule, schedule_to_text, mn_decode, mn_encode, create_wallet_file, - get_blockchain_interface_instance, sync_wallet) + get_blockchain_interface_instance, sync_wallet, + RegtestBitcoinCoreInterface, tweak_tumble_schedule, + human_readable_schedule_entry, tumbler_taker_finished_update, + get_tumble_log, restart_waiter) from qtsupport import (ScheduleWizard, warnings, config_tips, config_types, TaskThread, QtHandler, XStream, Buttons, CloseButton, @@ -292,6 +295,7 @@ class SpendTab(QWidget): wizard = ScheduleWizard() wizard.exec_() self.loaded_schedule = wizard.get_schedule() + self.tumbler_options = wizard.opts self.sch_label2.setText(wizard.get_name()) self.sched_view.setText(schedule_to_text(self.loaded_schedule)) self.sch_startButton.setEnabled(True) @@ -315,11 +319,14 @@ class SpendTab(QWidget): title='Error') else: w.statusBar().showMessage("Schedule loaded OK.") - self.sch_label2.setText(os.path.basename(str(firstarg))) - self.sched_view.setText(rawsched) + self.updateSchedView(rawsched, os.path.basename(str(firstarg))) self.sch_startButton.setEnabled(True) self.loaded_schedule = schedule + def updateSchedView(self, text, name): + self.sch_label2.setText(name) + self.sched_view.setText(text) + def getDonateLayout(self): donateLayout = QHBoxLayout() self.donateCheckBox = QCheckBox() @@ -575,11 +582,12 @@ class SpendTab(QWidget): self.taker_info_response = None return - def callback_takerFinished(self, res, fromtx=False, waittime=0.0): + def callback_takerFinished(self, res, fromtx=False, waittime=0.0, + txdetails=None): self.taker_finished_res = res self.taker_finished_fromtx = fromtx - #TODO; equivalent of reactor.callLater to deliberately delay (for tumbler) - self.taker_finished_waittime = int(waittime*1000) + self.taker_finished_waittime = waittime + self.taker_finished_txdetails = txdetails self.jmclient_obj.emit(QtCore.SIGNAL('JMCLIENT:finished')) return @@ -665,40 +673,44 @@ class SpendTab(QWidget): """Callback (after pass-through signal) for jmclient.Taker on completion of each join transaction. """ + sfile = os.path.join(logsdir, 'TUMBLE.schedule') + #non-GUI-specific state updates first: + tumbler_taker_finished_update(self.taker, sfile, tumble_log, + self.tumbler_options, self.taker_finished_res, + self.taker_finished_fromtx, + self.taker_finished_waittime, + self.taker_finished_txdetails) + + #Shows the schedule updates in the GUI; TODO make this more visual + self.updateSchedView(schedule_to_text(self.taker.schedule), + 'TUMBLE.schedule') + + #GUI-specific updates; QTimer.singleShort serves the role + #of reactor.callLater + if self.taker_finished_fromtx == "unconfirmed": + w.statusBar().showMessage( + "Transaction seen on network: " + self.taker.txid) + return if self.taker_finished_fromtx: - #not the final finished transaction if self.taker_finished_res: - w.statusBar().showMessage("Transaction completed successfully.") - self.persistTxToHistory(self.taker.my_cj_addr, - self.taker.cjamount, + self.persistTxToHistory(self.taker.my_cj_addr, self.taker.cjamount, self.taker.txid) - log.debug("Waiting for: " + str( - self.taker_finished_waittime/1000.0) + " secs.") - QtCore.QTimer.singleShot(self.taker_finished_waittime, + w.statusBar().showMessage("Transaction confirmed: " + self.taker.txid) + #singleShot argument is in milliseconds + QtCore.QTimer.singleShot(int(self.taker_finished_waittime*60*1000), self.startNextTransaction) else: - #a transaction failed to reach broadcast; - #restart processing from the failed schedule entry; - #note that for some failure vectors this is essentially - #an infinite loop, but the user can abort any time (or - #modify the wallet e.g. to add commitment utxos). - self.taker.schedule_index -= 1 - log.info("Transaction failed after timeout, trying again") + w.statusBar().showMessage("Transaction failed, trying again...") QtCore.QTimer.singleShot(0, self.startNextTransaction) self.giveUp() else: - #the final, or a permanent failure - if not self.taker_finished_res: - log.info("Did not complete successfully, shutting down") - else: - log.info("All transactions completed correctly") + if self.taker_finished_res: + self.persistTxToHistory(self.taker.my_cj_addr, self.taker.cjamount, + self.taker.txid) w.statusBar().showMessage("All transaction(s) completed successfully.") - self.persistTxToHistory(self.taker.my_cj_addr, - self.taker.cjamount, - self.taker.txid) if len(self.taker.schedule) == 1: msg = "Transaction has been broadcast.\n" + "Txid: " + \ - str(self.taker.txid) + str(self.taker.txid) else: msg = "All transactions have been broadcast." JMQtMessageBox(self, msg, title="Success") @@ -1437,10 +1449,18 @@ except Exception as e: exit(1) update_config_for_gui() -#we're not downloading from github, so logs dir -#might not exist +#to allow testing of confirm/unconfirm callback for multiple txs +if isinstance(jm_single().bc_interface, RegtestBitcoinCoreInterface): + jm_single().bc_interface.tick_forward_chain_interval = 10 + jm_single().maker_timeout_sec = 5 + +#prepare for logging if not os.path.exists('logs'): os.makedirs('logs') +logsdir = os.path.join(os.path.dirname(jm_single().config_location), "logs") +#tumble log will not always be used, but is made available anyway: +tumble_log = get_tumble_log(logsdir) + appWindowTitle = 'JoinMarketQt' w = JMMainWindow() tabWidget = QTabWidget(w) diff --git a/scripts/qtsupport.py b/scripts/qtsupport.py index 70a278e..0a75d2d 100644 --- a/scripts/qtsupport.py +++ b/scripts/qtsupport.py @@ -496,7 +496,7 @@ class SchDynamicPage1(QWizardPage): results = [] sN = ['Starting mixdepth', 'Average number of counterparties', 'How many mixdepths to tumble through', - 'Average wait time between transactions, in seconds', + 'Average wait time between transactions, in minutes', 'Average number of transactions per mixdepth'] #Tooltips sH = ["The starting mixdepth can be decided from the Wallet tab; it must " @@ -619,19 +619,19 @@ class ScheduleWizard(QWizard): destaddrs = [str(x) for x in [self.field("destaddr0").toString(), self.field("destaddr1").toString(), self.field("destaddr2").toString()]] - opts = {} - opts['mixdepthsrc'] = int(self.field("mixdepthsrc").toString()) - opts['mixdepthcount'] = int(self.field("mixdepthcount").toString()) - opts['txfee'] = -1 - opts['addrcount'] = 3 - opts['makercountrange'] = (int(self.field("makercount").toString()), 1) - opts['minmakercount'] = 2 - opts['txcountparams'] = (int(self.field("txcountparams").toString()), 1) - opts['mintxcount'] = 1 - opts['amountpower'] = 100.0 - opts['timelambda'] = float(self.field("timelambda").toString()) - opts['waittime'] = 20 - opts['mincjamount'] = 1000000 + self.opts = {} + self.opts['mixdepthsrc'] = int(self.field("mixdepthsrc").toString()) + self.opts['mixdepthcount'] = int(self.field("mixdepthcount").toString()) + self.opts['txfee'] = -1 + self.opts['addrcount'] = 3 + self.opts['makercountrange'] = (int(self.field("makercount").toString()), 1) + self.opts['minmakercount'] = 2 + self.opts['txcountparams'] = (int(self.field("txcountparams").toString()), 1) + self.opts['mintxcount'] = 1 + self.opts['amountpower'] = 100.0 + self.opts['timelambda'] = float(self.field("timelambda").toString()) + self.opts['waittime'] = 20 + self.opts['mincjamount'] = 1000000 #needed for Taker to check: - jm_single().mincjamount = opts['mincjamount'] - return get_tumble_schedule(opts, destaddrs) + jm_single().mincjamount = self.opts['mincjamount'] + return get_tumble_schedule(self.opts, destaddrs) diff --git a/scripts/tumbler.py b/scripts/tumbler.py index 5c6b869..3731a0c 100644 --- a/scripts/tumbler.py +++ b/scripts/tumbler.py @@ -1,3 +1,4 @@ +#! /usr/bin/env python from __future__ import absolute_import, print_function import random @@ -17,49 +18,24 @@ from jmclient import (Taker, load_program_config, get_schedule, Wallet, sync_wallet, get_tumble_schedule, RegtestBitcoinCoreInterface, estimate_tx_fee, tweak_tumble_schedule, human_readable_schedule_entry, - schedule_to_text) + schedule_to_text, restart_waiter, get_tumble_log, + tumbler_taker_finished_update) from jmbase.support import get_log, debug_dump_object, get_password from cli_options import get_tumbler_parser log = get_log() +logsdir = os.path.join(os.path.dirname( + jm_single().config_location), "logs") -def restart_waiter(txid): - ctr = 0 - log.info("Waiting for confirmation of last transaction: " + str(txid)) - while True: - time.sleep(10) - ctr += 1 - if not (ctr % 12): - log.debug("Still waiting for confirmation of last transaction ...") - res = jm_single().bc_interface.query_utxo_set(txid, includeconf=True) - if not res[0]: - continue - if res[0]['confirms'] > 0: - break - log.info("The last transaction is now in a block; continuing.") def main(): - #Prepare log file giving simplified information - #on progress of tumble. - tumble_log = logging.getLogger('tumbler') - tumble_log.setLevel(logging.DEBUG) - logFormatter = logging.Formatter( - ('%(asctime)s %(message)s')) - logsdir = os.path.join(os.path.dirname( - jm_single().config_location), "logs") - fileHandler = logging.FileHandler(os.path.join(logsdir, 'TUMBLE.log')) - fileHandler.setFormatter(logFormatter) - tumble_log.addHandler(fileHandler) - + tumble_log = get_tumble_log(logsdir) (options, args) = get_tumbler_parser().parse_args() options = vars(options) - if len(args) < 1: parser.error('Needs a wallet file') sys.exit(0) - load_program_config() - #Load the wallet wallet_name = args[0] max_mix_depth = options['mixdepthsrc'] + options['mixdepthcount'] @@ -124,8 +100,10 @@ def main(): print("Progress logging to logs/TUMBLE.log") - #callback for order checking; dummy/passthrough def filter_orders_callback(orders_fees, cjamount): + """Since the tumbler does not use interactive fee checking, + we use the -x values from the command line instead. + """ orders, total_cj_fee = orders_fees abs_cj_fee = 1.0 * total_cj_fee / taker.n_counterparties rel_cj_fee = abs_cj_fee / cjamount @@ -139,112 +117,17 @@ def main(): return True def taker_finished(res, fromtx=False, waittime=0.0, txdetails=None): - """on_finished_callback for tumbler + """on_finished_callback for tumbler; processing is almost entirely + deferred to generic taker_finished in tumbler_support module, except + here reactor signalling. """ - def unconf_update(addtolog=False): - #on taker side, cache index update is only required after tx - #push, to avoid potential of address reuse in case of a crash, - #because addresses are not public until broadcast (whereas for makers, - #they are public *during* negotiation). So updating the cache here - #is sufficient - taker.wallet.update_cache_index() - - #We persist the fact that the transaction is complete to the - #schedule file. Note that if a tweak to the schedule occurred, - #it only affects future (non-complete) transactions, so the final - #full record should always be accurate; but TUMBLE.log should be - #used for checking what actually happened. - completion_flag = 1 if not addtolog else taker.txid - taker.schedule[taker.schedule_index][5] = completion_flag - with open(os.path.join(logsdir, options['schedulefile']), - "wb") as f: - f.write(schedule_to_text(taker.schedule)) - - if addtolog: - tumble_log.info("Completed successfully this entry:") - #the log output depends on if it's to INTERNAL - hrdestn = None - if taker.schedule[taker.schedule_index][3] in ["INTERNAL", "addrask"]: - hrdestn = taker.my_cj_addr - #Whether sweep or not, the amt is not in satoshis; use taker data - hramt = taker.cjamount - tumble_log.info(human_readable_schedule_entry( - taker.schedule[taker.schedule_index], hramt, hrdestn)) - tumble_log.info("Txid was: " + taker.txid) - - if fromtx == "unconfirmed": - #unconfirmed event means transaction has been propagated, - #we update state to prevent accidentally re-creating it in - #any crash/restart condition - unconf_update(True) - return - - if fromtx: - if res: - #this has no effect except in the rare case that confirmation - #is immediate; also it does not repeat the log entry. - unconf_update() - - if taker.schedule[taker.schedule_index+1][3] == 'addrask': - jm_single().debug_silence[0] = True - print('\n'.join(['=' * 60] * 3)) - print('Tumbler requires more addresses to stop amount correlation') - print('Obtain a new destination address from your bitcoin recipient') - print(' for example click the button that gives a new deposit address') - print('\n'.join(['=' * 60] * 1)) - while True: - destaddr = raw_input('insert new address: ') - addr_valid, errormsg = validate_address(destaddr) - if addr_valid: - break - print( - 'Address ' + destaddr + ' invalid. ' + errormsg + ' try again') - jm_single().debug_silence[0] = False - taker.schedule[taker.schedule_index+1][3] = destaddr - - waiting_message = "Waiting for: " + str(waittime) + " minutes." - tumble_log.info(waiting_message) - log.info(waiting_message) - txd, txid = txdetails - taker.wallet.remove_old_utxos(txd) - taker.wallet.add_new_utxos(txd, txid) - reactor.callLater(waittime*60, - clientfactory.getClient().clientStart) - else: - #a transaction failed; tumbler is aggressive in trying to - #complete; we tweak the schedule from this point in the mixdepth, - #then try again: - tumble_log.info("Transaction attempt failed, tweaking schedule" - " and trying again.") - tumble_log.info("The paramaters of the failed attempt: ") - tumble_log.info(str(taker.schedule[taker.schedule_index])) - log.info("Schedule entry: " + str( - taker.schedule[taker.schedule_index]) + \ - " failed after timeout, trying again") - taker.schedule_index -= 1 - taker.schedule = tweak_tumble_schedule(options, taker.schedule, - taker.schedule_index) - tumble_log.info("We tweaked the schedule, the new schedule is:") - tumble_log.info(pprint.pformat(taker.schedule)) - reactor.callLater(0, clientfactory.getClient().clientStart) - else: - if not res: - failure_msg = "Did not complete successfully, shutting down" - tumble_log.info(failure_msg) - log.info(failure_msg) - else: - log.info("All transactions completed correctly") - tumble_log.info("Completed successfully the last entry:") - #Whether sweep or not, the amt is not in satoshis; use taker data - hramt = taker.cjamount - tumble_log.info(human_readable_schedule_entry( - taker.schedule[taker.schedule_index], hramt)) - #copy of above, TODO refactor out - taker.schedule[taker.schedule_index][5] = 1 - with open(os.path.join(logsdir, options['schedulefile']), - "wb") as f: - f.write(schedule_to_text(taker.schedule)) + sfile = os.path.join(logsdir, options['schedulefile']) + tumbler_taker_finished_update(taker, sfile, tumble_log, options, + res, fromtx, waittime, txdetails) + if not fromtx: reactor.stop() + elif fromtx != "unconfirmed": + reactor.callLater(waittime*60, clientfactory.getClient().clientStart) #to allow testing of confirm/unconfirm callback for multiple txs if isinstance(jm_single().bc_interface, RegtestBitcoinCoreInterface):