Browse Source

Overhall bci to use twisted only, no threads

Updates to joinmarket-qt to reflect; no signals, only callbacks
Includes code for watching txs both via notify addr and txs;
taker uses the latter (knows full txid), maker the former
Additional code for watching tx spends included for downstream.
master
Adam Gibson 8 years ago
parent
commit
d50cc0eca8
No known key found for this signature in database
GPG Key ID: B3AE09F1E9A3197A
  1. 410
      jmclient/jmclient/blockchaininterface.py
  2. 8
      jmclient/jmclient/client_protocol.py
  3. 179
      scripts/joinmarket-qt.py
  4. 3
      scripts/sendpayment.py
  5. 3
      scripts/tumbler.py

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

8
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

179
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("<b><font color=red>JOINMARKET ALERT: " +
joinmarket_alert[0] + "</font></b>")
mbinfo.append(" ")
if core_alert[0]:
mbinfo.append("<b><font color=red>BITCOIN CORE ALERT: " +
core_alert[0] + "</font></b>")
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_())

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

3
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,

Loading…
Cancel
Save