diff --git a/jmclient/jmclient/__init__.py b/jmclient/jmclient/__init__.py index c05af72..df0e0b9 100644 --- a/jmclient/jmclient/__init__.py +++ b/jmclient/jmclient/__init__.py @@ -28,7 +28,7 @@ from .podle import (set_commitment_file, get_commitment_file, generate_podle_error_string, add_external_commitments, PoDLE, generate_podle, get_podle_commitments, update_commitments) -from .schedule import get_schedule, get_tumble_schedule +from .schedule import get_schedule, get_tumble_schedule, schedule_to_text from .commitment_utils import get_utxo_info, validate_utxo_data, quit # Set default logging handler to avoid "No handler found" warnings. diff --git a/jmclient/jmclient/schedule.py b/jmclient/jmclient/schedule.py index 96e64c4..dcd81d4 100644 --- a/jmclient/jmclient/schedule.py +++ b/jmclient/jmclient/schedule.py @@ -108,4 +108,7 @@ def get_tumble_schedule(options, destaddrs): for t in tx_list: schedule.append((t['srcmixdepth'], t['amount_fraction'], t['makercount'], t['destination'], t['wait'])) - return schedule \ No newline at end of file + return schedule + +def schedule_to_text(schedule): + return "\n".join([",".join([str(y) for y in x]) for x in schedule]) \ No newline at end of file diff --git a/scripts/joinmarket-qt.py b/scripts/joinmarket-qt.py index 8eb618d..c9c9d4a 100644 --- a/scripts/joinmarket-qt.py +++ b/scripts/joinmarket-qt.py @@ -20,12 +20,11 @@ Some widgets copied and modified from https://github.com/spesmilo/electrum along with this program. If not, see . ''' -import sys, base64, textwrap, re, datetime, os, math, json, logging +import sys, base64, textwrap, datetime, os, logging import Queue, platform, csv, threading, time from decimal import Decimal from functools import partial -from collections import namedtuple from PyQt4 import QtCore from PyQt4.QtGui import * @@ -37,12 +36,6 @@ elif platform.system() == 'Darwin': else: MONOSPACE_FONT = 'monospace' -GREEN_BG = "QWidget {background-color:#80ff80;}" -RED_BG = "QWidget {background-color:#ffcccc;}" -RED_FG = "QWidget {color:red;}" -BLUE_FG = "QWidget {color:blue;}" -BLACK_FG = "QWidget {color:black;}" - import jmbitcoin as btc JM_CORE_VERSION = '0.2.2' @@ -52,92 +45,20 @@ from jmclient import (load_program_config, get_network, Wallet, get_p2pk_vbyte, jm_single, validate_address, get_log, weighted_order_choose, Taker, JMTakerClientProtocolFactory, WalletError, - start_reactor, get_schedule, get_tumble_schedule) -#from joinmarket import load_program_config, get_network, Wallet, encryptData, \ -# get_p2pk_vbyte, jm_single, mn_decode, mn_encode, create_wallet_file, \ -# validate_address, random_nick, get_log, IRCMessageChannel, \ -# weighted_order_choose, get_blockchain_interface_instance, joinmarket_alert, \ -# core_alert + start_reactor, get_schedule, get_tumble_schedule, + schedule_to_text) + +from qtsupport import (ScheduleWizard, warnings, config_tips, config_types, + TaskThread, QtHandler, XStream, Buttons, CloseButton, + CopyButton, CopyCloseButton, OkButton, CancelButton, + HelpLabel, check_password_strength, + update_password_strength, make_password_dialog, + PasswordDialog, MyTreeWidget, JMQtMessageBox) def satoshis_to_amt_str(x): return str(Decimal(x)/Decimal('1e8')) + " BTC" log = get_log() -donation_address = '1LT6rwv26bV7mgvRosoSCyGM7ttVRsYidP' -donation_address_testnet = 'mz6FQosuiNe8135XaQqWYmXsa3aD8YsqGL' - -warnings = {"blockr_privacy": """You are using blockr as your method of -connecting to the blockchain; this means -that blockr.com can see the addresses you -query. This is bad for privacy - consider -using a Bitcoin Core node instead."""} -#configuration types -config_types = {'rpc_port': int, - 'port': int, - 'usessl': bool, - 'socks5': bool, - 'network': bool, - 'checktx': bool, - 'socks5_port': int, - 'maker_timeout_sec': int, - 'tx_fees': int, - 'gaplimit': int, - 'check_high_fee': int, - 'max_mix_depth': int, - 'txfee_default': int, - 'order_wait_time': int, - 'privacy_warning': None} -config_tips = { - 'blockchain_source': 'options: blockr, bitcoin-rpc', - 'network': 'one of "testnet" or "mainnet"', - 'checktx': 'whether to check fees before completing transaction', - 'rpc_host': - 'the host for bitcoind; only used if blockchain_source is bitcoin-rpc', - 'rpc_port': 'port for connecting to bitcoind over rpc', - 'rpc_user': 'user for connecting to bitcoind over rpc', - 'rpc_password': 'password for connecting to bitcoind over rpc', - 'host': 'hostname for IRC server', - 'channel': 'channel name on IRC server', - 'port': 'port for connecting to IRC server', - 'usessl': 'check to use SSL for connection to IRC', - 'socks5': 'check to use SOCKS5 proxy for IRC connection', - 'socks5_host': 'host for SOCKS5 proxy', - 'socks5_port': 'port for SOCKS5 proxy', - 'maker_timeout_sec': 'timeout for waiting for replies from makers', - 'merge_algorithm': 'for dust sweeping, try merge_algorithm = gradual, \n' + - 'for more rapid dust sweeping, try merge_algorithm = greedy \n' + - 'for most rapid dust sweeping, try merge_algorithm = greediest \n' + - ' but dont forget to bump your miner fees!', - 'tx_fees': - 'the fee estimate is based on a projection of how many satoshis \n' + - 'per kB are needed to get in one of the next N blocks, N set here \n' + - 'as the value of "tx_fees". This estimate is high if you set N=1, \n' + - 'so we choose N=3 for a more reasonable figure, \n' + - 'as our default. Note that for clients not using a local blockchain \n' + - 'instance, we retrieve an estimate from the API at blockcypher.com, currently. \n', - 'gaplimit': 'How far forward to search for used addresses in the HD wallet', - 'check_high_fee': 'Percent fee considered dangerously high, default 2%', - 'max_mix_depth': 'Total number of mixdepths in the wallet, default 5', - 'txfee_default': 'Number of satoshis per counterparty for an initial\n' + - 'tx fee estimate; this value is not usually used and is best left at\n' + - 'the default of 5000', - 'order_wait_time': 'How long to wait for orders to arrive on entering\n' + - 'the message channel, default is 30s' -} - - -def JMQtMessageBox(obj, msg, mbtype='info', title=''): - mbtypes = {'info': QMessageBox.information, - 'crit': QMessageBox.critical, - 'warn': QMessageBox.warning, - 'question': QMessageBox.question} - title = "JoinmarketQt - " + title - if mbtype == 'question': - return QMessageBox.question(obj, title, msg, QMessageBox.Yes, - QMessageBox.No) - else: - mbtypes[mbtype](obj, title, msg) - def update_config_for_gui(): '''The default joinmarket config does not contain these GUI settings @@ -205,395 +126,11 @@ def getSettingsWidgets(): results.append((ql, qle)) return results -class TaskThread(QtCore.QThread): - '''Thread that runs background tasks. Callbacks are guaranteed - to happen in the context of its parent.''' - - Task = namedtuple("Task", "task cb_success cb_done cb_error") - doneSig = QtCore.pyqtSignal(object, object, object) - - def __init__(self, parent, on_error=None): - super(TaskThread, self).__init__(parent) - self.on_error = on_error - self.tasks = Queue.Queue() - self.doneSig.connect(self.on_done) - self.start() - - def add(self, task, on_success=None, on_done=None, on_error=None): - on_error = on_error or self.on_error - self.tasks.put(TaskThread.Task(task, on_success, on_done, on_error)) - - def run(self): - while True: - task = self.tasks.get() - if not task: - break - try: - result = task.task() - self.doneSig.emit(result, task.cb_done, task.cb_success) - except BaseException: - self.doneSig.emit(sys.exc_info(), task.cb_done, task.cb_error) - - def on_done(self, result, cb_done, cb): - # This runs in the parent's thread. - if cb_done: - cb_done() - if cb: - cb(result) - - def stop(self): - self.tasks.put(None) - -class QtHandler(logging.Handler): - - def __init__(self): - logging.Handler.__init__(self) - - def emit(self, record): - record = self.format(record) - if record: XStream.stdout().write('%s\n' % record) - handler = QtHandler() handler.setFormatter(logging.Formatter("%(levelname)s:%(message)s")) log.addHandler(handler) - -class XStream(QtCore.QObject): - _stdout = None - _stderr = None - messageWritten = QtCore.pyqtSignal(str) - - def flush(self): - pass - - def fileno(self): - return -1 - - def write(self, msg): - if (not self.signalsBlocked()): - self.messageWritten.emit(unicode(msg)) - - @staticmethod - def stdout(): - if (not XStream._stdout): - XStream._stdout = XStream() - sys.stdout = XStream._stdout - return XStream._stdout - - @staticmethod - def stderr(): - if (not XStream._stderr): - XStream._stderr = XStream() - sys.stderr = XStream._stderr - return XStream._stderr - - -class Buttons(QHBoxLayout): - - def __init__(self, *buttons): - QHBoxLayout.__init__(self) - self.addStretch(1) - for b in buttons: - self.addWidget(b) - - -class CloseButton(QPushButton): - - def __init__(self, dialog): - QPushButton.__init__(self, "Close") - self.clicked.connect(dialog.close) - self.setDefault(True) - - -class CopyButton(QPushButton): - - def __init__(self, text_getter, app): - QPushButton.__init__(self, "Copy") - self.clicked.connect(lambda: app.clipboard().setText(text_getter())) - - -class CopyCloseButton(QPushButton): - - def __init__(self, text_getter, app, dialog): - QPushButton.__init__(self, "Copy and Close") - self.clicked.connect(lambda: app.clipboard().setText(text_getter())) - self.clicked.connect(dialog.close) - self.setDefault(True) - - -class OkButton(QPushButton): - - def __init__(self, dialog, label=None): - QPushButton.__init__(self, label or "OK") - self.clicked.connect(dialog.accept) - self.setDefault(True) - - -class CancelButton(QPushButton): - - def __init__(self, dialog, label=None): - QPushButton.__init__(self, label or "Cancel") - self.clicked.connect(dialog.reject) - - -class HelpLabel(QLabel): - - def __init__(self, text, help_text, wtitle): - QLabel.__init__(self, text) - self.help_text = help_text - self.wtitle = wtitle - self.font = QFont() - self.setStyleSheet(BLUE_FG) - - def mouseReleaseEvent(self, x): - QMessageBox.information(w, self.wtitle, self.help_text, 'OK') - - def enterEvent(self, event): - self.font.setUnderline(True) - self.setFont(self.font) - app.setOverrideCursor(QCursor(QtCore.Qt.PointingHandCursor)) - return QLabel.enterEvent(self, event) - - def leaveEvent(self, event): - self.font.setUnderline(False) - self.setFont(self.font) - app.setOverrideCursor(QCursor(QtCore.Qt.ArrowCursor)) - return QLabel.leaveEvent(self, event) - - -def check_password_strength(password): - ''' - Check the strength of the password entered by the user and return back the same - :param password: password entered by user in New Password - :return: password strength Weak or Medium or Strong - ''' - password = unicode(password) - n = math.log(len(set(password))) - num = re.search("[0-9]", password) is not None and re.match( - "^[0-9]*$", password) is None - caps = password != password.upper() and password != password.lower() - extra = re.match("^[a-zA-Z0-9]*$", password) is None - score = len(password) * (n + caps + num + extra) / 20 - password_strength = {0: "Weak", 1: "Medium", 2: "Strong", 3: "Very Strong"} - return password_strength[min(3, int(score))] - - -def update_password_strength(pw_strength_label, password): - ''' - call the function check_password_strength and update the label pw_strength - interactively as the user is typing the password - :param pw_strength_label: the label pw_strength - :param password: password entered in New Password text box - :return: None - ''' - if password: - colors = {"Weak": "Red", - "Medium": "Blue", - "Strong": "Green", - "Very Strong": "Green"} - strength = check_password_strength(password) - label = "Password Strength"+ ": "+"" + strength + "" - else: - label = "" - pw_strength_label.setText(label) - - -def make_password_dialog(self, msg, new_pass=True): - - self.new_pw = QLineEdit() - self.new_pw.setEchoMode(2) - self.conf_pw = QLineEdit() - self.conf_pw.setEchoMode(2) - - vbox = QVBoxLayout() - label = QLabel(msg) - label.setWordWrap(True) - - grid = QGridLayout() - grid.setSpacing(8) - grid.setColumnMinimumWidth(0, 70) - grid.setColumnStretch(1, 1) - #TODO perhaps add an icon here - logo = QLabel() - lockfile = ":icons/lock.png" - logo.setPixmap(QPixmap(lockfile).scaledToWidth(36)) - logo.setAlignment(QtCore.Qt.AlignCenter) - - grid.addWidget(logo, 0, 0) - grid.addWidget(label, 0, 1, 1, 2) - vbox.addLayout(grid) - - grid = QGridLayout() - grid.setSpacing(8) - grid.setColumnMinimumWidth(0, 250) - grid.setColumnStretch(1, 1) - - grid.addWidget(QLabel('New Password' if new_pass else 'Password'), 1, 0) - grid.addWidget(self.new_pw, 1, 1) - - grid.addWidget(QLabel('Confirm Password'), 2, 0) - grid.addWidget(self.conf_pw, 2, 1) - vbox.addLayout(grid) - - #Password Strength Label - self.pw_strength = QLabel() - grid.addWidget(self.pw_strength, 3, 0, 1, 2) - self.new_pw.textChanged.connect( - lambda: update_password_strength(self.pw_strength, self.new_pw.text())) - - vbox.addStretch(1) - vbox.addLayout(Buttons(CancelButton(self), OkButton(self))) - return vbox - - -class PasswordDialog(QDialog): - - def __init__(self): - super(PasswordDialog, self).__init__() - self.initUI() - - def initUI(self): - self.setWindowTitle('Create a new password') - msg = "Enter a new password" - self.setLayout(make_password_dialog(self, msg)) - self.show() - - -class MyTreeWidget(QTreeWidget): - - def __init__(self, - parent, - create_menu, - headers, - stretch_column=None, - editable_columns=None): - QTreeWidget.__init__(self, parent) - self.parent = parent - self.stretch_column = stretch_column - self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - self.customContextMenuRequested.connect(create_menu) - self.setUniformRowHeights(True) - # extend the syntax for consistency - self.addChild = self.addTopLevelItem - self.insertChild = self.insertTopLevelItem - self.editor = None - self.pending_update = False - if editable_columns is None: - editable_columns = [stretch_column] - self.editable_columns = editable_columns - self.itemActivated.connect(self.on_activated) - self.update_headers(headers) - - def update_headers(self, headers): - self.setColumnCount(len(headers)) - self.setHeaderLabels(headers) - self.header().setStretchLastSection(False) - for col in range(len(headers)): - sm = QHeaderView.Stretch if col == self.stretch_column else QHeaderView.ResizeToContents - self.header().setResizeMode(col, sm) - - def editItem(self, item, column): - if column in self.editable_columns: - self.editing_itemcol = (item, column, unicode(item.text(column))) - # Calling setFlags causes on_changed events for some reason - item.setFlags(item.flags() | Qt.ItemIsEditable) - QTreeWidget.editItem(self, item, column) - item.setFlags(item.flags() & ~Qt.ItemIsEditable) - - def keyPressEvent(self, event): - if event.key() == QtCore.Qt.Key_F2: - self.on_activated(self.currentItem(), self.currentColumn()) - else: - QTreeWidget.keyPressEvent(self, event) - - def permit_edit(self, item, column): - return (column in self.editable_columns and - self.on_permit_edit(item, column)) - - def on_permit_edit(self, item, column): - return True - - def on_activated(self, item, column): - if self.permit_edit(item, column): - self.editItem(item, column) - else: - pt = self.visualItemRect(item).bottomLeft() - pt.setX(50) - self.emit( - QtCore.SIGNAL('customContextMenuRequested(const QPoint&)'), pt) - - def createEditor(self, parent, option, index): - self.editor = QStyledItemDelegate.createEditor(self.itemDelegate(), - parent, option, index) - self.editor.connect(self.editor, QtCore.SIGNAL("editingFinished()"), - self.editing_finished) - return self.editor - - def editing_finished(self): - # Long-time QT bug - pressing Enter to finish editing signals - # editingFinished twice. If the item changed the sequence is - # Enter key: editingFinished, on_change, editingFinished - # Mouse: on_change, editingFinished - # This mess is the cleanest way to ensure we make the - # on_edited callback with the updated item - if self.editor: - (item, column, prior_text) = self.editing_itemcol - if self.editor.text() == prior_text: - self.editor = None # Unchanged - ignore any 2nd call - elif item.text(column) == prior_text: - pass # Buggy first call on Enter key, item not yet updated - else: - # What we want - the updated item - self.on_edited(*self.editing_itemcol) - self.editor = None - - # Now do any pending updates - if self.editor is None and self.pending_update: - self.pending_update = False - self.on_update() - - def on_edited(self, item, column, prior): - '''Called only when the text actually changes''' - key = str(item.data(0, Qt.UserRole).toString()) - text = unicode(item.text(column)) - self.parent.wallet.set_label(key, text) - if text: - item.setForeground(column, QBrush(QColor('black'))) - else: - text = self.parent.wallet.get_default_label(key) - item.setText(column, text) - item.setForeground(column, QBrush(QColor('gray'))) - self.parent.history_list.update() - self.parent.update_completions() - - def update(self): - # Defer updates if editing - if self.editor: - self.pending_update = True - else: - self.on_update() - - def on_update(self): - pass - - def get_leaves(self, root): - child_count = root.childCount() - if child_count == 0: - yield root - for i in range(child_count): - item = root.child(i) - for x in self.get_leaves(item): - yield x - - def filter(self, p, columns): - p = unicode(p).lower() - for item in self.get_leaves(self.invisibleRootItem()): - item.setHidden(all([unicode(item.text(column)).lower().find(p) == -1 - for column in columns])) - - class SettingsTab(QDialog): def __init__(self): @@ -694,163 +231,6 @@ class SettingsTab(QDialog): results.append((QLabel(label), qt)) return results -""" TODO implement this option -class SchStaticPage(QWizardPage): - def __init__(self, parent): - super(SchStaticPage, self).__init__(parent) - self.setTitle("Manually create a schedule entry") - layout = QGridLayout() - wdgts = getSettingsWidgets() - for i, x in enumerate(wdgts): - layout.addWidget(x[0], i + 1, 0) - layout.addWidget(x[1], i + 1, 1, 1, 2) - wdgts[0][1].editingFinished.connect( - lambda: checkAddress(self, wdgts[0][1].text())) - self.setLayout(layout) -""" - -class SchDynamicPage1(QWizardPage): - def __init__(self, parent): - super(SchDynamicPage1, self).__init__(parent) - self.setTitle("Tumble schedule generation") - self.setSubTitle("Set parameters for the sequence of transactions in the tumble.") - results = [] - sN = ['Starting mixdepth', 'Average number of counterparties', - 'How many mixdepths to tumble through', - 'Average wait time between transactions, in seconds', - 'Average number of transactions per mixdepth'] - #Tooltips - sH = ["The starting mixdepth can be decided from the Wallet tab; it must " - "have coins in it, but it's OK if some coins are in other mixdepths.", - "How many other participants are in each coinjoin, on average; but " - "each individual coinjoin will have a number that's slightly varied " - "from this, randomly", - "For example, if you start at mixdepth 1 and enter 4 here, the tumble " - "will move coins from mixdepth 1 to mixdepth 5", - "This is the time waited *after* 1 confirmation has occurred, and is " - "varied randomly.", - "Will be varied randomly, with a minimum of 1 per mixdepth"] - #types - sT = [int, int, int, float, int] - #constraints - sMM = [(0, jm_single().config.getint("GUI", "max_mix_depth") - 1), (3, 20), - (1, 5), (0.00000001, 100.0, 8), (2, 10)] - sD = ['', '', '', '', ''] - for x in zip(sN, sH, sT, sD, sMM): - ql = QLabel(x[0]) - ql.setToolTip(x[1]) - qle = QLineEdit(x[3]) - if x[2] == int: - qle.setValidator(QIntValidator(*x[4])) - if x[2] == float: - qle.setValidator(QDoubleValidator(*x[4])) - results.append((ql, qle)) - layout = QGridLayout() - layout.setSpacing(4) - for i, x in enumerate(results): - layout.addWidget(x[0], i + 1, 0) - layout.addWidget(x[1], i + 1, 1, 1, 2) - self.setLayout(layout) - self.registerField("mixdepthsrc*", results[0][1]) - self.registerField("makercount*", results[1][1]) - self.registerField("mixdepthcount*", results[2][1]) - self.registerField("timelambda*", results[3][1]) - self.registerField("txcountparams*", results[4][1]) - -class SchDynamicPage2(QWizardPage): - - def __init__(self, parent): - super(SchDynamicPage2, self).__init__(parent) - self.setTitle("Tumble schedule generation 2") - self.setSubTitle("Set destination addresses for tumble.") - layout = QGridLayout() - layout.setSpacing(4) - #by default create three address fields - addrLEs = [] - #for testing - testaddrs = ["mteaYsGsLCL9a4cftZFTpGEWXNwZyDt5KS", - "msFGHeut3rfJk5sKuoZNfpUq9MeVMqmido", - "mkZfBXCRPs8fCmwWLrspjCvYozDhK6Eepz"] - for i in range(3): - layout.addWidget(QLabel("Destination address: " + str(i)), i, 0) - addrLEs.append(QLineEdit(testaddrs[i])) - layout.addWidget(addrLEs[-1], i, 1, 1, 2) - #addrLEs[-1].editingFinished.connect( - # lambda: checkAddress(self, addrLEs[-1].text())) - self.registerField("destaddr"+str(i), addrLEs[-1]) - self.setLayout(layout) - -class SchFinishPage(QWizardPage): - def __init__(self, parent): - super(SchFinishPage, self).__init__(parent) - self.setTitle("Save your schedule") - self.setSubTitle("The schedule will be saved to this file when you click Finish") - layout = QGridLayout() - layout.setSpacing(4) - layout.addWidget(QLabel("Enter schedule name: "), 0, 0) - self.schedName = QLineEdit() - layout.addWidget(self.schedName, 0, 1, 1, 2) - self.registerField("schedfilename*", self.schedName) - self.setLayout(layout) - -class SchIntroPage(QWizardPage): - def __init__(self, parent): - super(SchIntroPage, self).__init__(parent) - self.setTitle("Generate a join transaction schedule") - self.rbgroup = QButtonGroup(self) - self.r0 = QRadioButton("Define schedule manually (not yet implemented)") - self.r0.setEnabled(False) - self.r1 = QRadioButton("Generate a tumble schedule automatically") - self.rbgroup.addButton(self.r0) - self.rbgroup.addButton(self.r1) - layout = QVBoxLayout() - layout.addWidget(self.r0) - layout.addWidget(self.r1) - self.setLayout(layout) - -""" - def nextId(self): - if self.rbgroup.checkedButton() == self.r0: - self.parent().staticSchedule = True - return 3 - elif self.rbgroup.checkedButton() == self.r1: - self.parent().staticSchedule = False - return 1 - else: - return 0 -""" - -class ScheduleWizard(QWizard): - def __init__(self): - super(ScheduleWizard, self).__init__() - self.setWindowTitle("Joinmarket schedule generator") - self.setPage(0, SchIntroPage(self)) - self.setPage(1, SchDynamicPage1(self)) - self.setPage(2, SchDynamicPage2(self)) - #self.setPage(3, SchStaticPage(self)) - self.setPage(3, SchFinishPage(self)) - - def get_schedule(self): - destaddrs = [str(x) for x in [self.field("destaddr0").toString(), - self.field("destaddr1").toString(), - self.field("destaddr2").toString()]] - opts = {} - opts['mixdepthsrc'] = int(self.field("mixdepthsrc").toString()) - opts['mixdepthcount'] = int(self.field("mixdepthcount").toString()) - opts['txfee'] = -1 - opts['addrcount'] = 3 - opts['makercountrange'] = (int(self.field("makercount").toString()), 1) - opts['minmakercount'] = 2 - opts['txcountparams'] = (int(self.field("txcountparams").toString()), 1) - opts['mintxcount'] = 1 - opts['amountpower'] = 100.0 - opts['timelambda'] = float(self.field("timelambda").toString()) - opts['waittime'] = 20 - opts['mincjamount'] = 1000000 - #needed for Taker to check: - jm_single().mincjamount = opts['mincjamount'] - return get_tumble_schedule(opts, destaddrs) - class SpendTab(QWidget): def __init__(self): @@ -881,9 +261,9 @@ class SpendTab(QWidget): wizard = ScheduleWizard() wizard.exec_() self.loaded_schedule = wizard.get_schedule() - print(str(self.loaded_schedule)) - self.toggleButtons(False, False, True, False) - + self.sch_label2.setText(wizard.get_name()) + self.sched_view.setText(schedule_to_text(self.loaded_schedule)) + self.sch_startButton.setEnabled(True) def selectSchedule(self): current_path = os.path.dirname(os.path.realpath(__file__)) @@ -906,8 +286,7 @@ class SpendTab(QWidget): w.statusBar().showMessage("Schedule loaded OK.") self.sch_label2.setText(os.path.basename(str(firstarg))) self.sched_view.setText(rawsched) - self.schedule_set_button.setEnabled(True) - self.toggleButtons(False, False, True, False) + self.sch_startButton.setEnabled(True) self.loaded_schedule = schedule def getDonateLayout(self): @@ -1082,12 +461,9 @@ class SpendTab(QWidget): "Connecting to IRC.\nView real-time log in the lower pane.", title="Sendpayment") - if multiple: - self.toggleButtons(False, False, False, True) - else: - self.toggleButtons(False, True, False, False) + self.toggleButtons(False, sched=multiple) - log.debug('starting coinjoin(s)..') + log.debug('starting coinjoin ..') w.statusBar().showMessage("Syncing wallet ...") jm_single().bc_interface.sync_wallet(w.wallet, fast=True) @@ -1252,11 +628,13 @@ class SpendTab(QWidget): self.giveUp() def startNextTransaction(self): - log.debug("SNT being called") jm_single().bc_interface.sync_wallet(w.wallet) self.clientfactory.getClient().clientStart() def takerFinished(self): + """Callback (after pass-through signal) for jmclient.Taker + on completion of each join transaction. + """ if self.taker_finished_fromtx: #not the final finished transaction if self.taker_finished_res: @@ -1266,7 +644,8 @@ class SpendTab(QWidget): self.taker.txid) log.debug("Waiting for: " + str( self.taker_finished_waittime/1000.0) + " secs.") - QtCore.QTimer.singleShot(self.taker_finished_waittime, self.startNextTransaction) + QtCore.QTimer.singleShot(self.taker_finished_waittime, + self.startNextTransaction) else: #a transaction failed; just stop self.giveUp() @@ -1299,18 +678,31 @@ class SpendTab(QWidget): txhist = w.centralWidget().widget(3) txhist.updateTxInfo() - def toggleButtons(self, send, abort, schsend, schabort): - self.startButton.setEnabled(send) - self.abortButton.setEnabled(abort) - self.sch_startButton.setEnabled(schsend) - self.sch_abortButton.setEnabled(schabort) + 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. + """ + 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 + btns = (self.startButton, self.abortButton, + self.schedule_set_button, self.schedule_generate_button, + self.sch_startButton, self.sch_abortButton) + for b, s in zip(btns, btnsettings): + b.setEnabled(s) def giveUp(self): self.aborted = True log.debug("Transaction aborted.") self.qtw.setTabEnabled(0, True) self.qtw.setTabEnabled(1, True) - self.toggleButtons(True, False, False, False) + self.toggleButtons(True) w.statusBar().showMessage("Transaction aborted.") def cleanUp(self): @@ -1343,7 +735,7 @@ class SpendTab(QWidget): return self.qtw.setTabEnabled(0, True) self.qtw.setTabEnabled(1, True) - self.toggleButtons(True, False, False, False) + self.toggleButtons(True) def validateSettings(self): valid, errmsg = validate_address(self.widgets[0][1].text()) diff --git a/scripts/qtsupport.py b/scripts/qtsupport.py new file mode 100644 index 0000000..69f71a0 --- /dev/null +++ b/scripts/qtsupport.py @@ -0,0 +1,659 @@ +#!/usr/bin/env python +from __future__ import print_function + +''' +Qt files for the wizard for initiating a tumbler run. + + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import sys, math, re, logging, Queue +from collections import namedtuple +from decimal import Decimal + +from PyQt4 import QtCore +from PyQt4.QtGui import * + +from jmclient import (load_program_config, get_network, Wallet, + get_p2pk_vbyte, jm_single, validate_address, + get_log, weighted_order_choose, Taker, + JMTakerClientProtocolFactory, WalletError, + start_reactor, get_schedule, get_tumble_schedule) + + +GREEN_BG = "QWidget {background-color:#80ff80;}" +RED_BG = "QWidget {background-color:#ffcccc;}" +RED_FG = "QWidget {color:red;}" +BLUE_FG = "QWidget {color:blue;}" +BLACK_FG = "QWidget {color:black;}" + +donation_address = '1LT6rwv26bV7mgvRosoSCyGM7ttVRsYidP' +donation_address_testnet = 'mz6FQosuiNe8135XaQqWYmXsa3aD8YsqGL' + +warnings = {"blockr_privacy": """You are using blockr as your method of +connecting to the blockchain; this means +that blockr.com can see the addresses you +query. This is bad for privacy - consider +using a Bitcoin Core node instead."""} +#configuration types +config_types = {'rpc_port': int, + 'port': int, + 'usessl': bool, + 'socks5': bool, + 'network': bool, + 'checktx': bool, + 'socks5_port': int, + 'maker_timeout_sec': int, + 'tx_fees': int, + 'gaplimit': int, + 'check_high_fee': int, + 'max_mix_depth': int, + 'txfee_default': int, + 'order_wait_time': int, + 'privacy_warning': None} +config_tips = { + 'blockchain_source': 'options: blockr, bitcoin-rpc', + 'network': 'one of "testnet" or "mainnet"', + 'checktx': 'whether to check fees before completing transaction', + 'rpc_host': + 'the host for bitcoind; only used if blockchain_source is bitcoin-rpc', + 'rpc_port': 'port for connecting to bitcoind over rpc', + 'rpc_user': 'user for connecting to bitcoind over rpc', + 'rpc_password': 'password for connecting to bitcoind over rpc', + 'host': 'hostname for IRC server', + 'channel': 'channel name on IRC server', + 'port': 'port for connecting to IRC server', + 'usessl': 'check to use SSL for connection to IRC', + 'socks5': 'check to use SOCKS5 proxy for IRC connection', + 'socks5_host': 'host for SOCKS5 proxy', + 'socks5_port': 'port for SOCKS5 proxy', + 'maker_timeout_sec': 'timeout for waiting for replies from makers', + 'merge_algorithm': 'for dust sweeping, try merge_algorithm = gradual, \n' + + 'for more rapid dust sweeping, try merge_algorithm = greedy \n' + + 'for most rapid dust sweeping, try merge_algorithm = greediest \n' + + ' but dont forget to bump your miner fees!', + 'tx_fees': + 'the fee estimate is based on a projection of how many satoshis \n' + + 'per kB are needed to get in one of the next N blocks, N set here \n' + + 'as the value of "tx_fees". This estimate is high if you set N=1, \n' + + 'so we choose N=3 for a more reasonable figure, \n' + + 'as our default. Note that for clients not using a local blockchain \n' + + 'instance, we retrieve an estimate from the API at blockcypher.com, currently. \n', + 'gaplimit': 'How far forward to search for used addresses in the HD wallet', + 'check_high_fee': 'Percent fee considered dangerously high, default 2%', + 'max_mix_depth': 'Total number of mixdepths in the wallet, default 5', + 'txfee_default': 'Number of satoshis per counterparty for an initial\n' + + 'tx fee estimate; this value is not usually used and is best left at\n' + + 'the default of 5000', + 'order_wait_time': 'How long to wait for orders to arrive on entering\n' + + 'the message channel, default is 30s' +} + + + +def JMQtMessageBox(obj, msg, mbtype='info', title=''): + mbtypes = {'info': QMessageBox.information, + 'crit': QMessageBox.critical, + 'warn': QMessageBox.warning, + 'question': QMessageBox.question} + title = "JoinmarketQt - " + title + if mbtype == 'question': + return QMessageBox.question(obj, title, msg, QMessageBox.Yes, + QMessageBox.No) + else: + mbtypes[mbtype](obj, title, msg) + + +class TaskThread(QtCore.QThread): + '''Thread that runs background tasks. Callbacks are guaranteed + to happen in the context of its parent.''' + + Task = namedtuple("Task", "task cb_success cb_done cb_error") + doneSig = QtCore.pyqtSignal(object, object, object) + + def __init__(self, parent, on_error=None): + super(TaskThread, self).__init__(parent) + self.on_error = on_error + self.tasks = Queue.Queue() + self.doneSig.connect(self.on_done) + self.start() + + def add(self, task, on_success=None, on_done=None, on_error=None): + on_error = on_error or self.on_error + self.tasks.put(TaskThread.Task(task, on_success, on_done, on_error)) + + def run(self): + while True: + task = self.tasks.get() + if not task: + break + try: + result = task.task() + self.doneSig.emit(result, task.cb_done, task.cb_success) + except BaseException: + self.doneSig.emit(sys.exc_info(), task.cb_done, task.cb_error) + + def on_done(self, result, cb_done, cb): + # This runs in the parent's thread. + if cb_done: + cb_done() + if cb: + cb(result) + + def stop(self): + self.tasks.put(None) + +class QtHandler(logging.Handler): + + def __init__(self): + logging.Handler.__init__(self) + + def emit(self, record): + record = self.format(record) + if record: XStream.stdout().write('%s\n' % record) + + +class XStream(QtCore.QObject): + _stdout = None + _stderr = None + messageWritten = QtCore.pyqtSignal(str) + + def flush(self): + pass + + def fileno(self): + return -1 + + def write(self, msg): + if (not self.signalsBlocked()): + self.messageWritten.emit(unicode(msg)) + + @staticmethod + def stdout(): + if (not XStream._stdout): + XStream._stdout = XStream() + sys.stdout = XStream._stdout + return XStream._stdout + + @staticmethod + def stderr(): + if (not XStream._stderr): + XStream._stderr = XStream() + sys.stderr = XStream._stderr + return XStream._stderr + + +class Buttons(QHBoxLayout): + + def __init__(self, *buttons): + QHBoxLayout.__init__(self) + self.addStretch(1) + for b in buttons: + self.addWidget(b) + + +class CloseButton(QPushButton): + + def __init__(self, dialog): + QPushButton.__init__(self, "Close") + self.clicked.connect(dialog.close) + self.setDefault(True) + + +class CopyButton(QPushButton): + + def __init__(self, text_getter, app): + QPushButton.__init__(self, "Copy") + self.clicked.connect(lambda: app.clipboard().setText(text_getter())) + + +class CopyCloseButton(QPushButton): + + def __init__(self, text_getter, app, dialog): + QPushButton.__init__(self, "Copy and Close") + self.clicked.connect(lambda: app.clipboard().setText(text_getter())) + self.clicked.connect(dialog.close) + self.setDefault(True) + + +class OkButton(QPushButton): + + def __init__(self, dialog, label=None): + QPushButton.__init__(self, label or "OK") + self.clicked.connect(dialog.accept) + self.setDefault(True) + + +class CancelButton(QPushButton): + + def __init__(self, dialog, label=None): + QPushButton.__init__(self, label or "Cancel") + self.clicked.connect(dialog.reject) + + +class HelpLabel(QLabel): + + def __init__(self, text, help_text, wtitle): + QLabel.__init__(self, text) + self.help_text = help_text + self.wtitle = wtitle + self.font = QFont() + self.setStyleSheet(BLUE_FG) + + def mouseReleaseEvent(self, x): + QMessageBox.information(w, self.wtitle, self.help_text, 'OK') + + def enterEvent(self, event): + self.font.setUnderline(True) + self.setFont(self.font) + app.setOverrideCursor(QCursor(QtCore.Qt.PointingHandCursor)) + return QLabel.enterEvent(self, event) + + def leaveEvent(self, event): + self.font.setUnderline(False) + self.setFont(self.font) + app.setOverrideCursor(QCursor(QtCore.Qt.ArrowCursor)) + return QLabel.leaveEvent(self, event) + + +def check_password_strength(password): + ''' + Check the strength of the password entered by the user and return back the same + :param password: password entered by user in New Password + :return: password strength Weak or Medium or Strong + ''' + password = unicode(password) + n = math.log(len(set(password))) + num = re.search("[0-9]", password) is not None and re.match( + "^[0-9]*$", password) is None + caps = password != password.upper() and password != password.lower() + extra = re.match("^[a-zA-Z0-9]*$", password) is None + score = len(password) * (n + caps + num + extra) / 20 + password_strength = {0: "Weak", 1: "Medium", 2: "Strong", 3: "Very Strong"} + return password_strength[min(3, int(score))] + + +def update_password_strength(pw_strength_label, password): + ''' + call the function check_password_strength and update the label pw_strength + interactively as the user is typing the password + :param pw_strength_label: the label pw_strength + :param password: password entered in New Password text box + :return: None + ''' + if password: + colors = {"Weak": "Red", + "Medium": "Blue", + "Strong": "Green", + "Very Strong": "Green"} + strength = check_password_strength(password) + label = "Password Strength"+ ": "+"" + strength + "" + else: + label = "" + pw_strength_label.setText(label) + + +def make_password_dialog(self, msg, new_pass=True): + + self.new_pw = QLineEdit() + self.new_pw.setEchoMode(2) + self.conf_pw = QLineEdit() + self.conf_pw.setEchoMode(2) + + vbox = QVBoxLayout() + label = QLabel(msg) + label.setWordWrap(True) + + grid = QGridLayout() + grid.setSpacing(8) + grid.setColumnMinimumWidth(0, 70) + grid.setColumnStretch(1, 1) + #TODO perhaps add an icon here + logo = QLabel() + lockfile = ":icons/lock.png" + logo.setPixmap(QPixmap(lockfile).scaledToWidth(36)) + logo.setAlignment(QtCore.Qt.AlignCenter) + + grid.addWidget(logo, 0, 0) + grid.addWidget(label, 0, 1, 1, 2) + vbox.addLayout(grid) + + grid = QGridLayout() + grid.setSpacing(8) + grid.setColumnMinimumWidth(0, 250) + grid.setColumnStretch(1, 1) + + grid.addWidget(QLabel('New Password' if new_pass else 'Password'), 1, 0) + grid.addWidget(self.new_pw, 1, 1) + + grid.addWidget(QLabel('Confirm Password'), 2, 0) + grid.addWidget(self.conf_pw, 2, 1) + vbox.addLayout(grid) + + #Password Strength Label + self.pw_strength = QLabel() + grid.addWidget(self.pw_strength, 3, 0, 1, 2) + self.new_pw.textChanged.connect( + lambda: update_password_strength(self.pw_strength, self.new_pw.text())) + + vbox.addStretch(1) + vbox.addLayout(Buttons(CancelButton(self), OkButton(self))) + return vbox + + +class PasswordDialog(QDialog): + + def __init__(self): + super(PasswordDialog, self).__init__() + self.initUI() + + def initUI(self): + self.setWindowTitle('Create a new password') + msg = "Enter a new password" + self.setLayout(make_password_dialog(self, msg)) + self.show() + + +class MyTreeWidget(QTreeWidget): + + def __init__(self, + parent, + create_menu, + headers, + stretch_column=None, + editable_columns=None): + QTreeWidget.__init__(self, parent) + self.parent = parent + self.stretch_column = stretch_column + self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.customContextMenuRequested.connect(create_menu) + self.setUniformRowHeights(True) + # extend the syntax for consistency + self.addChild = self.addTopLevelItem + self.insertChild = self.insertTopLevelItem + self.editor = None + self.pending_update = False + if editable_columns is None: + editable_columns = [stretch_column] + self.editable_columns = editable_columns + self.itemActivated.connect(self.on_activated) + self.update_headers(headers) + + def update_headers(self, headers): + self.setColumnCount(len(headers)) + self.setHeaderLabels(headers) + self.header().setStretchLastSection(False) + for col in range(len(headers)): + sm = QHeaderView.Stretch if col == self.stretch_column else QHeaderView.ResizeToContents + self.header().setResizeMode(col, sm) + + def editItem(self, item, column): + if column in self.editable_columns: + self.editing_itemcol = (item, column, unicode(item.text(column))) + # Calling setFlags causes on_changed events for some reason + item.setFlags(item.flags() | Qt.ItemIsEditable) + QTreeWidget.editItem(self, item, column) + item.setFlags(item.flags() & ~Qt.ItemIsEditable) + + def keyPressEvent(self, event): + if event.key() == QtCore.Qt.Key_F2: + self.on_activated(self.currentItem(), self.currentColumn()) + else: + QTreeWidget.keyPressEvent(self, event) + + def permit_edit(self, item, column): + return (column in self.editable_columns and + self.on_permit_edit(item, column)) + + def on_permit_edit(self, item, column): + return True + + def on_activated(self, item, column): + if self.permit_edit(item, column): + self.editItem(item, column) + else: + pt = self.visualItemRect(item).bottomLeft() + pt.setX(50) + self.emit( + QtCore.SIGNAL('customContextMenuRequested(const QPoint&)'), pt) + + def createEditor(self, parent, option, index): + self.editor = QStyledItemDelegate.createEditor(self.itemDelegate(), + parent, option, index) + self.editor.connect(self.editor, QtCore.SIGNAL("editingFinished()"), + self.editing_finished) + return self.editor + + def editing_finished(self): + # Long-time QT bug - pressing Enter to finish editing signals + # editingFinished twice. If the item changed the sequence is + # Enter key: editingFinished, on_change, editingFinished + # Mouse: on_change, editingFinished + # This mess is the cleanest way to ensure we make the + # on_edited callback with the updated item + if self.editor: + (item, column, prior_text) = self.editing_itemcol + if self.editor.text() == prior_text: + self.editor = None # Unchanged - ignore any 2nd call + elif item.text(column) == prior_text: + pass # Buggy first call on Enter key, item not yet updated + else: + # What we want - the updated item + self.on_edited(*self.editing_itemcol) + self.editor = None + + # Now do any pending updates + if self.editor is None and self.pending_update: + self.pending_update = False + self.on_update() + + def on_edited(self, item, column, prior): + '''Called only when the text actually changes''' + key = str(item.data(0, Qt.UserRole).toString()) + text = unicode(item.text(column)) + self.parent.wallet.set_label(key, text) + if text: + item.setForeground(column, QBrush(QColor('black'))) + else: + text = self.parent.wallet.get_default_label(key) + item.setText(column, text) + item.setForeground(column, QBrush(QColor('gray'))) + self.parent.history_list.update() + self.parent.update_completions() + + def update(self): + # Defer updates if editing + if self.editor: + self.pending_update = True + else: + self.on_update() + + def on_update(self): + pass + + def get_leaves(self, root): + child_count = root.childCount() + if child_count == 0: + yield root + for i in range(child_count): + item = root.child(i) + for x in self.get_leaves(item): + yield x + + def filter(self, p, columns): + p = unicode(p).lower() + for item in self.get_leaves(self.invisibleRootItem()): + item.setHidden(all([unicode(item.text(column)).lower().find(p) == -1 + for column in columns])) + +""" TODO implement this option +class SchStaticPage(QWizardPage): + def __init__(self, parent): + super(SchStaticPage, self).__init__(parent) + self.setTitle("Manually create a schedule entry") + layout = QGridLayout() + wdgts = getSettingsWidgets() + for i, x in enumerate(wdgts): + layout.addWidget(x[0], i + 1, 0) + layout.addWidget(x[1], i + 1, 1, 1, 2) + wdgts[0][1].editingFinished.connect( + lambda: checkAddress(self, wdgts[0][1].text())) + self.setLayout(layout) +""" + +class SchDynamicPage1(QWizardPage): + def __init__(self, parent): + super(SchDynamicPage1, self).__init__(parent) + self.setTitle("Tumble schedule generation") + self.setSubTitle("Set parameters for the sequence of transactions in the tumble.") + results = [] + sN = ['Starting mixdepth', 'Average number of counterparties', + 'How many mixdepths to tumble through', + 'Average wait time between transactions, in seconds', + 'Average number of transactions per mixdepth'] + #Tooltips + sH = ["The starting mixdepth can be decided from the Wallet tab; it must " + "have coins in it, but it's OK if some coins are in other mixdepths.", + "How many other participants are in each coinjoin, on average; but " + "each individual coinjoin will have a number that's slightly varied " + "from this, randomly", + "For example, if you start at mixdepth 1 and enter 4 here, the tumble " + "will move coins from mixdepth 1 to mixdepth 5", + "This is the time waited *after* 1 confirmation has occurred, and is " + "varied randomly.", + "Will be varied randomly, with a minimum of 1 per mixdepth"] + #types + sT = [int, int, int, float, int] + #constraints + sMM = [(0, jm_single().config.getint("GUI", "max_mix_depth") - 1), (3, 20), + (1, 5), (0.00000001, 100.0, 8), (2, 10)] + sD = ['', '', '', '', ''] + for x in zip(sN, sH, sT, sD, sMM): + ql = QLabel(x[0]) + ql.setToolTip(x[1]) + qle = QLineEdit(x[3]) + if x[2] == int: + qle.setValidator(QIntValidator(*x[4])) + if x[2] == float: + qle.setValidator(QDoubleValidator(*x[4])) + results.append((ql, qle)) + layout = QGridLayout() + layout.setSpacing(4) + for i, x in enumerate(results): + layout.addWidget(x[0], i + 1, 0) + layout.addWidget(x[1], i + 1, 1, 1, 2) + self.setLayout(layout) + self.registerField("mixdepthsrc*", results[0][1]) + self.registerField("makercount*", results[1][1]) + self.registerField("mixdepthcount*", results[2][1]) + self.registerField("timelambda*", results[3][1]) + self.registerField("txcountparams*", results[4][1]) + +class SchDynamicPage2(QWizardPage): + + def __init__(self, parent): + super(SchDynamicPage2, self).__init__(parent) + self.setTitle("Tumble schedule generation 2") + self.setSubTitle("Set destination addresses for tumble.") + layout = QGridLayout() + layout.setSpacing(4) + #by default create three address fields + addrLEs = [] + #for testing + testaddrs = ["mteaYsGsLCL9a4cftZFTpGEWXNwZyDt5KS", + "msFGHeut3rfJk5sKuoZNfpUq9MeVMqmido", + "mkZfBXCRPs8fCmwWLrspjCvYozDhK6Eepz"] + for i in range(3): + layout.addWidget(QLabel("Destination address: " + str(i)), i, 0) + addrLEs.append(QLineEdit(testaddrs[i])) + layout.addWidget(addrLEs[-1], i, 1, 1, 2) + #addrLEs[-1].editingFinished.connect( + # lambda: checkAddress(self, addrLEs[-1].text())) + self.registerField("destaddr"+str(i), addrLEs[-1]) + self.setLayout(layout) + +class SchFinishPage(QWizardPage): + def __init__(self, parent): + super(SchFinishPage, self).__init__(parent) + self.setTitle("Save your schedule") + self.setSubTitle("The schedule will be saved to this file when you click Finish") + layout = QGridLayout() + layout.setSpacing(4) + layout.addWidget(QLabel("Enter schedule name: "), 0, 0) + self.schedName = QLineEdit() + layout.addWidget(self.schedName, 0, 1, 1, 2) + self.registerField("schedfilename*", self.schedName) + self.setLayout(layout) + +class SchIntroPage(QWizardPage): + def __init__(self, parent): + super(SchIntroPage, self).__init__(parent) + self.setTitle("Generate a join transaction schedule") + self.rbgroup = QButtonGroup(self) + self.r0 = QRadioButton("Define schedule manually (not yet implemented)") + self.r0.setEnabled(False) + self.r1 = QRadioButton("Generate a tumble schedule automatically") + self.rbgroup.addButton(self.r0) + self.rbgroup.addButton(self.r1) + layout = QVBoxLayout() + layout.addWidget(self.r0) + layout.addWidget(self.r1) + self.setLayout(layout) + +""" + def nextId(self): + if self.rbgroup.checkedButton() == self.r0: + self.parent().staticSchedule = True + return 3 + elif self.rbgroup.checkedButton() == self.r1: + self.parent().staticSchedule = False + return 1 + else: + return 0 +""" + +class ScheduleWizard(QWizard): + def __init__(self): + super(ScheduleWizard, self).__init__() + self.setWindowTitle("Joinmarket schedule generator") + self.setPage(0, SchIntroPage(self)) + self.setPage(1, SchDynamicPage1(self)) + self.setPage(2, SchDynamicPage2(self)) + #self.setPage(3, SchStaticPage(self)) + self.setPage(3, SchFinishPage(self)) + + def get_name(self): + return self.field("schedfilename").toString() + + def get_schedule(self): + destaddrs = [str(x) for x in [self.field("destaddr0").toString(), + self.field("destaddr1").toString(), + self.field("destaddr2").toString()]] + opts = {} + opts['mixdepthsrc'] = int(self.field("mixdepthsrc").toString()) + opts['mixdepthcount'] = int(self.field("mixdepthcount").toString()) + opts['txfee'] = -1 + opts['addrcount'] = 3 + opts['makercountrange'] = (int(self.field("makercount").toString()), 1) + opts['minmakercount'] = 2 + opts['txcountparams'] = (int(self.field("txcountparams").toString()), 1) + opts['mintxcount'] = 1 + opts['amountpower'] = 100.0 + opts['timelambda'] = float(self.field("timelambda").toString()) + opts['waittime'] = 20 + opts['mincjamount'] = 1000000 + #needed for Taker to check: + jm_single().mincjamount = opts['mincjamount'] + return get_tumble_schedule(opts, destaddrs)