diff --git a/jmclient/jmclient/__init__.py b/jmclient/jmclient/__init__.py index a2b10e3..d171434 100644 --- a/jmclient/jmclient/__init__.py +++ b/jmclient/jmclient/__init__.py @@ -37,7 +37,8 @@ from .commitment_utils import get_utxo_info, validate_utxo_data, quit from .taker_utils import (tumbler_taker_finished_update, restart_waiter, restart_wait, get_tumble_log, direct_send, tumbler_filter_orders_callback) -from .wallet_utils import wallet_tool_main +from .wallet_utils import (wallet_tool_main, wallet_generate_recover_bip39, + wallet_display) from .maker import Maker from .yieldgenerator import YieldGenerator, YieldGeneratorBasic, ygmain # Set default logging handler to avoid "No handler found" warnings. diff --git a/jmclient/jmclient/wallet_utils.py b/jmclient/jmclient/wallet_utils.py index 9e0fae5..96d3ccc 100644 --- a/jmclient/jmclient/wallet_utils.py +++ b/jmclient/jmclient/wallet_utils.py @@ -299,9 +299,11 @@ def wallet_showutxos(wallet, showprivkey): return json.dumps(unsp, indent=4) -def wallet_display(wallet, gaplimit, showprivkey, displayall=False): +def wallet_display(wallet, gaplimit, showprivkey, displayall=False, + serialized=True): """build the walletview object, - then return its serialization directly + then return its serialization directly if serialized, + else return the WalletView object. """ acctlist = [] rootpath = wallet.get_root_path() @@ -342,9 +344,12 @@ def wallet_display(wallet, gaplimit, showprivkey, displayall=False): acctlist.append(WalletViewAccount(rootpath, m, branchlist, xpub=xpub_account)) walletview = WalletView(rootpath, acctlist) - return walletview.serialize() + if serialized: + return walletview.serialize() + else: + return walletview -def get_password_check(): +def cli_password_check(): password = get_password('Enter wallet encryption passphrase: ') password2 = get_password('Reenter wallet encryption passphrase: ') if password != password2: @@ -353,13 +358,23 @@ def get_password_check(): password_key = btc.bin_dbl_sha256(password) return password, password_key -def persist_walletfile(walletspath, default_wallet_name, encrypted_seed): +def cli_get_walletname(): + return raw_input('Input wallet file name (default: wallet.json): ') + +def cli_user_words(words): + print('Write down this wallet recovery seed\n\n' + words +'\n') + +def cli_user_words_entry(): + return raw_input("Input 12 word recovery seed: ") + +def persist_walletfile(walletspath, default_wallet_name, encrypted_seed, + callbacks=(cli_get_walletname,)): timestamp = datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S") walletfile = json.dumps({'creator': 'joinmarket project', 'creation_time': timestamp, 'encrypted_seed': encrypted_seed.encode('hex'), 'network': get_network()}) - walletname = raw_input('Input wallet file name (default: wallet.json): ') + walletname = callbacks[0]() if len(walletname) == 0: walletname = default_wallet_name walletpath = os.path.join(walletspath, walletname) @@ -374,25 +389,39 @@ def persist_walletfile(walletspath, default_wallet_name, encrypted_seed): print('saved to ' + walletname) return True -def wallet_generate_recover_bip39(method, walletspath, default_wallet_name): +def wallet_generate_recover_bip39(method, walletspath, default_wallet_name, + callbacks=(cli_user_words, + cli_user_words_entry, + cli_password_check, + cli_get_walletname)): + """Optionally provide callbacks: + 0 - display seed + 1 - enter seed (for recovery) + 2 - enter password + 3 - enter wallet name + The defaults are for terminal entry. + """ #using 128 bit entropy, 12 words, mnemonic module m = Mnemonic("english") if method == "generate": words = m.generate() - print('Write down this wallet recovery seed\n\n' + words +'\n') + callbacks[0](words) elif method == 'recover': - words = raw_input('Input 12 word recovery seed: ') + words = callbacks[1]() entropy = str(m.to_entropy(words)) - password, password_key = get_password_check() + password, password_key = callbacks[2]() if not password: return False encrypted_entropy = encryptData(password_key, entropy) - return persist_walletfile(walletspath, default_wallet_name, encrypted_entropy) + return persist_walletfile(walletspath, default_wallet_name, encrypted_entropy, + callbacks=(callbacks[3],)) def wallet_generate_recover(method, walletspath, default_wallet_name='wallet.json'): if jm_single().config.get("POLICY", "segwit") == "true": - return wallet_generate_recover_bip39(method, walletspath, default_wallet_name) + #Here using default callbacks for scripts (not used in Qt) + return wallet_generate_recover_bip39(method, walletspath, + default_wallet_name) if method == 'generate': seed = btc.sha256(os.urandom(64))[:32] words = mn_encode(seed) @@ -406,7 +435,7 @@ def wallet_generate_recover(method, walletspath, return False seed = mn_decode(words) print(seed) - password, password_key = get_password_check() + password, password_key = cli_password_check() if not password: return False encrypted_seed = encryptData(password_key, seed.decode('hex')) diff --git a/scripts/joinmarket-qt.py b/scripts/joinmarket-qt.py index fb5f837..dc8ae85 100644 --- a/scripts/joinmarket-qt.py +++ b/scripts/joinmarket-qt.py @@ -44,16 +44,17 @@ donation_address = "1AZgQZWYRteh6UyF87hwuvyWj73NvWKpL" JM_CORE_VERSION = '0.2.2' JM_GUI_VERSION = '5' -from jmclient import (load_program_config, get_network, Wallet, - get_p2pk_vbyte, jm_single, validate_address, +from jmclient import (load_program_config, get_network, SegwitWallet, + get_p2sh_vbyte, jm_single, validate_address, get_log, weighted_order_choose, Taker, - JMTakerClientProtocolFactory, WalletError, + JMClientProtocolFactory, WalletError, start_reactor, get_schedule, get_tumble_schedule, - schedule_to_text, mn_decode, mn_encode, create_wallet_file, + schedule_to_text, create_wallet_file, get_blockchain_interface_instance, sync_wallet, direct_send, RegtestBitcoinCoreInterface, tweak_tumble_schedule, human_readable_schedule_entry, tumbler_taker_finished_update, - get_tumble_log, restart_wait, tumbler_filter_orders_callback) + get_tumble_log, restart_wait, tumbler_filter_orders_callback, + wallet_generate_recover_bip39, wallet_display) from qtsupport import (ScheduleWizard, TumbleRestartWizard, warnings, config_tips, config_types, TaskThread, QtHandler, XStream, Buttons, @@ -678,7 +679,7 @@ class SpendTab(QWidget): if not self.clientfactory: #First run means we need to start: create clientfactory #and start reactor Thread - self.clientfactory = JMTakerClientProtocolFactory(self.taker) + self.clientfactory = JMClientProtocolFactory(self.taker) thread = TaskThread(self) daemon = jm_single().config.getint("DAEMON", "no_daemon") daemon = True if daemon == 1 else False @@ -1128,18 +1129,24 @@ class JMWalletTab(QWidget): def create_menu(self, position): item = self.history.currentItem() address_valid = False + xpub_exists = False if item: - address = str(item.text(0)) - try: - btc.b58check_to_hex(address) + txt = str(item.text(0)) + if validate_address(txt)[0]: address_valid = True - except AssertionError: - log.debug('no btc address found, not creating menu item') + if "EXTERNAL" in txt: + parsed = txt.split() + if len(parsed) > 1: + xpub = parsed[1] + xpub_exists = True menu = QMenu() if address_valid: menu.addAction("Copy address to clipboard", - lambda: app.clipboard().setText(address)) + lambda: app.clipboard().setText(txt)) + if xpub_exists: + menu.addAction("Copy extended pubkey to clipboard", + lambda: app.clipboard().setText(xpub)) menu.addAction("Resync wallet from blockchain", lambda: w.resyncWallet()) #TODO add more items to context menu @@ -1150,7 +1157,7 @@ class JMWalletTab(QWidget): l.clear() if walletinfo: self.mainwindow = self.parent().parent().parent() - rows, mbalances, total_bal = walletinfo + rows, mbalances, xpubs, total_bal = walletinfo if get_network() == 'testnet': self.wallet_name = self.mainwindow.wallet.seed else: @@ -1167,9 +1174,10 @@ class JMWalletTab(QWidget): mdbalance, '', '', '', '']) l.addChild(m_item) for forchange in [0, 1]: - heading = 'EXTERNAL' if forchange == 0 else 'INTERNAL' - heading_end = ' addresses m/0/%d/%d/' % (i, forchange) - heading += heading_end + heading = "EXTERNAL" if forchange == 0 else "INTERNAL" + if walletinfo and heading == "EXTERNAL": + heading_end = ' ' + xpubs[i][forchange] + heading += heading_end seq_item = QTreeWidgetItem([heading, '', '', '', '']) m_item.addChild(seq_item) if not forchange: @@ -1313,7 +1321,7 @@ class JMMainWindow(QMainWindow): priv = self.wallet.get_key_from_addr(addr) private_keys[addr] = btc.wif_compressed_privkey( priv, - vbyte=get_p2pk_vbyte()) + vbyte=111) d.emit(QtCore.SIGNAL('computing_privkeys')) d.emit(QtCore.SIGNAL('show_privkeys')) @@ -1411,7 +1419,7 @@ class JMMainWindow(QMainWindow): title="Error") def selectWallet(self, testnet_seed=None): - if get_network() != 'testnet': + if jm_single().config.get("BLOCKCHAIN", "blockchain_source") != "regtest": current_path = os.path.dirname(os.path.realpath(__file__)) if os.path.isdir(os.path.join(current_path, 'wallets')): current_path = os.path.join(current_path, 'wallets') @@ -1448,7 +1456,7 @@ class JMMainWindow(QMainWindow): def loadWalletFromBlockchain(self, firstarg=None, pwd=None): if (firstarg and pwd) or (firstarg and get_network() == 'testnet'): try: - self.wallet = Wallet( + self.wallet = SegwitWallet( str(firstarg), pwd, max_mix_depth=jm_single().config.getint( @@ -1489,7 +1497,7 @@ class JMMainWindow(QMainWindow): def generateWallet(self): log.debug('generating wallet') - if get_network() == 'testnet': + if jm_single().config.get("BLOCKCHAIN", "blockchain_source") == "regtest": seed = self.getTestnetSeed() self.selectWallet(testnet_seed=seed) else: @@ -1506,24 +1514,7 @@ class JMMainWindow(QMainWindow): return return str(text).strip() - def initWallet(self, seed=None): - '''Creates a new mainnet - wallet - ''' - if not seed: - seed = btc.sha256(os.urandom(64))[:32] - words = mn_encode(seed) - mb = QMessageBox() - seed_recovery_warning = [ - "WRITE DOWN THIS WALLET RECOVERY SEED.", - "If you fail to do this, your funds are", - "at risk. Do NOT ignore this step!!!" - ] - mb.setText("\n".join(seed_recovery_warning)) - mb.setInformativeText(' '.join(words)) - mb.setStandardButtons(QMessageBox.Ok) - ret = mb.exec_() - + def getPassword(self): pd = PasswordDialog() while True: pd.exec_() @@ -1534,79 +1525,92 @@ class JMMainWindow(QMainWindow): title="Error") continue break + self.textpassword = str(pd.new_pw.text()) + + def getPasswordKey(self): + self.getPassword() + password_key = btc.bin_dbl_sha256(self.textpassword) + return (self.textpassword, password_key) - walletfile = create_wallet_file(str(pd.new_pw.text()), seed) + def getWalletName(self): walletname, ok = QInputDialog.getText(self, 'Choose wallet name', 'Enter wallet file name:', QLineEdit.Normal, "wallet.json") if not ok: JMQtMessageBox(self, "Create wallet aborted", mbtype='warn') - return - #create wallets subdir if it doesn't exist - if not os.path.exists('wallets'): - os.makedirs('wallets') - walletpath = os.path.join('wallets', str(walletname)) - # Does a wallet with the same name exist? - if os.path.isfile(walletpath): - JMQtMessageBox(self, - walletpath + ' already exists. Aborting.', - mbtype='warn', - title="Error") - return - else: - fd = open(walletpath, 'w') - fd.write(walletfile) - fd.close() - JMQtMessageBox(self, - 'Wallet saved to ' + str(walletname), + return None + self.walletname = str(walletname) + return self.walletname + + def displayWords(self, words): + mb = QMessageBox() + seed_recovery_warning = [ + "WRITE DOWN THIS WALLET RECOVERY SEED.", + "If you fail to do this, your funds are", + "at risk. Do NOT ignore this step!!!" + ] + mb.setText("\n".join(seed_recovery_warning)) + mb.setInformativeText(words) + mb.setStandardButtons(QMessageBox.Ok) + ret = mb.exec_() + + def initWallet(self, seed=None): + '''Creates a new mainnet + wallet + ''' + if not seed: + success = wallet_generate_recover_bip39("generate", + "wallets", + "wallet.json", + callbacks=(self.displayWords, + None, + self.getPasswordKey, + self.getWalletName)) + if not success: + JMQtMessageBox(self, "Failed to create new wallet file.", + title="Error", mbtype="warn") + return + JMQtMessageBox(self, 'Wallet saved to ' + self.walletname, title="Wallet created") - self.loadWalletFromBlockchain( - str(walletname), str(pd.new_pw.text())) + self.loadWalletFromBlockchain(self.walletname, pwd=self.textpassword) + else: + print('no seed to do') def get_wallet_printout(wallet): """Given a joinmarket wallet, retrieve the list of - addresses and corresponding balances to be displayed; - this could/should be a re-used function for both - command line and GUI. - The format of the retrieved data is: + addresses and corresponding balances to be displayed. + We retrieve a WalletView abstraction, and iterate over + sub-objects to arrange the per-mixdepth and per-address lists. + The format of the returned data is: rows: is of format [[[addr,index,bal,used],[addr,...]]*5, [[addr, index,..], [addr, index..]]*5] mbalances: is a simple array of 5 mixdepth balances - total_balance: whole wallet + xpubs: [[xpubext, xpubint], ...] Bitcoin amounts returned are in btc, not satoshis + TODO add metadata such as xpubs """ + walletview = wallet_display(wallet, jm_single().config.getint("GUI", + "gaplimit"), False, serialized=False) rows = [] mbalances = [] - total_balance = 0 - for m in range(wallet.max_mix_depth): + xpubs = [] + for j, acct in enumerate(walletview.children): + mbalances.append(acct.get_fmt_balance()) rows.append([]) - balance_depth = 0 - for forchange in [0, 1]: - rows[m].append([]) - for k in range(wallet.index[m][forchange] + jm_single( - ).config.getint("GUI", "gaplimit")): - addr = wallet.get_addr(m, forchange, k) - balance = 0.0 - for addrvalue in wallet.unspent.values(): - if addr == addrvalue['address']: - balance += addrvalue['value'] - balance_depth += balance - used = ('used' if k < wallet.index[m][forchange] else 'new') - if balance > 0.0 or (k >= wallet.index[m][forchange] and - forchange == 0): - rows[m][forchange].append([addr, str(k), "{0:.8f}".format( - balance / 1e8), used]) - mbalances.append(balance_depth) - total_balance += balance_depth - - return (rows, ["{0:.8f}".format(x / 1e8) for x in mbalances], - "{0:.8f}".format(total_balance / 1e8)) + xpubs.append([]) + for i, branch in enumerate(acct.children): + xpubs[j].append(branch.xpub) + rows[j].append([]) + for entry in branch.children: + rows[-1][i].append([entry.serialize_address(), + entry.serialize_wallet_position(), + entry.serialize_amounts(), + entry.serialize_extra_data()]) + return (rows, mbalances, xpubs, walletview.get_fmt_balance()) ################################ config_load_error = False -print('Temporarily disabled in version 0.3.0 waiting for update, please use scripts.') -sys.exit(0) app = QApplication(sys.argv) try: load_program_config() diff --git a/scripts/qtsupport.py b/scripts/qtsupport.py index 1a4467e..66bcca9 100644 --- a/scripts/qtsupport.py +++ b/scripts/qtsupport.py @@ -25,11 +25,7 @@ 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) +from jmclient import (jm_single, validate_address, get_tumble_schedule) GREEN_BG = "QWidget {background-color:#80ff80;}" @@ -413,8 +409,8 @@ class MyTreeWidget(QTreeWidget): 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) + #note, a single stretch column is currently not used. + self.header().setResizeMode(col, QHeaderView.Interactive) def editItem(self, item, column): if column in self.editable_columns: