diff --git a/jmclient/jmclient/blockchaininterface.py b/jmclient/jmclient/blockchaininterface.py index 00773a8..0771b09 100644 --- a/jmclient/jmclient/blockchaininterface.py +++ b/jmclient/jmclient/blockchaininterface.py @@ -15,7 +15,7 @@ import urllib import urllib2 import traceback from decimal import Decimal -from twisted.internet import reactor +from twisted.internet import reactor, task import btc @@ -807,156 +807,6 @@ class BlockrInterface(BlockchainInterface): #pragma: no cover return fee_per_kb - -def bitcoincore_timeout_callback(uc_called, txout_set, txnotify_fun_list, - timeoutfun): - log.debug('bitcoin core timeout callback uc_called = %s' % ('true' - if uc_called - else 'false')) - txnotify_tuple = None - for tnf in txnotify_fun_list: - if tnf[0] == txout_set and uc_called == tnf[-1]: - txnotify_tuple = tnf - break - if txnotify_tuple == None: - log.debug('stale timeout, returning') - return - txnotify_fun_list.remove(txnotify_tuple) - log.debug('timeoutfun txout_set=\n' + pprint.pformat(txout_set)) - reactor.callFromThread(timeoutfun, uc_called) - - -class NotifyRequestHeader(BaseHTTPServer.BaseHTTPRequestHandler): - - def __init__(self, request, client_address, base_server): - self.btcinterface = base_server.btcinterface - self.base_server = base_server - BaseHTTPServer.BaseHTTPRequestHandler.__init__( - self, request, client_address, base_server) - - def do_HEAD(self): - pages = ('/walletnotify?', '/alertnotify?') - - if self.path.startswith('/walletnotify?'): - txid = self.path[len(pages[0]):] - if not re.match('^[0-9a-fA-F]*$', txid): - log.debug('not a txid') - return - try: - tx = self.btcinterface.rpc('getrawtransaction', [txid]) - except (JsonRpcError, JsonRpcConnectionError) as e: - log.debug('transaction not found, probably a conflict') - return - #the following condition shouldn't be possible I believe; - #the rpc server wil return an error as above if the tx is not found. - if not re.match('^[0-9a-fA-F]*$', tx): #pragma: no cover - log.debug('not a txhex') - return - txd = btc.deserialize(tx) - tx_output_set = set([(sv['script'], sv['value']) for sv in txd[ - 'outs']]) - - txnotify_tuple = None - unconfirmfun, confirmfun, timeoutfun, uc_called = (None, None, None, - None) - to_be_updated = [] - to_be_removed = [] - for tnf in self.btcinterface.txnotify_fun: - tx_out = tnf[0] - if tx_out == tx_output_set: - txnotify_tuple = tnf - tx_out, unconfirmfun, confirmfun, timeoutfun, uc_called = tnf - if unconfirmfun is None: - log.debug('txid=' + txid + ' not being listened for') - else: - # on rare occasions people spend their output without waiting - # for a confirm - txdata = None - for n in range(len(txd['outs'])): - txdata = self.btcinterface.rpc('gettxout', [txid, n, True]) - if txdata is not None: - break - assert txdata is not None - if txdata['confirmations'] == 0: - reactor.callFromThread(unconfirmfun, txd, txid) - to_be_updated.append(txnotify_tuple) - log.debug('ran unconfirmfun') - if timeoutfun: - threading.Timer(jm_single().config.getfloat( - 'TIMEOUT', 'confirm_timeout_hours') * 60 * 60, - bitcoincore_timeout_callback, - args=(True, tx_output_set, - self.btcinterface.txnotify_fun, - timeoutfun)).start() - else: - if not uc_called: - reactor.callFromThread(unconfirmfun, txd, txid) - log.debug('saw confirmed tx before unconfirmed, ' + - 'running unconfirmfun first') - reactor.callFromThread(confirmfun, txd, txid, txdata['confirmations']) - to_be_removed.append(txnotify_tuple) - log.debug('ran confirmfun') - #If any notifyfun tuples need to update from unconfirm to confirm state: - for tbu in to_be_updated: - self.btcinterface.txnotify_fun.remove(tbu) - self.btcinterface.txnotify_fun.append(tbu[:-1] + (True,)) - #If any notifyfun tuples need to be removed as confirmed - for tbr in to_be_removed: - self.btcinterface.txnotify_fun.remove(tbr) - - elif self.path.startswith('/alertnotify?'): - jm_single().core_alert[0] = urllib.unquote(self.path[len(pages[ - 1]):]) - log.debug('Got an alert!\nMessage=' + jm_single().core_alert[0]) - - else: - log.debug( - 'ERROR: This is not a handled URL path. You may want to check your notify URL for typos.') - - request = urllib2.Request('http://localhost:' + str( - self.base_server.server_address[1] + 1) + self.path) - request.get_method = lambda: 'HEAD' - try: - urllib2.urlopen(request) - except: - pass - self.send_response(200) - # self.send_header('Connection', 'close') - self.end_headers() - - -class BitcoinCoreNotifyThread(threading.Thread): - - def __init__(self, btcinterface): - threading.Thread.__init__(self, name='CoreNotifyThread') - self.daemon = True - self.btcinterface = btcinterface - - def run(self): - notify_host = 'localhost' - notify_port = 62602 # defaults - config = jm_single().config - if 'notify_host' in config.options("BLOCKCHAIN"): - notify_host = config.get("BLOCKCHAIN", "notify_host").strip() - if 'notify_port' in config.options("BLOCKCHAIN"): - notify_port = int(config.get("BLOCKCHAIN", "notify_port")) - for inc in range(10): - hostport = (notify_host, notify_port + inc) - try: - httpd = BaseHTTPServer.HTTPServer(hostport, NotifyRequestHeader) - except Exception: - continue - httpd.btcinterface = self.btcinterface - log.debug('started bitcoin core notify listening thread, host=' + - str(notify_host) + ' port=' + str(hostport[1])) - httpd.serve_forever() - log.debug('failed to bind for bitcoin core notify listening') - -# must run bitcoind with -server -# -walletnotify="curl -sI --connect-timeout 1 http://localhost:62602/walletnotify?%s" -# and make sure curl is installed (git uses it, odds are you've already got it) - - class BitcoinCoreInterface(BlockchainInterface): def __init__(self, jsonRpc, network): @@ -973,24 +823,46 @@ class BitcoinCoreInterface(BlockchainInterface): self.notifythread = None self.txnotify_fun = [] self.wallet_synced = False + #task.LoopingCall objects that track transactions, keyed by txids. + #Format: {"txid": (loop, unconfirmed true/false, confirmed true/false, + #spent true/false), ..} + self.tx_watcher_loops = {} @staticmethod def get_wallet_name(wallet): return 'joinmarket-wallet-' + btc.dbl_sha256(wallet.keys[0][0])[:6] + def get_block(self, blockheight): + """Returns full serialized block at a given height. + """ + block_hash = self.rpc('getblockhash', [blockheight]) + block = self.rpc('getblock', [block_hash, False]) + if not block: + return False + return block + def rpc(self, method, args): - if method not in ['importaddress', 'walletpassphrase', 'getaccount']: + if method not in ['importaddress', 'walletpassphrase', 'getaccount', + 'gettransaction', 'getrawtransaction', 'gettxout']: log.debug('rpc: ' + method + " " + str(args)) res = self.jsonRpc.call(method, args) if isinstance(res, unicode): res = str(res) return res - def add_watchonly_addresses(self, addr_list, wallet_name): + def import_addresses(self, addr_list, wallet_name): log.debug('importing ' + str(len(addr_list)) + ' addresses into account ' + wallet_name) for addr in addr_list: self.rpc('importaddress', [addr, wallet_name, False]) + + def add_watchonly_addresses(self, addr_list, wallet_name): + """For backwards compatibility, this fn name is preserved + as the case where we quit the program if a rescan is required; + but in some cases a rescan is not required (if the address is known + to be new/unused). For that case use import_addresses instead. + """ + self.import_addresses(addr_list, wallet_name) if jm_single().config.get("BLOCKCHAIN", "blockchain_source") != 'regtest': #pragma: no cover #Exit conditions cannot be included in tests @@ -1233,6 +1105,13 @@ class BitcoinCoreInterface(BlockchainInterface): self.wallet_synced = True + def start_unspent_monitoring(self, wallet): + self.unspent_monitoring_loop = task.LoopingCall(self.sync_unspent, wallet) + self.unspent_monitoring_loop.start(1.0) + + def stop_unspent_monitoring(self): + self.unspent_monitoring_loop.stop() + def sync_unspent(self, wallet): from jmclient.wallet import BitcoinCoreWallet @@ -1262,18 +1141,21 @@ class BitcoinCoreInterface(BlockchainInterface): et = time.time() log.debug('bitcoind sync_unspent took ' + str((et - st)) + 'sec') - def add_tx_notify(self, - txd, - unconfirmfun, - confirmfun, - notifyaddr, - timeoutfun=None, - vb=None): + def add_tx_notify(self, txd, unconfirmfun, confirmfun, notifyaddr, + timeoutfun=None, spentfun=None, txid_flag=True, n=0, c=1, vb=None): + """Given a deserialized transaction txd, + callback functions for broadcast and confirmation of the transaction, + an address to import, and a callback function for timeout, set up + a polling loop to check for events on the transaction. Also optionally set + to trigger "confirmed" callback on number of confirmations c. Also checks + for spending (if spentfun is not None) of the outpoint n. + If txid_flag is True, we create a watcher loop on the txid (hence only + really usable in a segwit context, and only on fully formed transactions), + else we create a watcher loop on the output set of the transaction (taken + from the outs field of the txd). + """ if not vb: vb = get_p2pk_vbyte() - if not self.notifythread: - self.notifythread = BitcoinCoreNotifyThread(self) - self.notifythread.start() one_addr_imported = False for outs in txd['outs']: addr = btc.script_to_address(outs['script'], vb) @@ -1282,17 +1164,197 @@ class BitcoinCoreInterface(BlockchainInterface): break if not one_addr_imported: self.rpc('importaddress', [notifyaddr, 'joinmarket-notify', False]) - tx_output_set = set([(sv['script'], sv['value']) for sv in txd['outs']]) - self.txnotify_fun.append((tx_output_set, unconfirmfun, confirmfun, - timeoutfun, False)) - - #create unconfirm timeout here, create confirm timeout in the other thread - if timeoutfun: - threading.Timer(jm_single().config.getint('TIMEOUT', - 'unconfirm_timeout_sec'), - bitcoincore_timeout_callback, - args=(False, tx_output_set, self.txnotify_fun, - timeoutfun)).start() + + #Warning! In case of txid_flag false, this is *not* a valid txid, + #but only a hash of an incomplete transaction serialization; but, + #it still suffices as a unique key for tracking, in this case. + txid = btc.txhash(btc.serialize(txd)) + if not txid_flag: + tx_output_set = set([(sv['script'], sv['value']) for sv in txd['outs']]) + loop = task.LoopingCall(self.outputs_watcher, notifyaddr, tx_output_set, + unconfirmfun, confirmfun, timeoutfun) + log.debug("Created watcher loop for address: " + notifyaddr) + loopkey = notifyaddr + else: + loop = task.LoopingCall(self.tx_watcher, txd, unconfirmfun, confirmfun, + spentfun, c, n) + log.debug("Created watcher loop for txid: " + txid) + loopkey = txid + self.tx_watcher_loops[loopkey] = [loop, False, False, False] + #Hardcoded polling interval, but in any case it can be very short. + loop.start(2.0) + #TODO Hardcoded very long timeout interval + reactor.callLater(7200, self.tx_timeout, txd, loopkey, timeoutfun) + + def tx_timeout(self, txd, loopkey, timeoutfun): + #TODO: 'loopkey' is an address not a txid for Makers, handle that. + if not timeoutfun: + return + if not txid in self.tx_watcher_loops: + return + if not self.tx_watcher_loops[loopkey][1]: + #Not confirmed after 2 hours; give up + log.info("Timed out waiting for confirmation of: " + str(loopkey)) + self.tx_watcher_loops[loopkey][0].stop() + timeoutfun(txd, loopkey) + + def get_deser_from_gettransaction(self, rpcretval): + """Get full transaction deserialization from a call + to `gettransaction` + """ + if not "hex" in rpcretval: + log.info("Malformed gettransaction output") + return None + #str cast for unicode + hexval = str(rpcretval["hex"]) + return btc.deserialize(hexval) + + def outputs_watcher(self, notifyaddr, tx_output_set, unconfirmfun, confirmfun, + timeoutfun): + """Given a key for the watcher loop (txid), a set of outputs, and + unconfirm, confirm and timeout callbacks, check to see if a transaction + matching that output set has appeared in the wallet. Call the callbacks + and update the watcher loop state. End the loop when the confirmation + has been seen (no spent monitoring here). + """ + wl = self.tx_watcher_loops[notifyaddr] + txlist = self.rpc("listtransactions", ["*", 1000, 0, True]) + for tx in txlist[::-1]: + #changed syntax in 0.14.0; allow both syntaxes + try: + res = self.rpc("gettransaction", [tx["txid"], True]) + except: + try: + res = self.rpc("gettransaction", [tx["txid"], 1]) + except: + #This should never happen (gettransaction is a wallet rpc). + log.info("Failed any gettransaction call") + res = None + if not res: + continue + if "confirmations" not in res: + log.debug("Malformed gettx result: " + str(res)) + return + txd = self.get_deser_from_gettransaction(res) + if txd is None: + continue + txos = set([(sv['script'], sv['value']) for sv in txd['outs']]) + if not txos == tx_output_set: + continue + #Here we have found a matching transaction in the wallet. + real_txid = btc.txhash(btc.serialize(txd)) + if not wl[1] and res["confirmations"] == 0: + log.debug("Tx: " + str(real_txid) + " seen on network.") + unconfirmfun(txd, real_txid) + wl[1] = True + return + if not wl[2] and res["confirmations"] > 0: + log.debug("Tx: " + str(real_txid) + " has " + str( + res["confirmations"]) + " confirmations.") + confirmfun(txd, real_txid, res["confirmations"]) + wl[2] = True + wl[0].stop() + return + if res["confirmations"] < 0: + log.debug("Tx: " + str(real_txid) + " has a conflict. Abandoning.") + wl[0].stop() + return + + def tx_watcher(self, txd, unconfirmfun, confirmfun, spentfun, c, n): + """Called at a polling interval, checks if the given deserialized + transaction (which must be fully signed) is (a) broadcast, (b) confirmed + and (c) spent from at index n, and notifies confirmation if number + of confs = c. + TODO: Deal with conflicts correctly. Here just abandons monitoring. + """ + txid = btc.txhash(btc.serialize(txd)) + wl = self.tx_watcher_loops[txid] + try: + res = self.rpc('gettransaction', [txid, True]) + except JsonRpcError as e: + return + if not res: + return + if "confirmations" not in res: + log.debug("Malformed gettx result: " + str(res)) + return + if not wl[1] and res["confirmations"] == 0: + log.debug("Tx: " + str(txid) + " seen on network.") + unconfirmfun(txd, txid) + wl[1] = True + return + if not wl[2] and res["confirmations"] > 0: + log.debug("Tx: " + str(txid) + " has " + str( + res["confirmations"]) + " confirmations.") + confirmfun(txd, txid, res["confirmations"]) + if c <= res["confirmations"]: + wl[2] = True + #Note we do not stop the monitoring loop when + #confirmations occur, since we are also monitoring for spending. + return + if res["confirmations"] < 0: + log.debug("Tx: " + str(txid) + " has a conflict. Abandoning.") + wl[0].stop() + return + if not spentfun or wl[3]: + return + #To trigger the spent callback, we check if this utxo outpoint appears in + #listunspent output with 0 or more confirmations. Note that this requires + #we have added the destination address to the watch-only wallet, otherwise + #that outpoint will not be returned by listunspent. + res2 = self.rpc('listunspent', [0, 999999]) + if not res2: + return + txunspent = False + for r in res2: + if "txid" not in r: + continue + if txid == r["txid"] and n == r["vout"]: + txunspent = True + break + if not txunspent: + #We need to find the transaction which spent this one; + #assuming the address was added to the wallet, then this + #transaction must be in the recent list retrieved via listunspent. + #For each one, use gettransaction to check its inputs. + #This is a bit expensive, but should only occur once. + txlist = self.rpc("listtransactions", ["*", 1000, 0, True]) + for tx in txlist[::-1]: + #changed syntax in 0.14.0; allow both syntaxes + try: + res = self.rpc("gettransaction", [tx["txid"], True]) + except: + try: + res = self.rpc("gettransaction", [tx["txid"], 1]) + except: + #This should never happen (gettransaction is a wallet rpc). + log.info("Failed any gettransaction call") + res = None + if not res: + continue + deser = self.get_deser_from_gettransaction(res) + if deser is None: + continue + for vin in deser["ins"]: + if not "outpoint" in vin: + #coinbases + continue + if vin["outpoint"]["hash"] == txid and vin["outpoint"]["index"] == n: + #recover the deserialized form of the spending transaction. + log.info("We found a spending transaction: " + \ + btc.txhash(binascii.unhexlify(res["hex"]))) + res2 = self.rpc("gettransaction", [tx["txid"], True]) + spending_deser = self.get_deser_from_gettransaction(res2) + if not spending_deser: + log.info("ERROR: could not deserialize spending tx.") + #Should never happen, it's a parsing bug. + #No point continuing to monitor, we just hope we + #can extract the secret by scanning blocks. + wl[3] = True + return + spentfun(spending_deser, vin["outpoint"]["hash"]) + wl[3] = True + return def pushtx(self, txhex): try: diff --git a/jmclient/jmclient/client_protocol.py b/jmclient/jmclient/client_protocol.py index 52fdaf3..fd1d262 100644 --- a/jmclient/jmclient/client_protocol.py +++ b/jmclient/jmclient/client_protocol.py @@ -65,6 +65,7 @@ class JMClientProtocol(amp.AMP): d.addErrback(self.defaultErrback) def connectionMade(self): + print('connection was made, starting client') self.factory.setClient(self) self.clientStart() @@ -213,6 +214,7 @@ class JMMakerClientProtocol(JMClientProtocol): jm_single().bc_interface.add_tx_notify(tx, self.unconfirm_callback, self.confirm_callback, offer["cjaddr"], + txid_flag=False, vb=get_p2sh_vbyte()) d = self.callRemote(commands.JMTXSigs, nick=nick, @@ -441,7 +443,7 @@ class JMClientProtocolFactory(protocol.ClientFactory): def buildProtocol(self, addr): return self.protocol(self, self.client) -def start_reactor(host, port, factory, ish=True, daemon=False, rs=True): #pragma: no cover +def start_reactor(host, port, factory, ish=True, daemon=False, rs=True, gui=False): #pragma: no cover #(Cannot start the reactor in tests) #Not used in prod (twisted logging): #startLogging(stdout) @@ -473,13 +475,13 @@ def start_reactor(host, port, factory, ish=True, daemon=False, rs=True): #pragma jlog.error("Tried 100 ports but cannot listen on any of them. Quitting.") sys.exit(1) port += 1 - if usessl: ctx = ClientContextFactory() reactor.connectSSL(host, port, factory, ctx) else: reactor.connectTCP(host, port, factory) if rs: - reactor.run(installSignalHandlers=ish) + if not gui: + reactor.run(installSignalHandlers=ish) if isinstance(jm_single().bc_interface, RegtestBitcoinCoreInterface): jm_single().bc_interface.shutdown_signal = True diff --git a/scripts/joinmarket-qt.py b/scripts/joinmarket-qt.py index e1fb0e8..ea427d6 100644 --- a/scripts/joinmarket-qt.py +++ b/scripts/joinmarket-qt.py @@ -23,6 +23,7 @@ Some widgets copied and modified from https://github.com/spesmilo/electrum import sys, base64, textwrap, datetime, os, logging import platform, csv, threading, time + from decimal import Decimal from functools import partial @@ -38,6 +39,9 @@ else: import jmbitcoin as btc +app = QApplication(sys.argv) +from qtreactor import pyqt4reactor +pyqt4reactor.install() #General Joinmarket donation address; TODO donation_address = "1AZgQZWYRteh6UyF87hwuvyWj73NvWKpL" @@ -64,6 +68,7 @@ from qtsupport import (ScheduleWizard, TumbleRestartWizard, warnings, config_tip PasswordDialog, MyTreeWidget, JMQtMessageBox, BLUE_FG, donation_more_message) +from twisted.internet import task def satoshis_to_amt_str(x): return str(Decimal(x)/Decimal('1e8')) + " BTC" @@ -301,25 +306,12 @@ class SpendTab(QWidget): self.initUI() self.taker = None self.filter_offers_response = None - self.taker_info_response = None self.clientfactory = None self.tumbler_options = None - #signals from client backend to GUI - self.jmclient_obj = QtCore.QObject() #timer for waiting for confirmation on restart self.restartTimer = QtCore.QTimer() #timer for wait for next transaction self.nextTxTimer = None - #This signal/callback requires user acceptance decision. - self.jmclient_obj.connect(self.jmclient_obj, QtCore.SIGNAL('JMCLIENT:offers'), - self.checkOffers) - #This signal/callback is for information only (including abort/error - #conditions which require no feedback from user. - self.jmclient_obj.connect(self.jmclient_obj, QtCore.SIGNAL('JMCLIENT:info'), - self.takerInfo) - #Signal indicating Taker has finished its work - self.jmclient_obj.connect(self.jmclient_obj, QtCore.SIGNAL('JMCLIENT:finished'), - self.takerFinished) #tracks which mode the spend tab is run in self.spendstate = SpendStateMgr(self.toggleButtons) self.spendstate.reset() #trigger callback to 'ready' state @@ -645,25 +637,12 @@ class SpendTab(QWidget): res = self.showBlockrWarning() if res == True: return - - #all settings are valid; start - #dialog removed for now, annoying, may review later - #JMQtMessageBox( - # self, - # "Connecting to IRC.\nView real-time log in the lower pane.", - # title="Coinjoin starting") - log.debug('starting coinjoin ..') - - #DON'T sync wallet since unless cache is updated, will forget index - #w.statusBar().showMessage("Syncing wallet ...") - #sync_wallet(w.wallet, fast=True) - #Decide whether to interrupt processing to sanity check the fees if self.tumbler_options: check_offers_callback = self.checkOffersTumbler elif jm_single().config.get("GUI", "checktx") == "true": - check_offers_callback = self.callback_checkOffers + check_offers_callback = self.checkOffers else: check_offers_callback = None @@ -672,119 +651,68 @@ class SpendTab(QWidget): self.spendstate.loaded_schedule, order_chooser=weighted_order_choose, callbacks=[check_offers_callback, - self.callback_takerInfo, - self.callback_takerFinished], + self.takerInfo, + self.takerFinished], tdestaddrs=destaddrs, ignored_makers=ignored_makers) if not self.clientfactory: #First run means we need to start: create clientfactory - #and start reactor Thread + #and start reactor connections self.clientfactory = JMClientProtocolFactory(self.taker) - thread = TaskThread(self) daemon = jm_single().config.getint("DAEMON", "no_daemon") daemon = True if daemon == 1 else False - thread.add(partial(start_reactor, - "localhost", + start_reactor("localhost", jm_single().config.getint("GUI", "daemon_port"), self.clientfactory, ish=False, - daemon=daemon)) + daemon=daemon, + gui=True) else: #This will re-use IRC connections in background (daemon), no restart - self.clientfactory.getClient().taker = self.taker + self.clientfactory.getClient().client = self.taker self.clientfactory.getClient().clientStart() w.statusBar().showMessage("Connecting to IRC ...") - def callback_checkOffers(self, offers_fee, cjamount): - """Receives the signal from the JMClient thread - """ - if self.taker.aborted: - log.debug("Not processing offers, user has aborted.") - return False - self.offers_fee = offers_fee - self.jmclient_obj.emit(QtCore.SIGNAL('JMCLIENT:offers')) - #The JMClient thread must wait for user input - while not self.filter_offers_response: - time.sleep(0.1) - if self.filter_offers_response == "ACCEPT": - self.filter_offers_response = None - #The user is now committed to the transaction - self.abortButton.setEnabled(False) - return True - self.filter_offers_response = None - return False - - def callback_takerInfo(self, infotype, infomsg): - if infotype == "ABORT": - self.taker_info_type = 'warn' - elif infotype == "INFO": - self.taker_info_type = 'info' - else: - raise NotImplementedError - self.taker_infomsg = infomsg - self.jmclient_obj.emit(QtCore.SIGNAL('JMCLIENT:info')) - while not self.taker_info_response: - time.sleep(0.1) - #No need to check response type, only OK for msgbox - self.taker_info_response = None - return - - def callback_takerFinished(self, res, fromtx=False, waittime=0.0, - txdetails=None): - self.taker_finished_res = res - self.taker_finished_fromtx = fromtx - self.taker_finished_waittime = waittime - self.taker_finished_txdetails = txdetails - self.jmclient_obj.emit(QtCore.SIGNAL('JMCLIENT:finished')) - return - - def takerInfo(self): - if self.taker_info_type == "info": - #cannot use dialogs that interrupt gui thread here - if len(self.taker_infomsg) > 200: - log.info("INFO: " + self.taker_infomsg) + def takerInfo(self, infotype, infomsg): + if infotype == "INFO": + #use of a dialog interrupts processing?, investigate. + if len(infomsg) > 200: + log.info("INFO: " + infomsg) else: - w.statusBar().showMessage(self.taker_infomsg) - elif self.taker_info_type == "warn": - JMQtMessageBox(self, self.taker_infomsg, - mbtype=self.taker_info_type) + w.statusBar().showMessage(infomsg) + elif infotype == "ABORT": + JMQtMessageBox(self, infomsg, + mbtype='warn') #Abort signal explicitly means this transaction will not continue. self.abortTransactions() - self.taker_info_response = True + else: + raise NotImplementedError def checkOffersTumbler(self, offers_fees, cjamount): return tumbler_filter_orders_callback(offers_fees, cjamount, self.taker, self.tumbler_options) - def checkOffers(self): + def checkOffers(self, offers_fee, cjamount): """Parse offers and total fee from client protocol, allow the user to agree or decide. """ - if not self.offers_fee: + if self.taker.aborted: + log.debug("Not processing offers, user has aborted.") + return False + + if not offers_fee: JMQtMessageBox(self, "Not enough matching offers found.", mbtype='warn', title="Error") self.giveUp() return - offers, total_cj_fee = self.offers_fee + offers, total_cj_fee = offers_fee total_fee_pc = 1.0 * total_cj_fee / self.taker.cjamount #Note this will be a new value if sweep, else same as previously entered btc_amount_str = satoshis_to_amt_str(self.taker.cjamount) - #TODO separate this out into a function mbinfo = [] - #See note above re: alerts - """ - if joinmarket_alert[0]: - mbinfo.append("JOINMARKET ALERT: " + - joinmarket_alert[0] + "") - mbinfo.append(" ") - if core_alert[0]: - mbinfo.append("BITCOIN CORE ALERT: " + - core_alert[0] + "") - mbinfo.append(" ") - """ mbinfo.append("Sending amount: " + btc_amount_str) mbinfo.append("to address: " + self.taker.my_cj_addr) mbinfo.append(" ") @@ -815,17 +743,19 @@ class SpendTab(QWidget): mbtype='question', title=title) if reply == QMessageBox.Yes: - #amount is now accepted; pass control back to reactor - self.filter_offers_response = "ACCEPT" + #amount is now accepted; + #The user is now committed to the transaction + self.abortButton.setEnabled(False) + return True else: self.filter_offers_response = "REJECT" self.giveUp() + return False def startNextTransaction(self): - #sync_wallet(w.wallet, fast=True) self.clientfactory.getClient().clientStart() - def takerFinished(self): + def takerFinished(self, res, fromtx=False, waittime=0.0, txdetails=None): """Callback (after pass-through signal) for jmclient.Taker on completion of each join transaction. """ @@ -833,10 +763,10 @@ class SpendTab(QWidget): if self.tumbler_options: sfile = os.path.join(logsdir, 'TUMBLE.schedule') 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) + self.tumbler_options, res, + fromtx, + waittime, + txdetails) self.spendstate.loaded_schedule = self.taker.schedule #Shows the schedule updates in the GUI; TODO make this more visual @@ -845,7 +775,7 @@ class SpendTab(QWidget): #GUI-specific updates; QTimer.singleShot serves the role #of reactor.callLater - if self.taker_finished_fromtx == "unconfirmed": + if fromtx == "unconfirmed": w.statusBar().showMessage( "Transaction seen on network: " + self.taker.txid) if self.spendstate.typestate == 'single': @@ -862,8 +792,8 @@ class SpendTab(QWidget): if self.spendstate.typestate == 'multiple' and not self.tumbler_options: self.taker.wallet.update_cache_index() return - if self.taker_finished_fromtx: - if self.taker_finished_res: + if fromtx: + if res: w.statusBar().showMessage("Transaction confirmed: " + self.taker.txid) #singleShot argument is in milliseconds if self.nextTxTimer: @@ -871,13 +801,13 @@ class SpendTab(QWidget): self.nextTxTimer = QtCore.QTimer() self.nextTxTimer.setSingleShot(True) self.nextTxTimer.timeout.connect(self.startNextTransaction) - self.nextTxTimer.start(int(self.taker_finished_waittime*60*1000)) + self.nextTxTimer.start(int(waittime*60*1000)) #QtCore.QTimer.singleShot(int(self.taker_finished_waittime*60*1000), # self.startNextTransaction) #see note above re multiple/tumble duplication if self.spendstate.typestate == 'multiple' and \ not self.tumbler_options: - txd, txid = self.taker_finished_txdetails + txd, txid = txdetails self.taker.wallet.remove_old_utxos(txd) self.taker.wallet.add_new_utxos(txd, txid) else: @@ -888,7 +818,7 @@ class SpendTab(QWidget): #currently does not continue for non-tumble schedules self.giveUp() else: - if self.taker_finished_res: + if res: w.statusBar().showMessage("All transaction(s) completed successfully.") if len(self.taker.schedule) == 1: msg = "Transaction has been confirmed.\n" + "Txid: " + \ @@ -1471,12 +1401,14 @@ class JMMainWindow(QMainWindow): if 'listunspent_args' not in jm_single().config.options('POLICY'): jm_single().config.set('POLICY', 'listunspent_args', '[0]') assert self.wallet, "No wallet loaded" - thread = TaskThread(self) - task = partial(sync_wallet, self.wallet, True) - thread.add(task, on_done=self.updateWalletInfo) + reactor.callLater(0, self.syncWalletUpdate, True) self.statusBar().showMessage("Reading wallet from blockchain ...") return True + def syncWalletUpdate(self, fast): + sync_wallet(self.wallet, fast=fast) + self.updateWalletInfo() + def updateWalletInfo(self): t = self.centralWidget().widget(0) if not self.wallet: #failure to sync in constructor means object is not created @@ -1611,7 +1543,6 @@ def get_wallet_printout(wallet): ################################ config_load_error = False -app = QApplication(sys.argv) try: load_program_config() except Exception as e: @@ -1630,7 +1561,8 @@ update_config_for_gui() #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 + jm_single().bc_interface.simulating = True + jm_single().maker_timeout_sec = 15 #trigger start with a fake tx jm_single().bc_interface.pushtx("00"*20) @@ -1657,5 +1589,6 @@ w.setWindowTitle(appWindowTitle + suffix) tabWidget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) w.setCentralWidget(tabWidget) w.show() - +from twisted.internet import reactor +reactor.runReturn() sys.exit(app.exec_()) diff --git a/scripts/sendpayment.py b/scripts/sendpayment.py index 6e75d19..9850669 100644 --- a/scripts/sendpayment.py +++ b/scripts/sendpayment.py @@ -98,7 +98,8 @@ def main(): #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 + jm_single().bc_interface.simulating = True + jm_single().maker_timeout_sec = 15 chooseOrdersFunc = None if options.pickorders: diff --git a/scripts/tumbler.py b/scripts/tumbler.py index c39dbc6..e16c3d3 100644 --- a/scripts/tumbler.py +++ b/scripts/tumbler.py @@ -121,7 +121,8 @@ def main(): #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 + jm_single().bc_interface.simulating = True + jm_single().maker_timeout_sec = 15 #instantiate Taker with given schedule and run taker = Taker(wallet,