Browse Source

Track state in Qt UI to enable actions for single/multi join.

Created simple state tracking class SpendStateMgr.
Also: remove stale comments and test code from sendpayment.
Also: don't continue aborted Taker in stallMonitor.
master
Adam Gibson 9 years ago
parent
commit
0752f4df58
No known key found for this signature in database
GPG Key ID: B3AE09F1E9A3197A
  1. 3
      jmclient/jmclient/client_protocol.py
  2. 127
      scripts/joinmarket-qt.py
  3. 41
      scripts/sendpayment.py

3
jmclient/jmclient/client_protocol.py

@ -105,6 +105,9 @@ class JMTakerClientProtocol(amp.AMP):
on to the next item before we were woken up.
"""
jlog.info("STALL MONITOR:")
if self.taker.aborted:
jlog.info("Transaction was aborted.")
return
if not self.taker.schedule_index == schedule_index:
#TODO pre-initialize() ?
jlog.info("No stall detected, continuing")

127
scripts/joinmarket-qt.py

@ -265,6 +265,28 @@ class SettingsTab(QDialog):
results.append((QLabel(label), qt))
return results
class SpendStateMgr(object):
"""A primitive class keep track of the mode
in which the spend tab is being run
"""
def __init__(self, updatecallback):
self.typestate = 'single'
self.runstate = 'ready'
self.updatecallback = updatecallback
def updateType(self, t):
self.typestate = t
self.updatecallback()
def updateRun(self, r):
self.runstate = r
self.updatecallback()
def reset(self):
self.typestate = 'single'
self.runstate = 'ready'
self.updatecallback()
class SpendTab(QWidget):
def __init__(self):
@ -274,6 +296,7 @@ class SpendTab(QWidget):
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()
#This signal/callback requires user acceptance decision.
@ -288,6 +311,8 @@ class SpendTab(QWidget):
self.takerFinished)
#will be set in 'multiple join' tab if the user chooses to run a schedule
self.loaded_schedule = None
#tracks which mode the spend tab is run in
self.spendstate = SpendStateMgr(self.toggleButtons)
def generateTumbleSchedule(self):
#needs a set of tumbler options and destination addresses, so needs
@ -320,7 +345,7 @@ class SpendTab(QWidget):
else:
w.statusBar().showMessage("Schedule loaded OK.")
self.updateSchedView(rawsched, os.path.basename(str(firstarg)))
self.sch_startButton.setEnabled(True)
self.spendstate.updateType('multiple')
self.loaded_schedule = schedule
def updateSchedView(self, text, name):
@ -434,7 +459,7 @@ class SpendTab(QWidget):
'You will be prompted to decide whether to accept\n' +
'the transaction after connecting, and shown the\n' +
'fees to pay; you can cancel at that point if you wish.')
self.startButton.clicked.connect(self.startSendPayment)
self.startButton.clicked.connect(self.startSingle)
self.abortButton = QPushButton('Abort')
self.abortButton.setEnabled(False)
buttons = QHBoxLayout()
@ -476,16 +501,37 @@ class SpendTab(QWidget):
self.textedit.verticalScrollBar().setValue(maxi)
def startMultiple(self):
self.qtw.setTabEnabled(0, False)
self.startSendPayment(multiple=True)
def startSendPayment(self, ignored_makers=None, multiple=False):
if not self.spendstate.runstate == 'ready':
log.info("Cannot start join, already running.")
self.taker_schedule = self.loaded_schedule
#self.qtw.setTabEnabled(0, False)
self.spendstate.updateType('multiple')
self.spendstate.updateRun('running')
self.startJoin()
def startSingle(self):
if not self.spendstate.runstate == 'ready':
log.info("Cannot start join, already running.")
if not self.validateSettings():
return
destaddr = str(self.widgets[0][1].text())
#convert from bitcoins (enforced by QDoubleValidator) to satoshis
btc_amount_str = str(self.widgets[3][1].text())
amount = int(Decimal(btc_amount_str) * Decimal('1e8'))
makercount = int(self.widgets[1][1].text())
mixdepth = int(self.widgets[2][1].text())
#note 'amount' is integer, so not interpreted as fraction
#see notes in sample testnet schedule for format
self.taker_schedule = [[mixdepth, amount, makercount, destaddr, 0, 0]]
self.spendstate.updateType('single')
self.spendstate.updateRun('running')
self.startJoin()
def startJoin(self, ignored_makers=None):
if not w.wallet:
JMQtMessageBox(self, "Cannot start without a loaded wallet.",
mbtype="crit", title="Error")
return
if not multiple and not self.validateSettings():
return
if jm_single().config.get("BLOCKCHAIN",
"blockchain_source") == 'blockr':
res = self.showBlockrWarning()
@ -493,29 +539,16 @@ class SpendTab(QWidget):
return
#all settings are valid; start
JMQtMessageBox(
self,
"Connecting to IRC.\nView real-time log in the lower pane.",
title="Coinjoin starting")
self.toggleButtons(False, sched=multiple)
#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 ..')
w.statusBar().showMessage("Syncing wallet ...")
sync_wallet(w.wallet, fast=True)
if not multiple:
destaddr = str(self.widgets[0][1].text())
#convert from bitcoins (enforced by QDoubleValidator) to satoshis
btc_amount_str = str(self.widgets[3][1].text())
amount = int(Decimal(btc_amount_str) * Decimal('1e8'))
makercount = int(self.widgets[1][1].text())
mixdepth = int(self.widgets[2][1].text())
#note 'amount' is integer, so not interpreted as fraction
self.taker_schedule = [[mixdepth, amount, makercount, destaddr, 0]]
else:
assert self.loaded_schedule
self.taker_schedule = self.loaded_schedule
#Decide whether to interrupt processing to sanity check the fees
if jm_single().config.get("GUI", "checktx") == "true":
@ -675,7 +708,8 @@ class SpendTab(QWidget):
"""
sfile = os.path.join(logsdir, 'TUMBLE.schedule')
#non-GUI-specific state updates first:
tumbler_taker_finished_update(self.taker, sfile, tumble_log,
if self.tumbler_options:
tumbler_taker_finished_update(self.taker, sfile, tumble_log,
self.tumbler_options, self.taker_finished_res,
self.taker_finished_fromtx,
self.taker_finished_waittime,
@ -727,22 +761,29 @@ class SpendTab(QWidget):
txhist = w.centralWidget().widget(3)
txhist.updateTxInfo()
def toggleButtons(self, on, sched=False):
"""If first arg is True, set all buttons "on" except "Abort" buttons.
(This is the starting condition, and reset condition).
If first arg is False, do the opposite, and:
If sched, the Abort button is only activated for the Multiple tab.
Else, the Abort button is only activated for the Single tab.
def toggleButtons(self):
"""Refreshes accessibility of buttons in the (single, multiple) join
tabs based on the current state as defined by the SpendStateMgr instance.
Thus, should always be called on any update to that instance.
"""
btnsettings = (True, False, True, True, True, False)
if not on:
btnsettings = [False, True, False, False, False, False]
if sched:
btnsettings[1] = False
btnsettings[5] = True
#The first two buttons are for the single join tab; the remaining 4
#are for the multijoin tab.
btns = (self.startButton, self.abortButton,
self.schedule_set_button, self.schedule_generate_button,
self.sch_startButton, self.sch_abortButton)
if self.spendstate.runstate == 'ready':
btnsettings = (True, False, True, True, True, False)
elif self.spendstate.runstate == 'running':
if self.spendstate.typestate == 'single':
#can only abort current run, nothing else
btnsettings = (False, True, False, False, False, False)
elif self.spendstate.typestate == 'multiple':
btnsettings = (False, False, False, False, False, True)
else:
assert False
else:
assert False
for b, s in zip(btns, btnsettings):
b.setEnabled(s)
@ -754,7 +795,8 @@ class SpendTab(QWidget):
log.debug("Transaction aborted.")
self.qtw.setTabEnabled(0, True)
self.qtw.setTabEnabled(1, True)
self.toggleButtons(True)
self.spendstate.reset()
self.tumbler_options = None
w.statusBar().showMessage("Transaction aborted.")
def cleanUp(self):
@ -780,14 +822,15 @@ class SpendTab(QWidget):
mbtype='question',
title="Transaction not completed.")
if reply == QMessageBox.Yes:
self.startSendPayment(
self.startJoin(
ignored_makers=self.taker.ignored_makers)
else:
self.giveUp()
return
self.qtw.setTabEnabled(0, True)
self.qtw.setTabEnabled(1, True)
self.toggleButtons(True)
self.spendstate.reset()
self.tumbler_options = None
def validateSettings(self):
valid, errmsg = validate_address(self.widgets[0][1].text())

41
scripts/sendpayment.py

@ -4,42 +4,8 @@ from __future__ import absolute_import, print_function
"""
A sample implementation of a single coinjoin script,
adapted from `sendpayment.py` in Joinmarket-Org/joinmarket.
This is designed
to illustrate the main functionality of the new architecture:
this code can be run in a separate environment (but not safely
over the internet, better on one machine) to the joinmarketdaemon.
Moreover, it can run several transactions as specified in a "schedule", like:
[(mixdepth, amount, N, destination),(m,a,N,d),..]
call it like the normal Joinmarket sendpayment, but optionally add
a port for the daemon:
`python sendpayment.py -p 27183 -N 3 -m 1 walletseed amount address`;
Schedule can be read from a file with the -S option, in which case no need to
provide amount, mixdepth, number of counterparties or destination from command line.
The idea is that the "backend" (daemon) will keep its orderbook and stay
connected on the message channel between runs, only shutting down
after all are complete. Joins are sequenced using the wallet-notify function as
previously for Joinmarket.
It should be very easy to extend this further, of course.
More complex applications can extend from Taker and add
more features. This will also allow
easier coding of non-CLI interfaces. A plugin for Electrum is in process
and already working.
Other potential customisations of the Taker object instantiation
include:
external_addr=None implies joining to another mixdepth
in the same wallet.
order_chooser can be set to a different custom function that selects
counterparty offers according to different rules.
For notes, see scripts/README.md; in particular, note the use
of "schedules" with the -S flag.
"""
import random
@ -286,9 +252,6 @@ def main():
log.info("All transactions completed correctly")
reactor.stop()
if isinstance(jm_single().bc_interface, RegtestBitcoinCoreInterface):
#to allow testing of confirm/unconfirm callback for multiple txs
jm_single().bc_interface.tick_forward_chain_interval = 10
taker = Taker(wallet,
schedule,
order_chooser=chooseOrdersFunc,

Loading…
Cancel
Save