#!/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)
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
if jm_single().config.get("BLOCKCHAIN", "blockchain_source") == "regtest":
testaddrs = ["mteaYsGsLCL9a4cftZFTpGEWXNwZyDt5KS",
"msFGHeut3rfJk5sKuoZNfpUq9MeVMqmido",
"mkZfBXCRPs8fCmwWLrspjCvYozDhK6Eepz"]
else:
testaddrs = ["","",""]
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)