From 6aaabb2f306bd6e9e8f4a24de23ad6d69480cc3c Mon Sep 17 00:00:00 2001 From: undeath Date: Sun, 24 Dec 2017 23:06:20 +0100 Subject: [PATCH] change yieldgenerator using new wallet implementation, start porting wallet_utils --- jmclient/jmclient/wallet_utils.py | 276 ++++++++++++++++++---------- jmclient/jmclient/yieldgenerator.py | 25 +-- 2 files changed, 186 insertions(+), 115 deletions(-) diff --git a/jmclient/jmclient/wallet_utils.py b/jmclient/jmclient/wallet_utils.py index 08e4e4a..c9b1ab6 100644 --- a/jmclient/jmclient/wallet_utils.py +++ b/jmclient/jmclient/wallet_utils.py @@ -9,10 +9,10 @@ from datetime import datetime from mnemonic import Mnemonic from optparse import OptionParser import getpass -from jmclient import (get_network, get_wallet_cls, Bip39Wallet, podle, - encryptData, get_p2sh_vbyte, get_p2pk_vbyte, jm_single, - mn_decode, mn_encode, BitcoinCoreInterface, - JsonRpcError, sync_wallet, WalletError) +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) from jmbase.support import get_password import jmclient.btc as btc @@ -330,25 +330,26 @@ def wallet_display(wallet, gaplimit, showprivkey, displayall=False, """ acctlist = [] rootpath = wallet.get_root_path() - for m in range(wallet.max_mix_depth): + for m in xrange(wallet.max_mixdepth): branchlist = [] for forchange in [0, 1]: entrylist = [] + # FIXME: why does this if/else exist? if forchange == 0: - xpub_key = btc.bip32_privtopub(wallet.keys[m][forchange]) + xpub_key = wallet.get_bip32_pub_export(m, forchange) else: xpub_key = "" - for k in range(wallet.index[m][forchange] + gaplimit): - addr = wallet.get_addr(m, forchange, k) + for k in xrange(wallet.get_next_unused_index(m, forchange) + gaplimit): + path = wallet.get_path(m, forchange, k) + addr = wallet.get_addr_path(path) balance = 0 - for addrvalue in wallet.unspent.values(): - if addr == addrvalue['address']: - balance += addrvalue['value'] - used = 'used' if k < wallet.index[m][forchange] else 'new' + 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' if showprivkey: - privkey = btc.wif_compressed_privkey( - wallet.get_key(m, forchange, k), get_p2pk_vbyte()) + privkey = wallet.get_wif_path(path) else: privkey = '' if (displayall or balance > 0 or @@ -362,8 +363,7 @@ def wallet_display(wallet, gaplimit, showprivkey, displayall=False, if ipb: branchlist.append(ipb) #get the xpub key of the whole account - xpub_account = btc.bip32_privtopub( - wallet.get_mixing_depth_keys(wallet.get_master_key())[m]) + xpub_account = wallet.get_bip32_pub_export(mixdepth=m) acctlist.append(WalletViewAccount(rootpath, m, branchlist, xpub=xpub_account)) walletview = WalletView(rootpath, acctlist) @@ -397,12 +397,14 @@ def cli_user_mnemonic_entry(): return (mnemonic_phrase, mnemonic_extension) def cli_get_mnemonic_extension(): - uin = raw_input('Would you like to use a two-factor mnemonic recovery' - + ' phrase? write \'n\' if you don\'t know what this is (y/n): ') + uin = raw_input("Would you like to use a two-factor mnemonic recovery " + "phrase? write 'n' if you don't know what this is (y/n): ") if len(uin) == 0 or uin[0] != 'y': - print('Not using mnemonic extension') + print("Not using mnemonic extension") return None #no mnemonic extension - return raw_input('Enter mnemonic extension: ') + print("Note: This will be stored in a reversible way. Do not reuse!") + return raw_input("Enter mnemonic extension: ") + def persist_walletfile(walletspath, default_wallet_name, encrypted_entropy, encrypted_mnemonic_extension=None, @@ -761,71 +763,170 @@ def wallet_fetch_history(wallet, options): def wallet_showseed(wallet): - if isinstance(wallet, Bip39Wallet): - if not wallet.entropy: - return "Entropy is not initialized." - m = Mnemonic("english") - text = "Wallet mnemonic recovery phrase:\n\n" + m.to_mnemonic(wallet.entropy) + "\n" - if wallet.mnemonic_extension: - text += '\nWallet mnemonic extension: ' + wallet.mnemonic_extension + '\n' - return text - hexseed = wallet.seed - print("hexseed = " + hexseed) - words = mn_encode(hexseed) - return "Wallet mnemonic seed phrase:\n\n" + " ".join(words) + "\n" + seed, extension = wallet.get_mnemonic_words() + text = "Wallet mnemonic recovery phrase:\n\n{}\n".format(seed) + if extension: + text += "\nWallet mnemonic extension: {}\n".format(extension) + return text + def wallet_importprivkey(wallet, mixdepth): - 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 ' - 'non-intuitive behaviour and loss of funds.\n Recommended instead ' - 'is to use the \'sweep\' feature of sendpayment.py ') - privkeys = raw_input('Enter private key(s) to import: ') + 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 " + "non-intuitive behaviour and loss of funds.\n Recommended instead " + "is to use the \'sweep\' feature of sendpayment.py.") + privkeys = raw_input("Enter private key(s) to import: ") privkeys = privkeys.split(',') if ',' in privkeys else privkeys.split() + imported_addr = [] # TODO read also one key for each line - for privkey in privkeys: + for wif in privkeys: # TODO is there any point in only accepting wif format? check what # other wallets do - privkey_bin = btc.from_wif_privkey(privkey, - vbyte=get_p2pk_vbyte()).decode('hex')[:-1] - encrypted_privkey = encryptData(wallet.password_key, privkey_bin) - if 'imported_keys' not in wallet.walletdata: - wallet.walletdata['imported_keys'] = [] - wallet.walletdata['imported_keys'].append( - {'encrypted_privkey': encrypted_privkey.encode('hex'), - 'mixdepth': mixdepth}) - if wallet.walletdata['imported_keys']: - fd = open(wallet.path, 'w') - fd.write(json.dumps(wallet.walletdata)) - fd.close() - print('Private key(s) successfully imported') + imported_addr.append(wallet.import_private_key(mixdepth, wif)) + wallet.save() + + if not imported_addr: + print("Warning: No keys imported!") + return + + # show addresses to user so they can verify everything went as expected + print("Imported keys for addresses:") + for addr in imported_addr: + print(addr) + def wallet_dumpprivkey(wallet, hdpath): - pathlist = bip32pathparse(hdpath) - print('got pathlist: ' + str(pathlist)) - if pathlist and len(pathlist) in [5, 4]: - #note here we assume the path conforms to Wallet or SegwitWallet(BIP49) standard - m, forchange, k = pathlist[-3:] - key = wallet.get_key(m, forchange, k) - wifkey = btc.wif_compressed_privkey(key, vbyte=get_p2pk_vbyte()) - return wifkey - else: - return hdpath + " is not a valid hd wallet path" + path = wallet.path_repr_to_path(hdpath) + return wallet.get_wif_path(path) # will raise exception on invalid path + def wallet_signmessage(wallet, hdpath, message): - if hdpath.startswith(wallet.get_root_path()): - hp = bip32pathparse(hdpath) - m, forchange, k = hp[-3:] - key = wallet.get_key(m, forchange, k) - addr = wallet.pubkey_to_address(btc.privkey_to_pubkey(key)) - print('Using address: ' + addr) + msg = message.encode('utf-8') + + 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)) + + +def get_wallet_type(): + if jm_single().config.get('POLICY', 'segwit') == 'true': + return 'p2sh-p2wpkh' + return 'p2pkh' + + +def get_wallet_cls(wtype=None): + if wtype is None: + wtype = get_wallet_type() + + cls = WALLET_IMPLEMENTATIONS.get(wtype) + + if not cls: + raise WalletError("No wallet implementation found for type {}." + "".format(wtype)) + return cls + + +def create_wallet(path, password, max_mixdepth, **kwargs): + storage = Storage(path, password, create=True) + wallet_cls = get_wallet_cls() + wallet_cls.initialize(storage, get_network(), max_mixdepth=max_mixdepth, + **kwargs) + + +def open_test_wallet_maybe(path, seed, max_mixdepth, **kwargs): + """ + Create a volatile test wallet if path is a hex-encoded string of length 64, + otherwise run open_wallet(). + + params: + path: path to wallet file, ignored for test wallets + seed: hex-encoded test seed + max_mixdepth: see create_wallet(), ignored when calling open_wallet() + kwargs: see open_wallet() + + returns: + wallet object + """ + class SewgitTestWallet(ImportWalletMixin, BIP49Wallet): + TYPE = 'p2sh-p2wpkh' + + if len(seed) == SewgitTestWallet.ENTROPY_BYTES * 2: + try: + seed = binascii.unhexlify(seed) + except binascii.Error: + pass + else: + storage = VolatileStorage() + SewgitTestWallet.initialize( + storage, get_network(), max_mixdepth=max_mixdepth, + entropy=seed) + assert 'ask_for_password' not in kwargs + assert 'read_only' not in kwargs + return SewgitTestWallet(storage, **kwargs) + + return open_wallet(path, **kwargs) + + +def open_wallet(path, ask_for_password=True, read_only=False, **kwargs): + """ + Open the wallet file at path and return the corresponding wallet object. + + params: + path: str, full path to wallet file + ask_for_password: bool, if False password is assumed unset and user + will not be asked to type it + read_only: bool, if True, open wallet in read-only mode + kwargs: additional options to pass to wallet's init method + + returns: + wallet object + """ + if ask_for_password: + while True: + try: + # do not try empty password, assume unencrypted on empty password + pwd = get_password("Enter wallet decryption passphrase: ") or None + storage = Storage(path, password=pwd, read_only=read_only) + except StoragePasswordError: + print("Wrong password, try again.") + continue + except Exception as e: + print("Failed to load wallet, error message: " + repr(e)) + raise e + break else: - print('%s is not a valid hd wallet path' % hdpath) - return None - sig = btc.ecdsa_sign(message, key, formsg=True) - retval = "Signature: " + str(sig) + "\n" - retval += "To verify this in Bitcoin Core use the RPC command 'verifymessage'" - return retval + storage = Storage(path, read_only=read_only) + + wallet_cls = get_wallet_cls(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']) + + if not wtype: + raise WalletError("File {} is not a valid wallet.".format(storage.path)) + + wtype = wtype.decode('ascii') + return get_wallet_cls(wtype) + + +def wallet_sanity_check(wallet): + if wallet.network != get_network(): + raise Exception("Wallet network mismatch: we are on {} but wallet is " + "on {}".format(get_network(), wallet.network)) + + +def get_wallet_path(file_name, wallet_dir): + # TODO: move default wallet path to ~/.joinmarket + wallet_dir = wallet_dir or 'wallets' + return os.path.join(wallet_dir, file_name) + def wallet_tool_main(wallet_root_path): """Main wallet tool script function; returned is a string (output or error) @@ -853,29 +954,12 @@ def wallet_tool_main(wallet_root_path): method = args[0] else: seed = args[0] + wallet_path = get_wallet_path(seed, wallet_root_path) method = ('display' if len(args) == 1 else args[1].lower()) - if not os.path.exists(os.path.join(wallet_root_path, seed)): - wallet = get_wallet_cls()(seed, None, options.maxmixdepth, - options.gaplimit, extend_mixdepth= not maxmixdepth_configured, - storepassword=(method == 'importprivkey'), - wallet_dir=wallet_root_path) - else: - while True: - try: - pwd = get_password("Enter wallet decryption passphrase: ") - wallet = get_wallet_cls()(seed, pwd, - options.maxmixdepth, - options.gaplimit, - extend_mixdepth=not maxmixdepth_configured, - storepassword=(method == 'importprivkey'), - wallet_dir=wallet_root_path) - except WalletError: - print("Wrong password, try again.") - continue - except Exception as e: - print("Failed to load wallet, error message: " + repr(e)) - sys.exit(0) - break + + wallet = open_test_wallet_maybe( + wallet_path, seed, options.maxmixdepth, gap_limit=options.gaplimit) + if method not in noscan_methods: # if nothing was configured, we override bitcoind's options so that # unconfirmed balance is included in the wallet display by default diff --git a/jmclient/jmclient/yieldgenerator.py b/jmclient/jmclient/yieldgenerator.py index 72dc585..564933d 100644 --- a/jmclient/jmclient/yieldgenerator.py +++ b/jmclient/jmclient/yieldgenerator.py @@ -7,10 +7,10 @@ import time import abc from twisted.python.log import startLogging from optparse import OptionParser -from jmbase import get_password from jmclient import (Maker, jm_single, get_network, load_program_config, get_log, get_wallet_cls, sync_wallet, JMClientProtocolFactory, start_reactor, calc_cj_fee, WalletError) +from .wallet_utils import open_test_wallet_maybe, get_wallet_path jlog = get_log() @@ -232,23 +232,11 @@ def ygmain(ygclass, txfee=1000, cjfee_a=200, cjfee_r=0.002, ordertype='swreloffe nickserv_password = options.password load_program_config() - if not os.path.exists(os.path.join('wallets', wallet_name)): - wallet = get_wallet_cls()(wallet_name, None, max_mix_depth=MAX_MIX_DEPTH, - gaplimit=options.gaplimit) - else: - while True: - try: - pwd = get_password("Enter wallet decryption passphrase: ") - wallet = get_wallet_cls()(wallet_name, pwd, - max_mix_depth=MAX_MIX_DEPTH, - gaplimit=options.gaplimit) - except WalletError: - print("Wrong password, try again.") - continue - except Exception as e: - print("Failed to load wallet, error message: " + repr(e)) - sys.exit(0) - break + + wallet_path = get_wallet_path(wallet_name, 'wallets') + wallet = open_test_wallet_maybe( + wallet_path, wallet_name, 4, gap_limit=options.gaplimit) + if jm_single().config.get("BLOCKCHAIN", "blockchain_source") == "electrum-server": jm_single().bc_interface.synctype = "with-script" sync_wallet(wallet, fast=options.fastsync) @@ -265,4 +253,3 @@ def ygmain(ygclass, txfee=1000, cjfee_a=200, cjfee_r=0.002, ordertype='swreloffe start_reactor(jm_single().config.get("DAEMON", "daemon_host"), jm_single().config.getint("DAEMON", "daemon_port"), clientfactory, daemon=daemon) -