From 914a40e3a1ab56257ef7d025293e1f21005a88bb Mon Sep 17 00:00:00 2001 From: undeath Date: Fri, 29 Jun 2018 14:13:01 +0200 Subject: [PATCH] adopt wallet_utils for new wallet --- jmclient/jmclient/__init__.py | 2 +- jmclient/jmclient/wallet_utils.py | 457 ++++++++++++++++-------------- 2 files changed, 241 insertions(+), 218 deletions(-) diff --git a/jmclient/jmclient/__init__.py b/jmclient/jmclient/__init__.py index 1dd44b1..8174bd9 100644 --- a/jmclient/jmclient/__init__.py +++ b/jmclient/jmclient/__init__.py @@ -46,7 +46,7 @@ from .taker_utils import (tumbler_taker_finished_update, restart_waiter, from .wallet_utils import ( wallet_tool_main, wallet_generate_recover_bip39, open_wallet, open_test_wallet_maybe, create_wallet, get_wallet_cls, get_wallet_path, - wallet_display, SewgitTestWallet) + 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 fbef685..e7bc12a 100644 --- a/jmclient/jmclient/wallet_utils.py +++ b/jmclient/jmclient/wallet_utils.py @@ -5,20 +5,16 @@ import sys import sqlite3 import binascii from datetime import datetime -from mnemonic import Mnemonic from optparse import OptionParser from jmclient import (get_network, WALLET_IMPLEMENTATIONS, Storage, podle, - encryptData, get_p2sh_vbyte, get_p2pk_vbyte, jm_single, mn_decode, - mn_encode, BitcoinCoreInterface, JsonRpcError, sync_wallet, WalletError, - BIP49Wallet, ImportWalletMixin, VolatileStorage, StoragePasswordError) + jm_single, BitcoinCoreInterface, JsonRpcError, sync_wallet, WalletError, + VolatileStorage, StoragePasswordError, + is_segwit_mode, SegwitLegacyWallet, LegacyWallet) from jmbase.support import get_password +from cryptoengine import TYPE_P2PKH, TYPE_P2SH_P2WPKH import jmclient.btc as btc -class SewgitTestWallet(ImportWalletMixin, BIP49Wallet): - TYPE = 'p2sh-p2wpkh' - - def get_wallettool_parser(): description = ( 'Use this script to monitor and manage your Joinmarket wallet.\n' @@ -46,8 +42,9 @@ def get_wallettool_parser(): '--maxmixdepth', action='store', type='int', - dest='maxmixdepth', - help='how many mixing depths to display, default=5') + dest='mixdepths', + help='how many mixing depths to initialize in the wallet', + default=5) parser.add_option('-g', '--gap-limit', type="int", @@ -86,9 +83,30 @@ def get_wallettool_parser(): type='str', dest='hd_path', help='hd wallet path (e.g. m/0/0/0/000)') + parser.add_option('--key-type', # note: keep in sync with map_key_type + type='choice', + choices=('standard', 'segwit-p2sh'), + action='store', + dest='key_type', + default=None, + help=("Key type when importing private keys.\n" + "If your address starts with '1' use 'standard', " + "if your address starts with '3' use 'segwit-p2sh.\n" + "Native segwit addresses (starting with 'bc') are" + "not yet supported.")) return parser +def map_key_type(parser_key_choice): + if not parser_key_choice: + return parser_key_choice + if parser_key_choice == 'standard': + return TYPE_P2PKH + if parser_key_choice == 'segwit-p2sh': + return TYPE_P2SH_P2WPKH + raise Exception("Unknown key type choice '{}'.".format(parser_key_choice)) + + """The classes in this module manage representations of wallet states; but they know nothing about Bitcoin, so do not attempt to validate addresses, keys, BIP32 or relationships. @@ -126,10 +144,9 @@ WalletView* classes manage wallet representations. """ class WalletViewBase(object): - def __init__(self, bip32path, children=None, serclass=str, + def __init__(self, wallet_path_repr, children=None, serclass=str, custom_separator=None): - assert bip32pathparse(bip32path) - self.bip32path = bip32path + self.wallet_path_repr = wallet_path_repr self.children = children self.serclass = serclass self.separator = custom_separator if custom_separator else "\t" @@ -143,11 +160,10 @@ class WalletViewBase(object): return "{0:.08f}".format(self.get_balance(include_unconf)) class WalletViewEntry(WalletViewBase): - def __init__(self, bip32path, account, forchange, aindex, addr, amounts, + def __init__(self, wallet_path_repr, account, forchange, aindex, addr, amounts, used = 'new', serclass=str, priv=None, custom_separator=None): - self.bip32path = bip32path - super(WalletViewEntry, self).__init__(bip32path, serclass=serclass, - custom_separator=custom_separator) + super(WalletViewEntry, self).__init__(wallet_path_repr, serclass=serclass, + custom_separator=custom_separator) self.account = account assert forchange in [0, 1, -1] self.forchange =forchange @@ -175,10 +191,7 @@ class WalletViewEntry(WalletViewBase): return self.serclass(self.separator.join([left, addr, amounts, extradata])) def serialize_wallet_position(self): - bippath = self.bip32path + bip32sep + str(self.account) + "'" + \ - bip32sep + str(self.forchange) + bip32sep + "{0:03d}".format(self.aindex) - assert bip32pathparse(bippath) - return self.serclass(bippath) + return self.wallet_path_repr.ljust(20) def serialize_address(self): return self.serclass(self.address) @@ -198,11 +211,11 @@ class WalletViewEntry(WalletViewBase): return self.serclass(ed) class WalletViewBranch(WalletViewBase): - def __init__(self, bip32path, account, forchange, branchentries=None, + def __init__(self, wallet_path_repr, account, forchange, branchentries=None, xpub=None, serclass=str, custom_separator=None): - super(WalletViewBranch, self).__init__(bip32path, children=branchentries, - serclass=serclass, - custom_separator=custom_separator) + super(WalletViewBranch, self).__init__(wallet_path_repr, children=branchentries, + serclass=serclass, + custom_separator=custom_separator) self.account = account assert forchange in [0, 1, -1] self.forchange = forchange @@ -223,20 +236,18 @@ class WalletViewBranch(WalletViewBase): return self.serclass(entryseparator.join(lines)) def serialize_branch_header(self): - bippath = self.bip32path + bip32sep + str(self.account) + "'" + \ - bip32sep + str(self.forchange) - assert bip32pathparse(bippath) start = "external addresses" if self.forchange == 0 else "internal addresses" if self.forchange == -1: start = "Imported keys" - return self.serclass(self.separator.join([start, bippath, self.xpub])) + return self.serclass(self.separator.join([start, self.wallet_path_repr, + self.xpub])) class WalletViewAccount(WalletViewBase): - def __init__(self, bip32path, account, branches=None, account_name="mixdepth", + def __init__(self, wallet_path_repr, account, branches=None, account_name="mixdepth", serclass=str, custom_separator=None, xpub=None): - super(WalletViewAccount, self).__init__(bip32path, children=branches, - serclass=serclass, - custom_separator=custom_separator) + super(WalletViewAccount, self).__init__(wallet_path_repr, children=branches, + serclass=serclass, + custom_separator=custom_separator) self.account = account self.account_name = account_name self.xpub = xpub @@ -259,12 +270,11 @@ class WalletViewAccount(WalletViewBase): x.serialize(entryseparator) for x in self.branches] + [footer])) class WalletView(WalletViewBase): - def __init__(self, bip32path, accounts, wallet_name="JM wallet", + def __init__(self, wallet_path_repr, accounts, wallet_name="JM wallet", serclass=str, custom_separator=None): - super(WalletView, self).__init__(bip32path, children=accounts, - serclass=serclass, - custom_separator=custom_separator) - self.bip32path = bip32path + super(WalletView, self).__init__(wallet_path_repr, children=accounts, + serclass=serclass, + custom_separator=custom_separator) self.wallet_name = wallet_name assert all([isinstance(x, WalletViewAccount) for x in accounts]) self.accounts = accounts @@ -280,40 +290,41 @@ class WalletView(WalletViewBase): x.serialize(entryseparator, summarize=False) for x in self.accounts] + [footer])) def get_imported_privkey_branch(wallet, m, showprivkey): - if m in wallet.imported_privkeys: - entries = [] - for i, privkey in enumerate(wallet.imported_privkeys[m]): - pub = btc.privkey_to_pubkey(privkey) - addr = btc.pubkey_to_p2sh_p2wpkh_address(pub, magicbyte=get_p2sh_vbyte()) - balance = 0.0 - for addrvalue in wallet.unspent.values(): - if addr == addrvalue['address']: - balance += addrvalue['value'] - used = ('used' if balance > 0.0 else 'empty') - if showprivkey: - wip_privkey = btc.wif_compressed_privkey( - privkey, get_p2pk_vbyte()) - else: - wip_privkey = '' - entries.append(WalletViewEntry("m/0", m, -1, - i, addr, [balance, balance], - used=used,priv=wip_privkey)) + entries = [] + for path in wallet.yield_imported_paths(m): + addr = wallet.get_addr_path(path) + script = wallet.get_script_path(path) + balance = 0.0 + for data in wallet.get_utxos_by_mixdepth_()[m].values(): + if script == data['script']: + balance += data['value'] + used = ('used' if balance > 0.0 else 'empty') + if showprivkey: + wip_privkey = wallet.get_wif_path(path) + else: + wip_privkey = '' + entries.append(WalletViewEntry(wallet.get_path_repr(path), m, -1, + 0, addr, [balance, balance], + used=used, priv=wip_privkey)) + + if entries: return WalletViewBranch("m/0", m, -1, branchentries=entries) return None def wallet_showutxos(wallet, showprivkey): unsp = {} max_tries = jm_single().config.getint("POLICY", "taker_utxo_retries") - for u, av in wallet.unspent.iteritems(): - key = wallet.get_key_from_addr(av['address']) - tries = podle.get_podle_tries(u, key, max_tries) - tries_remaining = max(0, max_tries - tries) - unsp[u] = {'address': av['address'], 'value': av['value'], - 'tries': tries, 'tries_remaining': tries_remaining, - 'external': False} - if showprivkey: - wifkey = btc.wif_compressed_privkey(key, vbyte=get_p2pk_vbyte()) - unsp[u]['privkey'] = wifkey + utxos = wallet.get_utxos_by_mixdepth() + for md in utxos: + for u, av in utxos[md].items(): + key = wallet.get_key_from_addr(av['address']) + tries = podle.get_podle_tries(u, key, max_tries) + tries_remaining = max(0, max_tries - tries) + unsp[u] = {'address': av['address'], 'value': av['value'], + 'tries': tries, 'tries_remaining': tries_remaining, + 'external': False} + if showprivkey: + unsp[u]['privkey'] = wallet.get_wif_path(av['path']) used_commitments, external_commitments = podle.get_podle_commitments() for u, ec in external_commitments.iteritems(): @@ -331,9 +342,9 @@ def wallet_display(wallet, gaplimit, showprivkey, displayall=False, then return its serialization directly if serialized, else return the WalletView object. """ + wallet.close() acctlist = [] - rootpath = wallet.get_root_path() - for m in xrange(wallet.max_mixdepth): + for m in xrange(wallet.max_mixdepth + 1): branchlist = [] for forchange in [0, 1]: entrylist = [] @@ -343,33 +354,37 @@ def wallet_display(wallet, gaplimit, showprivkey, displayall=False, else: xpub_key = "" - for k in xrange(wallet.get_next_unused_index(m, forchange) + gaplimit): + unused_index = wallet.get_next_unused_index(m, forchange) + for k in xrange(unused_index + gaplimit): path = wallet.get_path(m, forchange, k) addr = wallet.get_addr_path(path) balance = 0 for utxodata in wallet.get_utxos_by_mixdepth_()[m].values(): if path == utxodata['path']: balance += utxodata['value'] - used = 'used' if k < wallet.get_next_unused_index(m, forchange) else 'new' + used = 'used' if k < unused_index else 'new' if showprivkey: privkey = wallet.get_wif_path(path) else: privkey = '' if (displayall or balance > 0 or - (used == 'new' and forchange == 0)): - entrylist.append(WalletViewEntry(rootpath, m, forchange, k, - addr, [balance, balance], - priv=privkey, used=used)) - branchlist.append(WalletViewBranch(rootpath, m, forchange, - entrylist, xpub=xpub_key)) + (used == 'new' and forchange == 0)): + entrylist.append(WalletViewEntry( + wallet.get_path_repr(path), m, forchange, k, addr, + [balance, balance], priv=privkey, used=used)) + path = wallet.get_path_repr(wallet.get_path(m, forchange)) + branchlist.append(WalletViewBranch(path, m, forchange, entrylist, + xpub=xpub_key)) ipb = get_imported_privkey_branch(wallet, m, showprivkey) if ipb: branchlist.append(ipb) #get the xpub key of the whole account xpub_account = wallet.get_bip32_pub_export(mixdepth=m) - acctlist.append(WalletViewAccount(rootpath, m, branchlist, + path = wallet.get_path_repr(wallet.get_path(m)) + acctlist.append(WalletViewAccount(path, m, branchlist, xpub=xpub_account)) - walletview = WalletView(rootpath, acctlist) + path = wallet.get_path_repr(wallet.get_path()) + walletview = WalletView(path, acctlist) if serialized: return walletview.serialize(summarize=summarized) else: @@ -384,7 +399,7 @@ def cli_get_wallet_passphrase_check(): return password def cli_get_wallet_file_name(): - return raw_input('Input wallet file name (default: wallet.json): ') + return raw_input('Input wallet file name (default: wallet.jmdat): ') def cli_display_user_words(words, mnemonic_extension): text = 'Write down this wallet recovery mnemonic\n\n' + words +'\n' @@ -393,7 +408,7 @@ def cli_display_user_words(words, mnemonic_extension): print(text) def cli_user_mnemonic_entry(): - mnemonic_phrase = raw_input("Input 12 word mnemonic recovery phrase: ") + mnemonic_phrase = raw_input("Input mnemonic recovery phrase: ") mnemonic_extension = raw_input("Input mnemonic extension, leave blank if there isnt one: ") if len(mnemonic_extension.strip()) == 0: mnemonic_extension = None @@ -409,33 +424,8 @@ def cli_get_mnemonic_extension(): return raw_input("Enter mnemonic extension: ") -def persist_walletfile(walletspath, default_wallet_name, encrypted_entropy, - encrypted_mnemonic_extension=None, - callbacks=(cli_get_wallet_file_name,)): - timestamp = datetime.now().strftime("%Y/%m/%d %H:%M:%S") - walletjson = {'creator': 'joinmarket project', - 'creation_time': timestamp, - 'encrypted_entropy': encrypted_entropy.encode('hex'), - 'network': get_network()} - if encrypted_mnemonic_extension: - walletjson['encrypted_mnemonic_extension'] = encrypted_mnemonic_extension.encode('hex') - walletfile = json.dumps(walletjson) - walletname = callbacks[0]() - if len(walletname) == 0: - walletname = default_wallet_name - walletpath = os.path.join(walletspath, walletname) - # Does a wallet with the same name exist? - if os.path.isfile(walletpath): - print('ERROR: ' + walletpath + ' already exists. Aborting.') - return False - else: - fd = open(walletpath, 'w') - fd.write(walletfile) - fd.close() - print('saved to ' + walletname) - return True - def wallet_generate_recover_bip39(method, walletspath, default_wallet_name, + mixdepths=5, callbacks=(cli_display_user_words, cli_user_mnemonic_entry, cli_get_wallet_passphrase_check, @@ -449,68 +439,77 @@ def wallet_generate_recover_bip39(method, walletspath, default_wallet_name, 4 - enter mnemonic extension The defaults are for terminal entry. """ - #using 128 bit entropy, 12 words, mnemonic module - m = Mnemonic("english") + entropy = None + mnemonic_extension = None if method == "generate": mnemonic_extension = callbacks[4]() - words = m.generate() - callbacks[0](words, mnemonic_extension) elif method == 'recover': words, mnemonic_extension = callbacks[1]() + mnemonic_extension = mnemonic_extension and mnemonic_extension.strip() if not words: return False - entropy = str(m.to_entropy(words)) + try: + entropy = SegwitLegacyWallet.entropy_from_mnemonic(words) + except WalletError: + return False + else: + raise Exception("unknown method for wallet creation: '{}'" + .format(method)) + password = callbacks[2]() if not password: return False - password_key = btc.bin_dbl_sha256(password) - encrypted_entropy = encryptData(password_key, entropy) - encrypted_mnemonic_extension = None - if mnemonic_extension: - mnemonic_extension = mnemonic_extension.strip() - #check all ascii printable - if not all([a > '\x19' and a < '\x7f' for a in mnemonic_extension]): - return False - #padding to stop an adversary easily telling how long the mn extension is - #padding at the start because of how aes blocks are combined - #checksum in order to tell whether the decryption was successful - cleartext_length = 79 - padding_length = cleartext_length - 10 - len(mnemonic_extension) - if padding_length > 0: - padding = os.urandom(padding_length).replace('\xff', '\xfe') - else: - padding = '' - cleartext = (padding + '\xff' + mnemonic_extension + '\xff' - + btc.dbl_sha256(mnemonic_extension)[:8]) - encrypted_mnemonic_extension = encryptData(password_key, cleartext) - return persist_walletfile(walletspath, default_wallet_name, encrypted_entropy, - encrypted_mnemonic_extension, callbacks=(callbacks[3],)) + + wallet_name = callbacks[3]() + if not wallet_name: + wallet_name = default_wallet_name + wallet_path = os.path.join(walletspath, wallet_name) + + wallet = create_wallet(wallet_path, password, mixdepths - 1, + entropy=entropy, + entropy_extension=mnemonic_extension) + mnemonic, mnext = wallet.get_mnemonic_words() + callbacks[0] and callbacks[0](mnemonic, mnext or '') + wallet.close() + return True + def wallet_generate_recover(method, walletspath, - default_wallet_name='wallet.json'): - if jm_single().config.get("POLICY", "segwit") == "true": + default_wallet_name='wallet.jmdat', + mixdepths=5): + if is_segwit_mode(): #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) - print('Write down this wallet recovery seed\n\n' + ' '.join(words) + - '\n') - elif method == 'recover': - words = raw_input('Input 12 word recovery seed: ') - words = words.split() # default for split is 1 or more whitespace chars - if len(words) != 12: - print('ERROR: Recovery seed phrase must be exactly 12 words.') + return wallet_generate_recover_bip39( + method, walletspath, default_wallet_name, mixdepths=mixdepths) + + entropy = None + if method == 'recover': + seed = raw_input("Input 12 word recovery seed: ") + try: + entropy = LegacyWallet.entropy_from_mnemonic(seed) + except WalletError as e: + print("Unable to restore seed: {}".format(e.message)) return False - seed = mn_decode(words) - print(seed) + elif method != 'generate': + raise Exception("unknown method for wallet creation: '{}'" + .format(method)) + password = cli_get_wallet_passphrase_check() if not password: return False - password_key = btc.bin_dbl_sha256(password) - encrypted_seed = encryptData(password_key, seed.decode('hex')) - return persist_walletfile(walletspath, default_wallet_name, encrypted_seed) + + wallet_name = cli_get_wallet_file_name() + if not wallet_name: + wallet_name = default_wallet_name + wallet_path = os.path.join(walletspath, wallet_name) + + wallet = create_wallet(wallet_path, password, mixdepths - 1, + wallet_cls=LegacyWallet, entropy=entropy) + print("Write down and safely store this wallet recovery seed\n\n{}\n" + .format(wallet.get_mnemonic_words()[0])) + wallet.close() + return True + def wallet_fetch_history(wallet, options): # sort txes in a db because python can be really bad with large lists @@ -533,10 +532,12 @@ def wallet_fetch_history(wallet, options): in tx) tx_db.executemany('INSERT INTO transactions VALUES(?, ?, ?);', tx_data) - txes = tx_db.execute('SELECT DISTINCT txid, blockhash, blocktime ' - 'FROM transactions ORDER BY blocktime').fetchall() - wallet_addr_cache = wallet.addr_cache - wallet_addr_set = set(wallet_addr_cache.keys()) + + txes = tx_db.execute( + 'SELECT DISTINCT txid, blockhash, blocktime ' + 'FROM transactions ORDER BY blocktime').fetchall() + wallet_script_set = set(wallet.get_script_path(p) + for p in wallet.yield_known_paths()) def s(): return ',' if options.csv else ' ' @@ -575,13 +576,13 @@ def wallet_fetch_history(wallet, options): rpctx = jm_single().bc_interface.rpc('gettransaction', [tx['txid']]) txhex = str(rpctx['hex']) txd = btc.deserialize(txhex) - output_addr_values = dict(((btc.script_to_address(sv['script'], - get_p2sh_vbyte()), sv['value']) for sv in txd['outs'])) - our_output_addrs = wallet_addr_set.intersection( - output_addr_values.keys()) + output_script_values = {binascii.unhexlify(sv['script']): sv['value'] + for sv in txd['outs']} + our_output_scripts = wallet_script_set.intersection( + output_script_values.keys()) from collections import Counter - value_freq_list = sorted(Counter(output_addr_values.values()) + value_freq_list = sorted(Counter(output_script_values.values()) .most_common(), key=lambda x: -x[1]) non_cj_freq = 0 if len(value_freq_list)==1 else sum(zip( *value_freq_list[1:])[1]) @@ -601,12 +602,12 @@ def wallet_fetch_history(wallet, options): 'outpoint']['index']] rpc_inputs.append(input_dict) - rpc_input_addrs = set((btc.script_to_address(ind['script'], - get_p2sh_vbyte()) for ind in rpc_inputs)) - our_input_addrs = wallet_addr_set.intersection(rpc_input_addrs) - our_input_values = [ind['value'] for ind in rpc_inputs if btc. - script_to_address(ind['script'], get_p2sh_vbyte()) in - our_input_addrs] + rpc_input_scripts = set(binascii.unhexlify(ind['script']) + for ind in rpc_inputs) + our_input_scripts = wallet_script_set.intersection(rpc_input_scripts) + our_input_values = [ + ind['value'] for ind in rpc_inputs + if binascii.unhexlify(ind['script']) in our_input_scripts] our_input_value = sum(our_input_values) utxos_consumed = len(our_input_values) @@ -618,19 +619,19 @@ def wallet_fetch_history(wallet, options): mixdepth_dst = -1 #TODO this seems to assume all the input addresses are from the same # mixdepth, which might not be true - if len(our_input_addrs) == 0 and len(our_output_addrs) > 0: + if len(our_input_scripts) == 0 and len(our_output_scripts) > 0: #payment to us - amount = sum([output_addr_values[a] for a in our_output_addrs]) + amount = sum([output_script_values[a] for a in our_output_scripts]) tx_type = 'deposit ' cj_n = -1 delta_balance = amount - mixdepth_dst = tuple(wallet_addr_cache[a][0] for a in - our_output_addrs) + mixdepth_dst = tuple(wallet.get_script_mixdepth(a) + for a in our_output_scripts) if len(mixdepth_dst) == 1: mixdepth_dst = mixdepth_dst[0] - elif len(our_input_addrs) == 0 and len(our_output_addrs) == 0: + elif len(our_input_scripts) == 0 and len(our_output_scripts) == 0: continue # skip those that don't belong to our wallet - elif len(our_input_addrs) > 0 and len(our_output_addrs) == 0: + elif len(our_input_scripts) > 0 and len(our_output_scripts) == 0: # we swept coins elsewhere if is_coinjoin: tx_type = 'cj sweepout' @@ -638,13 +639,13 @@ def wallet_fetch_history(wallet, options): fees = our_input_value - cj_amount else: tx_type = 'sweep out ' - amount = sum([v for v in output_addr_values.values()]) + amount = sum([v for v in output_script_values.values()]) fees = our_input_value - amount delta_balance = -our_input_value - mixdepth_src = wallet_addr_cache[list(our_input_addrs)[0]][0] - elif len(our_input_addrs) > 0 and len(our_output_addrs) == 1: + mixdepth_src = wallet.get_script_mixdepth(list(our_input_scripts)[0]) + elif len(our_input_scripts) > 0 and len(our_output_scripts) == 1: # payment to somewhere with our change address getting the remaining - change_value = output_addr_values[list(our_output_addrs)[0]] + change_value = output_script_values[list(our_output_scripts)[0]] if is_coinjoin: tx_type = 'cj withdraw' amount = cj_amount @@ -655,25 +656,25 @@ def wallet_fetch_history(wallet, options): cj_n = -1 delta_balance = change_value - our_input_value fees = our_input_value - change_value - cj_amount - mixdepth_src = wallet_addr_cache[list(our_input_addrs)[0]][0] - elif len(our_input_addrs) > 0 and len(our_output_addrs) == 2: + mixdepth_src = wallet.get_script_mixdepth(list(our_input_scripts)[0]) + elif len(our_input_scripts) > 0 and len(our_output_scripts) == 2: #payment to self - out_value = sum([output_addr_values[a] for a in our_output_addrs]) + out_value = sum([output_script_values[a] for a in our_output_scripts]) if not is_coinjoin: print('this is wrong TODO handle non-coinjoin internal') tx_type = 'cj internal' amount = cj_amount delta_balance = out_value - our_input_value - mixdepth_src = wallet_addr_cache[list(our_input_addrs)[0]][0] - cj_addr = list(set([a for a,v in output_addr_values.iteritems() - if v == cj_amount]).intersection(our_output_addrs))[0] - mixdepth_dst = wallet_addr_cache[cj_addr][0] + mixdepth_src = wallet.get_script_mixdepth(list(our_input_scripts)[0]) + cj_script = list(set([a for a, v in output_script_values.iteritems() + if v == cj_amount]).intersection(our_output_scripts))[0] + mixdepth_dst = wallet.get_script_mixdepth(cj_script) else: tx_type = 'unknown type' - print('our utxos: ' + str(len(our_input_addrs)) \ - + ' in, ' + str(len(our_output_addrs)) + ' out') + print('our utxos: ' + str(len(our_input_scripts)) \ + + ' in, ' + str(len(our_output_scripts)) + ' out') balance += delta_balance - utxo_count += (len(our_output_addrs) - utxos_consumed) + utxo_count += (len(our_output_scripts) - utxos_consumed) index = '% 4d'%(i) timestamp = datetime.fromtimestamp(rpctx['blocktime'] ).strftime("%Y-%m-%d %H:%M") @@ -760,9 +761,10 @@ def wallet_fetch_history(wallet, options): print(('BUG ERROR: wallet balance (%s) does not match balance from ' + 'history (%s)') % (sat_to_str(total_wallet_balance), sat_to_str(balance))) - if utxo_count != len(wallet.unspent): + wallet_utxo_count = sum(map(len, wallet.get_utxos_by_mixdepth_().values())) + if utxo_count != wallet_utxo_count: print(('BUG ERROR: wallet utxo count (%d) does not match utxo count from ' + - 'history (%s)') % (len(wallet.unspent), utxo_count)) + 'history (%s)') % (wallet_utxo_count, utxo_count)) def wallet_showseed(wallet): @@ -773,7 +775,7 @@ def wallet_showseed(wallet): return text -def wallet_importprivkey(wallet, mixdepth): +def wallet_importprivkey(wallet, mixdepth, key_type): print("WARNING: This imported key will not be recoverable with your 12 " "word mnemonic phrase. Make sure you have backups.") print("WARNING: Handling of raw ECDSA bitcoin private keys can lead to " @@ -782,24 +784,35 @@ def wallet_importprivkey(wallet, mixdepth): privkeys = raw_input("Enter private key(s) to import: ") privkeys = privkeys.split(',') if ',' in privkeys else privkeys.split() imported_addr = [] + import_failed = 0 # TODO read also one key for each line for wif in privkeys: # TODO is there any point in only accepting wif format? check what # other wallets do - imported_addr.append(wallet.import_private_key(mixdepth, wif)) - wallet.save() + try: + path = wallet.import_private_key(mixdepth, wif, key_type=key_type) + except WalletError as e: + print("Failed to import key {}: {}".format(wif, e)) + import_failed += 1 + else: + imported_addr.append(wallet.get_addr_path(path)) if not imported_addr: print("Warning: No keys imported!") return + wallet.save() + # show addresses to user so they can verify everything went as expected - print("Imported keys for addresses:") - for addr in imported_addr: - print(addr) + print("Imported keys for addresses:\n{}".format('\n'.join(imported_addr))) + if import_failed: + print("Warning: failed to import {} keys".format(import_failed)) def wallet_dumpprivkey(wallet, hdpath): + if not hdpath: + print("Error: no hd wallet path supplied") + return False path = wallet.path_repr_to_path(hdpath) return wallet.get_wif_path(path) # will raise exception on invalid path @@ -807,17 +820,22 @@ def wallet_dumpprivkey(wallet, hdpath): def wallet_signmessage(wallet, hdpath, message): msg = message.encode('utf-8') + if not hdpath: + return "Error: no key path for signing specified" + if not message: + return "Error: no message specified" + path = wallet.path_repr_to_path(hdpath) sig = wallet.sign_message(msg, path) return ("Signature: {}\n" "To verify this in Bitcoin Core use the RPC command 'verifymessage'" - "".format(sig)) + .format(sig)) def get_wallet_type(): - if jm_single().config.get('POLICY', 'segwit') == 'true': - return 'p2sh-p2wpkh' - return 'p2pkh' + if is_segwit_mode(): + return TYPE_P2SH_P2WPKH + return TYPE_P2PKH def get_wallet_cls(wtype=None): @@ -832,15 +850,17 @@ def get_wallet_cls(wtype=None): return cls -def create_wallet(path, password, max_mixdepth, **kwargs): +def create_wallet(path, password, max_mixdepth, wallet_cls=None, **kwargs): storage = Storage(path, password, create=True) - wallet_cls = get_wallet_cls() + wallet_cls = wallet_cls or get_wallet_cls() wallet_cls.initialize(storage, get_network(), max_mixdepth=max_mixdepth, **kwargs) + storage.save() + return wallet_cls(storage) def open_test_wallet_maybe(path, seed, max_mixdepth, - test_wallet_cls=SewgitTestWallet, **kwargs): + test_wallet_cls=SegwitLegacyWallet, **kwargs): """ Create a volatile test wallet if path is a hex-encoded string of length 64, otherwise run open_wallet(). @@ -903,19 +923,18 @@ def open_wallet(path, ask_for_password=True, password=None, read_only=False, else: storage = Storage(path, password, read_only=read_only) - wallet_cls = get_wallet_cls(storage) + wallet_cls = get_wallet_cls_from_storage(storage) wallet = wallet_cls(storage, **kwargs) wallet_sanity_check(wallet) return wallet def get_wallet_cls_from_storage(storage): - wtype = storage.data.get([b'wallet_type']) + wtype = storage.data.get(b'wallet_type') - if not wtype: + if wtype is None: raise WalletError("File {} is not a valid wallet.".format(storage.path)) - wtype = wtype.decode('ascii') return get_wallet_cls(wtype) @@ -936,12 +955,6 @@ def wallet_tool_main(wallet_root_path): """ parser = get_wallettool_parser() (options, args) = parser.parse_args() - # if the index_cache stored in wallet.json is longer than the default - # then set maxmixdepth to the length of index_cache - maxmixdepth_configured = True - if not options.maxmixdepth: - maxmixdepth_configured = False - options.maxmixdepth = 5 noseed_methods = ['generate', 'recover'] methods = ['display', 'displayall', 'summary', 'showseed', 'importprivkey', @@ -953,6 +966,10 @@ def wallet_tool_main(wallet_root_path): parser.error('Needs a wallet file or method') sys.exit(0) + if options.mixdepths < 1: + parser.error("Must have at least one mixdepth.") + sys.exit(0) + if args[0] in noseed_methods: method = args[0] else: @@ -961,7 +978,7 @@ def wallet_tool_main(wallet_root_path): method = ('display' if len(args) == 1 else args[1].lower()) wallet = open_test_wallet_maybe( - wallet_path, seed, options.maxmixdepth, gap_limit=options.gaplimit) + wallet_path, seed, options.mixdepths - 1, gap_limit=options.gaplimit) if method not in noscan_methods: # if nothing was configured, we override bitcoind's options so that @@ -969,11 +986,13 @@ def wallet_tool_main(wallet_root_path): if 'listunspent_args' not in jm_single().config.options('POLICY'): jm_single().config.set('POLICY','listunspent_args', '[0]') sync_wallet(wallet, fast=options.fastsync) + wallet.save() #Now the wallet/data is prepared, execute the script according to the method if method == "display": return wallet_display(wallet, options.gaplimit, options.showprivkey) elif method == "displayall": - return wallet_display(wallet, options.gaplimit, options.showprivkey, displayall=True) + return wallet_display(wallet, options.gaplimit, options.showprivkey, + displayall=True) elif method == "summary": return wallet_display(wallet, options.gaplimit, options.showprivkey, summarized=True) elif method == "history": @@ -984,10 +1003,12 @@ def wallet_tool_main(wallet_root_path): else: return wallet_fetch_history(wallet, options) elif method == "generate": - retval = wallet_generate_recover("generate", wallet_root_path) + retval = wallet_generate_recover("generate", wallet_root_path, + mixdepths=options.mixdepths) return retval if retval else "Failed" elif method == "recover": - retval = wallet_generate_recover("recover", wallet_root_path) + retval = wallet_generate_recover("recover", wallet_root_path, + mixdepths=options.mixdepths) return retval if retval else "Failed" elif method == "showutxos": return wallet_showutxos(wallet, options.showprivkey) @@ -997,11 +1018,13 @@ def wallet_tool_main(wallet_root_path): return wallet_dumpprivkey(wallet, options.hd_path) elif method == "importprivkey": #note: must be interactive (security) - wallet_importprivkey(wallet, options.mixdepth) + wallet_importprivkey(wallet, options.mixdepth, + map_key_type(options.key_type)) return "Key import completed." elif method == "signmessage": return wallet_signmessage(wallet, options.hd_path, args[2]) + #Testing (can port to test modules, TODO) if __name__ == "__main__": if not test_bip32_pathparse():