diff --git a/jmclient/setup.py b/jmclient/setup.py index 83e7e71..dd2f805 100644 --- a/jmclient/setup.py +++ b/jmclient/setup.py @@ -9,5 +9,7 @@ setup(name='joinmarketclient', author_email='', license='GPL', packages=['jmclient'], - install_requires=['future', 'configparser;python_version<"3.2"', 'joinmarketbase==0.4.2', 'mnemonic', 'qt4reactor', 'argon2_cffi', 'bencoder.pyx', 'pyaes'], + install_requires=['future', 'configparser;python_version<"3.2"', + 'joinmarketbase==0.4.2', 'mnemonic', 'argon2_cffi', + 'bencoder.pyx', 'pyaes'], zip_safe=False) diff --git a/scripts/joinmarket-qt.py b/scripts/joinmarket-qt.py index ac8cc8d..dbd5d72 100644 --- a/scripts/joinmarket-qt.py +++ b/scripts/joinmarket-qt.py @@ -24,13 +24,15 @@ Some widgets copied and modified from https://github.com/spesmilo/electrum ''' import sys, datetime, os, logging -import platform, csv, threading, time +import platform, json, threading, time from decimal import Decimal +from PySide2 import QtCore -from PyQt4 import QtCore -from PyQt4.QtGui import * +from PySide2.QtGui import * + +from PySide2.QtWidgets import * if platform.system() == 'Windows': MONOSPACE_FONT = 'Lucida Console' @@ -41,11 +43,19 @@ else: import jmbitcoin as btc +# This is required to change the decimal separator +# to '.' regardless of the locale; TODO don't require +# this, but will require other edits for parsing amounts. +curL = QtCore.QLocale("en_US") +QtCore.QLocale.setDefault(curL) + app = QApplication(sys.argv) if 'twisted.internet.reactor' in sys.modules: del sys.modules['twisted.internet.reactor'] -from qtreactor import pyqt4reactor -pyqt4reactor.install() + +import qt5reactor +qt5reactor.install() + #General Joinmarket donation address; TODO donation_address = "1AZgQZWYRteh6UyF87hwuvyWj73NvWKpL" @@ -133,7 +143,8 @@ def getSettingsWidgets(): if x[2] == int: qle.setValidator(QIntValidator(*x[4])) if x[2] == float: - qle.setValidator(QDoubleValidator(*x[4])) + qdv = QDoubleValidator(*x[4]) + qle.setValidator(qdv) results.append((ql, qle)) return results @@ -152,7 +163,7 @@ class HelpLabel(QLabel): self.setStyleSheet(BLUE_FG) def mouseReleaseEvent(self, x): - QMessageBox.information(w, self.wtitle, self.help_text, 'OK') + QMessageBox.information(w, self.wtitle, self.help_text) def enterEvent(self, event): self.font.setUnderline(True) @@ -343,14 +354,14 @@ class SpendTab(QWidget): 'Choose Schedule File', directory=current_path) #TODO validate the schedule - log.debug('Looking for schedule in: ' + firstarg) + log.debug('Looking for schedule in: ' + str(firstarg)) if not firstarg: return #extract raw text before processing - with open(firstarg, 'rb') as f: + with open(firstarg[0], 'rb') as f: rawsched = f.read() - res, schedule = get_schedule(firstarg) + res, schedule = get_schedule(firstarg[0]) if not res: JMQtMessageBox(self, "Not a valid JM schedule file", mbtype='crit', title='Error') @@ -370,7 +381,7 @@ class SpendTab(QWidget): def updateSchedView(self): self.sch_label2.setText(self.spendstate.schedule_name) - self.sched_view.setText(schedule_to_text(self.spendstate.loaded_schedule)) + self.sched_view.setText(schedule_to_text(self.spendstate.loaded_schedule).decode('utf-8')) def getDonateLayout(self): donateLayout = QHBoxLayout() @@ -601,7 +612,7 @@ class SpendTab(QWidget): 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()) + btc_amount_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()) @@ -825,10 +836,10 @@ class SpendTab(QWidget): def persistTxToHistory(self, addr, amt, txid): #persist the transaction to history with open(jm_single().config.get("GUI", "history_file"), 'ab') as f: - f.write(','.join([addr, satoshis_to_amt_str(amt), txid, + f.write((','.join([addr, satoshis_to_amt_str(amt), txid, datetime.datetime.now( - ).strftime("%Y/%m/%d %H:%M:%S")])) - f.write('\n') #TODO: Windows + ).strftime("%Y/%m/%d %H:%M:%S")])).encode('utf-8')) + f.write(b'\n') #TODO: Windows #update the TxHistory tab txhist = w.centralWidget().widget(3) txhist.updateTxInfo() @@ -897,7 +908,7 @@ class SpendTab(QWidget): "Mixdepth must be chosen.", "Amount, in bitcoins, must be provided."] for i in range(1, 4): - if self.widgets[i][1].text().size() == 0: + if len(self.widgets[i][1].text()) == 0: JMQtMessageBox(self, errs[i - 1], mbtype='warn', title="Error") return False if not w.wallet: @@ -917,12 +928,12 @@ class TxHistoryTab(QWidget): def initUI(self): self.tHTW = MyTreeWidget(self, self.create_menu, self.getHeaders()) self.tHTW.setSelectionMode(QAbstractItemView.ExtendedSelection) - self.tHTW.header().setResizeMode(QHeaderView.Interactive) + self.tHTW.header().setSectionResizeMode(QHeaderView.Interactive) self.tHTW.header().setStretchLastSection(False) self.tHTW.on_update = self.updateTxInfo vbox = QVBoxLayout() self.setLayout(vbox) - vbox.setMargin(0) + vbox.setContentsMargins(0,0,0,0) vbox.setSpacing(0) vbox.addWidget(self.tHTW) self.updateTxInfo() @@ -952,7 +963,7 @@ class TxHistoryTab(QWidget): with open(hf, 'rb') as f: txlines = f.readlines() for tl in txlines: - txhist.append(tl.strip().split(',')) + txhist.append(tl.decode('utf-8').strip().split(',')) if not len(txhist[-1]) == 4: JMQtMessageBox(self, "Incorrectedly formatted file " + hf, @@ -1005,15 +1016,13 @@ class JMWalletTab(QWidget): self.history = v vbox = QVBoxLayout() self.setLayout(vbox) - vbox.setMargin(0) + vbox.setContentsMargins(0,0,0,0) vbox.setSpacing(0) vbox.addWidget(self.label1) vbox.addWidget(v) buttons = QWidget() vbox.addWidget(buttons) self.updateWalletInfo() - #vBoxLayout.addWidget(self.label2) - #vBoxLayout.addWidget(self.table) self.show() def getHeaders(self): @@ -1090,6 +1099,9 @@ class JMWalletTab(QWidget): class JMMainWindow(QMainWindow): + computing_privkeys_signal = QtCore.Signal() + show_privkeys_signal = QtCore.Signal() + def __init__(self, reactor): super(JMMainWindow, self).__init__() self.wallet = None @@ -1127,9 +1139,9 @@ class JMMainWindow(QMainWindow): aboutAction = QAction('About Joinmarket', self) aboutAction.triggered.connect(self.showAboutDialog) exportPrivAction = QAction('&Export keys', self) - exportPrivAction.setStatusTip('Export all private keys to a csv file') - exportPrivAction.triggered.connect(self.exportPrivkeysCsv) - menubar = QMenuBar() + exportPrivAction.setStatusTip('Export all private keys to a file') + exportPrivAction.triggered.connect(self.exportPrivkeysJson) + menubar = self.menuBar() walletMenu = menubar.addMenu('&Wallet') walletMenu.addAction(loadAction) @@ -1140,7 +1152,6 @@ class JMMainWindow(QMainWindow): aboutMenu = menubar.addMenu('&About') aboutMenu.addAction(aboutAction) - self.setMenuBar(menubar) self.show() def showAboutDialog(self): @@ -1171,7 +1182,7 @@ class JMMainWindow(QMainWindow): lyt.addWidget(btnbox) msgbox.exec_() - def exportPrivkeysCsv(self): + def exportPrivkeysJson(self): if not self.wallet: JMQtMessageBox(self, "No wallet loaded.", @@ -1220,8 +1231,8 @@ class JMMainWindow(QMainWindow): private_keys[addr] = btc.wif_compressed_privkey( priv, vbyte=get_p2pk_vbyte()) - d.emit(QtCore.SIGNAL('computing_privkeys')) - d.emit(QtCore.SIGNAL('show_privkeys')) + self.computing_privkeys_signal.emit() + self.show_privkeys_signal.emit() def show_privkeys(): s = "\n".join(map(lambda x: x[0] + "\t" + x[1], private_keys.items( @@ -1229,10 +1240,9 @@ class JMMainWindow(QMainWindow): e.setText(s) b.setEnabled(True) - d.connect( - d, QtCore.SIGNAL('computing_privkeys'), - lambda: e.setText("Please wait... %d/%d" % (len(private_keys), len(addresses)))) - d.connect(d, QtCore.SIGNAL('show_privkeys'), show_privkeys) + self.computing_privkeys_signal.connect(lambda: e.setText( + "Please wait... %d/%d" % (len(private_keys), len(addresses)))) + self.show_privkeys_signal.connect(show_privkeys) threading.Thread(target=privkeys_thread).start() if not d.exec_(): @@ -1241,13 +1251,13 @@ class JMMainWindow(QMainWindow): privkeys_fn_base = 'joinmarket-private-keys' i = 0 privkeys_fn = privkeys_fn_base - while os.path.isfile(privkeys_fn + '.csv'): + # Updated to use json format, simply because L1354 writer + # has some extremely weird behaviour cross Py2/Py3 + while os.path.isfile(privkeys_fn + '.json'): i += 1 privkeys_fn = privkeys_fn_base + str(i) try: - with open(privkeys_fn + '.csv', "w") as f: - transaction = csv.writer(f) - transaction.writerow(["address", "private_key"]) + with open(privkeys_fn + '.json', "wb") as f: for addr, pk in private_keys.items(): #sanity check if not addr == btc.pubkey_to_p2sh_p2wpkh_address( @@ -1258,20 +1268,20 @@ class JMMainWindow(QMainWindow): " critical error in key parsing.", mbtype='crit') return - transaction.writerow(["%34s" % addr, pk]) + f.write(json.dumps(private_keys, indent=4).encode('utf-8')) except (IOError, os.error) as reason: export_error_label = "JoinmarketQt was unable to produce a private key-export." JMQtMessageBox(None, export_error_label + "\n" + str(reason), mbtype='crit', - title="Unable to create csv") + title="Unable to create json file") - except Exception as e: - JMQtMessageBox(self, str(e), mbtype='crit', title="Error") + except Exception as er: + JMQtMessageBox(self, str(er), mbtype='crit', title="Error") return JMQtMessageBox(self, - "Private keys exported to: " + privkeys_fn + '.csv', + "Private keys exported to: " + privkeys_fn + '.json', title="Success") def seedEntry(self): @@ -1287,7 +1297,7 @@ class JMMainWindow(QMainWindow): pp_field = QLineEdit() pp_field.setEnabled(False) use_pp = QCheckBox('Input Mnemonic Extension', self) - use_pp.setCheckState(False) + use_pp.setCheckState(QtCore.Qt.CheckState(False)) use_pp.stateChanged.connect(lambda state: pp_field.setEnabled(state == QtCore.Qt.Checked)) pp_hbox.addWidget(use_pp) @@ -1307,8 +1317,8 @@ class JMMainWindow(QMainWindow): return None, None mn_extension = None if use_pp.checkState() == QtCore.Qt.Checked: - mn_extension = str(pp_field.text()) - return str(message_e.toPlainText()), mn_extension + mn_extension = pp_field.text() + return message_e.toPlainText(), mn_extension def restartForScan(self, msg): JMQtMessageBox(self, msg, mbtype='info', @@ -1341,7 +1351,7 @@ class JMMainWindow(QMainWindow): directory=current_path, options=QFileDialog.DontUseNativeDialog) #TODO validate the file looks vaguely like a wallet file - log.debug('Looking for wallet in: ' + firstarg) + log.debug('Looking for wallet in: ' + str(firstarg)) if not firstarg: return decrypted = False @@ -1353,13 +1363,13 @@ class JMMainWindow(QMainWindow): if not ok: return pwd = str(text).strip() - decrypted = self.loadWalletFromBlockchain(firstarg, pwd, restart_cb) + decrypted = self.loadWalletFromBlockchain(firstarg[0], pwd, restart_cb) else: if not testnet_seed: testnet_seed, ok = QInputDialog.getText(self, 'Load Testnet wallet', 'Enter a testnet seed:', - mode=QLineEdit.Normal) + QLineEdit.Normal) if not ok: return firstarg = str(testnet_seed) @@ -1372,7 +1382,7 @@ class JMMainWindow(QMainWindow): wallet_path = get_wallet_path(str(firstarg), None) try: self.wallet = open_test_wallet_maybe(wallet_path, str(firstarg), - None, ask_for_password=False, password=pwd, + None, ask_for_password=False, password=pwd.encode('utf-8'), gap_limit=jm_single().config.getint("GUI", "gaplimit")) except Exception as e: JMQtMessageBox(self, @@ -1438,7 +1448,7 @@ class JMMainWindow(QMainWindow): seed = self.getTestnetSeed() self.selectWallet(testnet_seed=seed) else: - self.initWallet() + self.initWallet(restart_cb=self.restartForScan) def getTestnetSeed(self): text, ok = QInputDialog.getText( @@ -1463,7 +1473,7 @@ class JMMainWindow(QMainWindow): continue break self.textpassword = str(pd.new_pw.text()) - return self.textpassword + return self.textpassword.encode('utf-8') def getWalletFileName(self): walletname, ok = QInputDialog.getText(self, 'Choose wallet name', diff --git a/scripts/qtsupport.py b/scripts/qtsupport.py index 45caa2a..49fd273 100644 --- a/scripts/qtsupport.py +++ b/scripts/qtsupport.py @@ -20,11 +20,11 @@ Qt files for the wizard for initiating a tumbler run. 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 +import math, re, logging +from PySide2 import QtCore +from PySide2.QtGui import * +from PySide2.QtWidgets import * -from PyQt4 import QtCore -from PyQt4.QtGui import * from jmclient import (jm_single, validate_address, get_tumble_schedule) @@ -193,45 +193,6 @@ def JMQtMessageBox(obj, msg, mbtype='info', title='', detailed_text= None): 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): @@ -245,7 +206,7 @@ class QtHandler(logging.Handler): class XStream(QtCore.QObject): _stdout = None _stderr = None - messageWritten = QtCore.pyqtSignal(str) + messageWritten = QtCore.Signal(str) def flush(self): pass @@ -255,20 +216,22 @@ class XStream(QtCore.QObject): def write(self, msg): if (not self.signalsBlocked()): - self.messageWritten.emit(unicode(msg)) + self.messageWritten.emit(msg) @staticmethod def stdout(): if (not XStream._stdout): XStream._stdout = XStream() - sys.stdout = XStream._stdout + # temporarily removed, seems not needed + #sys.stdout = XStream._stdout return XStream._stdout @staticmethod def stderr(): if (not XStream._stderr): XStream._stderr = XStream() - sys.stderr = XStream._stderr + # temporarily removed, seems not needed + #sys.stderr = XStream._stderr return XStream._stderr @@ -326,7 +289,6 @@ def check_password_strength(password): :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 @@ -361,9 +323,9 @@ def update_password_strength(pw_strength_label, password): def make_password_dialog(self, msg, new_pass=True): self.new_pw = QLineEdit() - self.new_pw.setEchoMode(2) + self.new_pw.setEchoMode(QLineEdit.EchoMode(2)) self.conf_pw = QLineEdit() - self.conf_pw.setEchoMode(2) + self.conf_pw.setEchoMode(QLineEdit.EchoMode(2)) vbox = QVBoxLayout() label = QLabel(msg) @@ -450,7 +412,7 @@ class MyTreeWidget(QTreeWidget): self.header().setStretchLastSection(False) for col in range(len(headers)): #note, a single stretch column is currently not used. - self.header().setResizeMode(col, QHeaderView.Interactive) + self.header().setSectionResizeMode(col, QHeaderView.Interactive) def editItem(self, item, column): if column in self.editable_columns: @@ -514,7 +476,7 @@ class MyTreeWidget(QTreeWidget): def on_edited(self, item, column, prior): '''Called only when the text actually changes''' - key = str(item.data(0, Qt.UserRole).toString()) + key = str(item.data(0, Qt.UserRole)) text = unicode(item.text(column)) self.parent.wallet.set_label(key, text) if text: @@ -625,7 +587,7 @@ class SchDynamicPage2(QWizardPage): def initializePage(self): addrLEs = [] - requested_mixdepths = int(self.field("mixdepthcount").toString()) + requested_mixdepths = int(self.field("mixdepthcount")) #for testing if jm_single().config.get("BLOCKCHAIN", "blockchain_source") == "regtest": testaddrs = ["mteaYsGsLCL9a4cftZFTpGEWXNwZyDt5KS", @@ -753,7 +715,7 @@ class ScheduleWizard(QWizard): def get_name(self): #TODO de-hardcode generated name return "TUMBLE.schedule" - #return self.field("schedfilename").toString() + def get_destaddrs(self): return self.destaddrs @@ -761,7 +723,7 @@ class ScheduleWizard(QWizard): def get_schedule(self): self.destaddrs = [] for i in range(self.page(2).required_addresses): - daddrstring = str(self.field("destaddr"+str(i)).toString()) + daddrstring = str(self.field("destaddr"+str(i))) if validate_address(daddrstring)[0]: self.destaddrs.append(daddrstring) elif daddrstring != "": @@ -769,22 +731,22 @@ class ScheduleWizard(QWizard): title='Error') return None self.opts = {} - self.opts['mixdepthsrc'] = int(self.field("mixdepthsrc").toString()) - self.opts['mixdepthcount'] = int(self.field("mixdepthcount").toString()) + self.opts['mixdepthsrc'] = int(self.field("mixdepthsrc")) + self.opts['mixdepthcount'] = int(self.field("mixdepthcount")) self.opts['txfee'] = -1 self.opts['addrcount'] = len(self.destaddrs) - self.opts['makercountrange'] = (int(self.field("makercount").toString()), - float(self.field("makercountsdev").toString())) - self.opts['minmakercount'] = int(self.field("minmakercount").toString()) - self.opts['txcountparams'] = (int(self.field("txcountparams").toString()), - float(self.field("txcountsdev").toString())) - self.opts['mintxcount'] = int(self.field("mintxcount").toString()) - self.opts['amountpower'] = float(self.field("amountpower").toString()) - self.opts['timelambda'] = float(self.field("timelambda").toString()) - self.opts['waittime'] = float(self.field("waittime").toString()) - self.opts['mincjamount'] = int(self.field("mincjamount").toString()) - relfeeval = float(self.field("maxrelfee").toString()) - absfeeval = int(self.field("maxabsfee").toString()) + self.opts['makercountrange'] = (int(self.field("makercount")), + float(self.field("makercountsdev"))) + self.opts['minmakercount'] = int(self.field("minmakercount")) + self.opts['txcountparams'] = (int(self.field("txcountparams")), + float(self.field("txcountsdev"))) + self.opts['mintxcount'] = int(self.field("mintxcount")) + self.opts['amountpower'] = float(self.field("amountpower")) + self.opts['timelambda'] = float(self.field("timelambda")) + self.opts['waittime'] = float(self.field("waittime")) + self.opts['mincjamount'] = int(self.field("mincjamount")) + relfeeval = float(self.field("maxrelfee")) + absfeeval = int(self.field("maxabsfee")) self.opts['maxcjfee'] = (relfeeval, absfeeval) #needed for Taker to check: jm_single().mincjamount = self.opts['mincjamount'] @@ -798,10 +760,10 @@ class TumbleRestartWizard(QWizard): def getOptions(self): self.opts = {} - self.opts['amountpower'] = float(self.field("amountpower").toString()) - self.opts['mincjamount'] = int(self.field("mincjamount").toString()) - relfeeval = float(self.field("maxrelfee").toString()) - absfeeval = int(self.field("maxabsfee").toString()) + self.opts['amountpower'] = float(self.field("amountpower")) + self.opts['mincjamount'] = int(self.field("mincjamount")) + relfeeval = float(self.field("maxrelfee")) + absfeeval = int(self.field("maxabsfee")) self.opts['maxcjfee'] = (relfeeval, absfeeval) #needed for Taker to check: jm_single().mincjamount = self.opts['mincjamount']