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,