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. on to the next item before we were woken up.
""" """
jlog.info("STALL MONITOR:") jlog.info("STALL MONITOR:")
if self.taker.aborted:
jlog.info("Transaction was aborted.")
return
if not self.taker.schedule_index == schedule_index: if not self.taker.schedule_index == schedule_index:
#TODO pre-initialize() ? #TODO pre-initialize() ?
jlog.info("No stall detected, continuing") jlog.info("No stall detected, continuing")

127
scripts/joinmarket-qt.py

@ -265,6 +265,28 @@ class SettingsTab(QDialog):
results.append((QLabel(label), qt)) results.append((QLabel(label), qt))
return results 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): class SpendTab(QWidget):
def __init__(self): def __init__(self):
@ -274,6 +296,7 @@ class SpendTab(QWidget):
self.filter_offers_response = None self.filter_offers_response = None
self.taker_info_response = None self.taker_info_response = None
self.clientfactory = None self.clientfactory = None
self.tumbler_options = None
#signals from client backend to GUI #signals from client backend to GUI
self.jmclient_obj = QtCore.QObject() self.jmclient_obj = QtCore.QObject()
#This signal/callback requires user acceptance decision. #This signal/callback requires user acceptance decision.
@ -288,6 +311,8 @@ class SpendTab(QWidget):
self.takerFinished) self.takerFinished)
#will be set in 'multiple join' tab if the user chooses to run a schedule #will be set in 'multiple join' tab if the user chooses to run a schedule
self.loaded_schedule = None self.loaded_schedule = None
#tracks which mode the spend tab is run in
self.spendstate = SpendStateMgr(self.toggleButtons)
def generateTumbleSchedule(self): def generateTumbleSchedule(self):
#needs a set of tumbler options and destination addresses, so needs #needs a set of tumbler options and destination addresses, so needs
@ -320,7 +345,7 @@ class SpendTab(QWidget):
else: else:
w.statusBar().showMessage("Schedule loaded OK.") w.statusBar().showMessage("Schedule loaded OK.")
self.updateSchedView(rawsched, os.path.basename(str(firstarg))) self.updateSchedView(rawsched, os.path.basename(str(firstarg)))
self.sch_startButton.setEnabled(True) self.spendstate.updateType('multiple')
self.loaded_schedule = schedule self.loaded_schedule = schedule
def updateSchedView(self, text, name): def updateSchedView(self, text, name):
@ -434,7 +459,7 @@ class SpendTab(QWidget):
'You will be prompted to decide whether to accept\n' + 'You will be prompted to decide whether to accept\n' +
'the transaction after connecting, and shown the\n' + 'the transaction after connecting, and shown the\n' +
'fees to pay; you can cancel at that point if you wish.') '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 = QPushButton('Abort')
self.abortButton.setEnabled(False) self.abortButton.setEnabled(False)
buttons = QHBoxLayout() buttons = QHBoxLayout()
@ -476,16 +501,37 @@ class SpendTab(QWidget):
self.textedit.verticalScrollBar().setValue(maxi) self.textedit.verticalScrollBar().setValue(maxi)
def startMultiple(self): def startMultiple(self):
self.qtw.setTabEnabled(0, False) if not self.spendstate.runstate == 'ready':
self.startSendPayment(multiple=True) log.info("Cannot start join, already running.")
self.taker_schedule = self.loaded_schedule
def startSendPayment(self, ignored_makers=None, multiple=False): #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: if not w.wallet:
JMQtMessageBox(self, "Cannot start without a loaded wallet.", JMQtMessageBox(self, "Cannot start without a loaded wallet.",
mbtype="crit", title="Error") mbtype="crit", title="Error")
return return
if not multiple and not self.validateSettings():
return
if jm_single().config.get("BLOCKCHAIN", if jm_single().config.get("BLOCKCHAIN",
"blockchain_source") == 'blockr': "blockchain_source") == 'blockr':
res = self.showBlockrWarning() res = self.showBlockrWarning()
@ -493,29 +539,16 @@ class SpendTab(QWidget):
return return
#all settings are valid; start #all settings are valid; start
JMQtMessageBox( #dialog removed for now, annoying, may review later
self, #JMQtMessageBox(
"Connecting to IRC.\nView real-time log in the lower pane.", # self,
title="Coinjoin starting") # "Connecting to IRC.\nView real-time log in the lower pane.",
# title="Coinjoin starting")
self.toggleButtons(False, sched=multiple)
log.debug('starting coinjoin ..') log.debug('starting coinjoin ..')
w.statusBar().showMessage("Syncing wallet ...") w.statusBar().showMessage("Syncing wallet ...")
sync_wallet(w.wallet, fast=True) 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 #Decide whether to interrupt processing to sanity check the fees
if jm_single().config.get("GUI", "checktx") == "true": if jm_single().config.get("GUI", "checktx") == "true":
@ -675,7 +708,8 @@ class SpendTab(QWidget):
""" """
sfile = os.path.join(logsdir, 'TUMBLE.schedule') sfile = os.path.join(logsdir, 'TUMBLE.schedule')
#non-GUI-specific state updates first: #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.tumbler_options, self.taker_finished_res,
self.taker_finished_fromtx, self.taker_finished_fromtx,
self.taker_finished_waittime, self.taker_finished_waittime,
@ -727,22 +761,29 @@ class SpendTab(QWidget):
txhist = w.centralWidget().widget(3) txhist = w.centralWidget().widget(3)
txhist.updateTxInfo() txhist.updateTxInfo()
def toggleButtons(self, on, sched=False): def toggleButtons(self):
"""If first arg is True, set all buttons "on" except "Abort" buttons. """Refreshes accessibility of buttons in the (single, multiple) join
(This is the starting condition, and reset condition). tabs based on the current state as defined by the SpendStateMgr instance.
If first arg is False, do the opposite, and: Thus, should always be called on any update to that instance.
If sched, the Abort button is only activated for the Multiple tab.
Else, the Abort button is only activated for the Single tab.
""" """
btnsettings = (True, False, True, True, True, False) #The first two buttons are for the single join tab; the remaining 4
if not on: #are for the multijoin tab.
btnsettings = [False, True, False, False, False, False]
if sched:
btnsettings[1] = False
btnsettings[5] = True
btns = (self.startButton, self.abortButton, btns = (self.startButton, self.abortButton,
self.schedule_set_button, self.schedule_generate_button, self.schedule_set_button, self.schedule_generate_button,
self.sch_startButton, self.sch_abortButton) 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): for b, s in zip(btns, btnsettings):
b.setEnabled(s) b.setEnabled(s)
@ -754,7 +795,8 @@ class SpendTab(QWidget):
log.debug("Transaction aborted.") log.debug("Transaction aborted.")
self.qtw.setTabEnabled(0, True) self.qtw.setTabEnabled(0, True)
self.qtw.setTabEnabled(1, True) self.qtw.setTabEnabled(1, True)
self.toggleButtons(True) self.spendstate.reset()
self.tumbler_options = None
w.statusBar().showMessage("Transaction aborted.") w.statusBar().showMessage("Transaction aborted.")
def cleanUp(self): def cleanUp(self):
@ -780,14 +822,15 @@ class SpendTab(QWidget):
mbtype='question', mbtype='question',
title="Transaction not completed.") title="Transaction not completed.")
if reply == QMessageBox.Yes: if reply == QMessageBox.Yes:
self.startSendPayment( self.startJoin(
ignored_makers=self.taker.ignored_makers) ignored_makers=self.taker.ignored_makers)
else: else:
self.giveUp() self.giveUp()
return return
self.qtw.setTabEnabled(0, True) self.qtw.setTabEnabled(0, True)
self.qtw.setTabEnabled(1, True) self.qtw.setTabEnabled(1, True)
self.toggleButtons(True) self.spendstate.reset()
self.tumbler_options = None
def validateSettings(self): def validateSettings(self):
valid, errmsg = validate_address(self.widgets[0][1].text()) 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, A sample implementation of a single coinjoin script,
adapted from `sendpayment.py` in Joinmarket-Org/joinmarket. adapted from `sendpayment.py` in Joinmarket-Org/joinmarket.
This is designed For notes, see scripts/README.md; in particular, note the use
to illustrate the main functionality of the new architecture: of "schedules" with the -S flag.
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.
""" """
import random import random
@ -286,9 +252,6 @@ def main():
log.info("All transactions completed correctly") log.info("All transactions completed correctly")
reactor.stop() 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, taker = Taker(wallet,
schedule, schedule,
order_chooser=chooseOrdersFunc, order_chooser=chooseOrdersFunc,

Loading…
Cancel
Save