Browse Source

refactor tumble callback code to be shared with GUI, other impls.

Also add detailed function definitions for Taker callbacks in
taker.py comments. Modifications to joinmarket-qt, tumbler
working but these modifications not yet complete.
master
Adam Gibson 9 years ago
parent
commit
78dd815b79
  1. 2
      jmclient/jmclient/__init__.py
  2. 63
      jmclient/jmclient/taker.py
  3. 159
      jmclient/jmclient/tumble_support.py
  4. 82
      scripts/joinmarket-qt.py
  5. 32
      scripts/qtsupport.py
  6. 153
      scripts/tumbler.py

2
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:

63
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):

159
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))

82
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)

32
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)

153
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):

Loading…
Cancel
Save