diff --git a/jmclient/jmclient/__init__.py b/jmclient/jmclient/__init__.py index 489c749..d9569f2 100644 --- a/jmclient/jmclient/__init__.py +++ b/jmclient/jmclient/__init__.py @@ -38,6 +38,7 @@ 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 # Set default logging handler to avoid "No handler found" warnings. try: diff --git a/jmclient/jmclient/wallet_utils.py b/jmclient/jmclient/wallet_utils.py new file mode 100644 index 0000000..699c96f --- /dev/null +++ b/jmclient/jmclient/wallet_utils.py @@ -0,0 +1,531 @@ +from __future__ import print_function +import json +import os +import pprint +import sys +import datetime +from decimal import Decimal +from optparse import OptionParser + +from jmclient import (get_network, Wallet, + encryptData, get_p2pk_vbyte, jm_single, + mn_decode, mn_encode, BitcoinCoreInterface, + JsonRpcError, sync_wallet, WalletError) +from jmbase.support import get_password +import jmclient.btc as btc + +def get_wallettool_parser(): + description = ( + 'Does useful little tasks involving your bip32 wallet. The ' + 'method is one of the following: (display) Shows addresses and ' + 'balances. (displayall) Shows ALL addresses and balances. ' + '(summary) Shows a summary of mixing depth balances. (generate) ' + 'Generates a new wallet. (recover) Recovers a wallet from the 12 ' + 'word recovery seed. (showutxos) Shows all utxos in the wallet, ' + 'including the corresponding private keys if -p is chosen; the ' + 'data is also written to a file "walletname.json.utxos" if the ' + 'option -u is chosen (so be careful about private keys). ' + '(showseed) Shows the wallet recovery seed ' + 'and hex seed. (importprivkey) Adds privkeys to this wallet, ' + 'privkeys are spaces or commas separated. (dumpprivkey) Export ' + 'a single private key, specify an hd wallet path (listwallets) ' + 'Lists all wallets with creator and timestamp. (history) Show ' + 'all historical transaction details. Requires Bitcoin Core.\n' + 'signmessage\t\tSign a message with the private key from an address\n' + '\t\t\tin the wallet. Use with -H and specify an HD wallet\n' + '\t\t\tpath for the address.') + parser = OptionParser(usage='usage: %prog [options] [wallet file] [method]', + description=description) + parser.add_option('-p', + '--privkey', + action='store_true', + dest='showprivkey', + help='print private key along with address, default false') + parser.add_option('-m', + '--maxmixdepth', + action='store', + type='int', + dest='maxmixdepth', + help='how many mixing depths to display, default=5') + parser.add_option('-g', + '--gap-limit', + type="int", + action='store', + dest='gaplimit', + help='gap limit for wallet, default=6', + default=6) + parser.add_option('-M', + '--mix-depth', + type="int", + action='store', + dest='mixdepth', + help='mixing depth to import private key into', + default=0) + parser.add_option('--csv', + action='store_true', + dest='csv', + default=False, + help=('When using the history method, output as csv')) + parser.add_option('--fast', + action='store_true', + dest='fastsync', + default=False, + help=('choose to do fast wallet sync, only for Core and ' + 'only for previously synced wallet')) + parser.add_option('-H', + '--hd', + action='store', + type='str', + dest='hd_path', + help='hd wallet path (e.g. m/0/0/0/000)') + return parser + + +"""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. +A console based output is provided as default, but underlying serializations +can be used by calling classes for UIs. +""" + +bip32sep = '/' + +def bip32pathparse(path): + if not path.startswith('m'): + return False + elements = path.split(bip32sep)[1:] + for e in elements: + try: + x = int(e) + except: + return False + if not e >= -1: + #-1 is allowed for dummy branches for imported keys + return False + return True + +def test_bip32_pathparse(): + assert bip32pathparse("m/2/1/0017") + assert not bip32pathparse("n/1/1/1/1") + assert bip32pathparse("m/0/1/100/3/2/2/21/004/005") + assert not bip32pathparse("m/0/0/00k") + return True +""" +WalletView* classes manage wallet representations. +""" + +class WalletViewBase(object): + def __init__(self, bip32path, children=None, serclass=str, + custom_separator=None): + assert bip32pathparse(bip32path) + self.bip32path = bip32path + self.children = children + self.serclass = serclass + self.separator = custom_separator if custom_separator else "\t" + + def get_balance(self, include_unconf=True): + if not include_unconf: + raise NotImplementedError("Separate conf/unconf balances not impl.") + return sum([x.get_balance() for x in self.children]) + + def get_fmt_balance(self, include_unconf=True): + return "{0:.08f}".format(self.get_balance(include_unconf)) + +class WalletViewEntry(WalletViewBase): + def __init__(self, bip32path, 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) + self.account = account + assert forchange in [0, 1, -1] + self.forchange =forchange + assert isinstance(aindex, int) + assert aindex >= 0 + self.aindex = aindex + self.address = addr + self.unconfirmed_amount, self.confirmed_amount = amounts + #note no validation here + self.private_key = priv + self.used = used + + def get_balance(self, include_unconf=True): + """Overwrites base class since no children + """ + if not include_unconf: + raise NotImplementedError("Separate conf/unconf balances not impl.") + return self.unconfirmed_amount/1e8 + + def serialize(self): + left = self.serialize_wallet_position() + addr = self.serialize_address() + amounts = self.serialize_amounts() + extradata = self.serialize_extra_data() + 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) + + def serialize_address(self): + return self.serclass(self.address) + + def serialize_amounts(self, unconf_separate=False, denom="BTC"): + if denom != "BTC": + raise NotImplementedError("Altern. denominations not yet implemented.") + if unconf_separate: + raise NotImplementedError("Separate handling of unconfirmed funds " + "not yet implemented.") + return self.serclass("{0:.08f}".format(self.unconfirmed_amount/1e8)) + + def serialize_extra_data(self): + ed = self.used + if self.private_key: + ed += self.separator + self.serclass(self.private_key) + return self.serclass(ed) + +class WalletViewBranch(WalletViewBase): + def __init__(self, bip32path, account, forchange, branchentries=None, + xpub=None, serclass=str, custom_separator=None): + super(WalletViewBranch, self).__init__(bip32path, children=branchentries, + serclass=serclass, + custom_separator=custom_separator) + self.account = account + assert forchange in [0, 1, -1] + self.forchange = forchange + if xpub: + assert xpub.startswith('xpub') or xpub.startswith('tpub') + self.xpub = xpub if xpub else "" + self.branchentries = branchentries + + def serialize(self, entryseparator="\n"): + lines = [self.serialize_branch_header()] + for we in self.branchentries: + lines.append(we.serialize()) + footer = "Balance:" + self.separator + self.get_fmt_balance() + lines.append(footer) + 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])) + +class WalletViewAccount(WalletViewBase): + def __init__(self, bip32path, account, branches=None, account_name="mixdepth", + serclass=str, custom_separator=None): + super(WalletViewAccount, self).__init__(bip32path, children=branches, + serclass=serclass, + custom_separator=custom_separator) + self.account = account + self.account_name = account_name + if branches: + assert len(branches) in [2, 3] #3 if imported keys + assert all([isinstance(x, WalletViewBranch) for x in branches]) + self.branches = branches + + def serialize(self, entryseparator="\n"): + header = self.account_name + self.separator + str(self.account) + footer = "Balance for mixdepth " + str( + self.account) + ":" + self.separator + self.get_fmt_balance() + return self.serclass(entryseparator.join([header] + [ + x.serialize(entryseparator) for x in self.branches] + [footer])) + +class WalletView(WalletViewBase): + def __init__(self, bip32path, 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 + self.wallet_name = wallet_name + assert all([isinstance(x, WalletViewAccount) for x in accounts]) + self.accounts = accounts + + def serialize(self, entryseparator="\n"): + header = self.wallet_name + footer = "Total balance:" + self.separator + self.get_fmt_balance() + return self.serclass(entryseparator.join([header] + [ + x.serialize(entryseparator) 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]): + addr = btc.privtoaddr(privkey, magicbyte=get_p2pk_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)) + return WalletViewBranch("m/0", m, -1, branchentries=entries) + return None + +def wallet_showutxos(wallet, showprivkey): + unsp = {} + if showprivkey: + for u, av in wallet.unspent.iteritems(): + addr = av['address'] + key = wallet.get_key_from_addr(addr) + wifkey = btc.wif_compressed_privkey(key, vbyte=get_p2pk_vbyte()) + unsp[u] = {'address': av['address'], + 'value': av['value'], 'privkey': wifkey} + else: + unsp = wallet.unspent + return json.dumps(unsp, indent=4) + +def wallet_display(wallet, gaplimit, showprivkey, displayall=False): + """build the walletview object, + then return its serialization directly + """ + acctlist = [] + for m in range(wallet.max_mix_depth): + branchlist = [] + for forchange in [0, 1]: + entrylist = [] + if forchange == 0: + xpub_key = btc.bip32_privtopub(wallet.keys[m][forchange]) + else: + xpub_key = "" + + for k in range(wallet.index[m][forchange] + gaplimit): + addr = wallet.get_addr(m, forchange, k) + 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' + if showprivkey: + privkey = btc.wif_compressed_privkey( + wallet.get_key(m, forchange, k), get_p2pk_vbyte()) + else: + privkey = '' + if (displayall or balance > 0 or + (used == 'new' and forchange == 0)): + entrylist.append(WalletViewEntry("m/0", m, forchange, k, + addr, [balance, balance], + priv=privkey, used=used)) + branchlist.append(WalletViewBranch("m/0", m, forchange, entrylist, + xpub=xpub_key)) + ipb = get_imported_privkey_branch(wallet, m, showprivkey) + if ipb: + branchlist.append(ipb) + acctlist.append(WalletViewAccount("m/0", m, branchlist)) + walletview = WalletView("m/0", acctlist) + return walletview.serialize() + +def wallet_generate_recover(method, walletspath, default_wallet_name='wallet.json'): + 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 False + seed = mn_decode(words) + print(seed) + password = getpass.getpass('Enter wallet encryption passphrase: ') + password2 = getpass.getpass('Reenter wallet encryption passphrase: ') + if password != password2: + print('ERROR. Passwords did not match') + return False + password_key = btc.bin_dbl_sha256(password) + encrypted_seed = encryptData(password_key, seed.decode('hex')) + 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): ') + 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_showseed(wallet): + hexseed = wallet.seed + print("hexseed = " + hexseed) + words = mn_encode(hexseed) + return "Wallet recovery seed\n\n" + " ".join(words) + "\n" + +def wallet_importprivkey(wallet, mixdepth): + print('WARNING: This imported key will not be recoverable with your 12 ' + + 'word mnemonic seed. 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() + # TODO read also one key for each line + for privkey 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') + +def wallet_dumpprivkey(wallet, hdpath): + if bip32pathparse(hdpath): + m, forchange, k = [int(y) for y in hdpath[4:].split('/')] + 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" + +def wallet_signmessage(wallet, hdpath, message): + if hdpath.startswith('m/0/'): + m, forchange, k = [int(y) for y in hdpath[4:].split('/')] + key = wallet.get_key(m, forchange, k) + addr = btc.privkey_to_address(key, magicbyte=get_p2pk_vbyte()) + print('Using address: ' + addr) + 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 + +def wallet_tool_main(wallet_root_path): + """Main wallet tool script function; returned is a string (output or error) + """ + 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', + 'showutxos'] + methods.extend(noseed_methods) + noscan_methods = ['showseed', 'importprivkey', 'dumpprivkey', 'signmessage'] + + if len(args) < 1: + parser.error('Needs a wallet file or method') + sys.exit(0) + + if args[0] in noseed_methods: + method = args[0] + else: + seed = args[0] + method = ('display' if len(args) == 1 else args[1].lower()) + if not os.path.exists(os.path.join(wallet_root_path, seed)): + wallet = Wallet(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 = Wallet(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 + 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 + if 'listunspent_args' not in jm_single().config.options('POLICY'): + jm_single().config.set('POLICY','listunspent_args', '[0]') + sync_wallet(wallet, fast=options.fastsync) + #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) + elif method == "generate": + retval = wallet_generate_recover("generate", wallet_root_path) + return retval if retval else "Failed" + elif method == "recover": + retval = wallet_generate_recover("recover", wallet_root_path) + return retval if retval else "Failed" + elif method == "showutxos": + return wallet_showutxos(wallet, options.showprivkey) + elif method == "showseed": + return wallet_showseed(wallet) + elif method == "dumpprivkey": + return wallet_dumpprivkey(wallet, options.hd_path) + elif method == "importprivkey": + #note: must be interactive (security) + wallet_importprivkey(wallet, options.mixdepth) + return "Key import completed." + elif method == "signmessage": + return wallet_signmessage(wallet, options.hd_path, args[1]) + +#Testing (can port to test modules, TODO) +if __name__ == "__main__": + if not test_bip32_pathparse(): + sys.exit(0) + rootpath="m/0" + walletbranch = 0 + accounts = range(3) + acctlist = [] + for a in accounts: + branches = [] + for forchange in range(2): + entries = [] + for i in range(4): + entries.append(WalletViewEntry(rootpath, a, forchange, + i, "DUMMYADDRESS"+str(i+a), + [i*10000000, i*10000000])) + branches.append(WalletViewBranch(rootpath, + a, forchange, branchentries=entries, + xpub="xpubDUMMYXPUB"+str(a+forchange))) + acctlist.append(WalletViewAccount(rootpath, a, branches=branches)) + wallet = WalletView(rootpath + "/" + str(walletbranch), + accounts=acctlist) + print(wallet.serialize()) + \ No newline at end of file diff --git a/scripts/wallet-tool.py b/scripts/wallet-tool.py index e2238ee..2e50513 100644 --- a/scripts/wallet-tool.py +++ b/scripts/wallet-tool.py @@ -1,534 +1,8 @@ from __future__ import absolute_import, print_function -import datetime -import getpass -import json -import os -import sys -import sqlite3 -from optparse import OptionParser +from jmclient import load_program_config, wallet_tool_main -from jmclient import (load_program_config, get_network, Wallet, - encryptData, get_p2pk_vbyte, jm_single, - mn_decode, mn_encode, BitcoinCoreInterface, - JsonRpcError, sync_wallet, WalletError) - -from jmbase.support import get_password - -import jmclient.btc as btc - -description = ( - 'Does useful little tasks involving your bip32 wallet. The ' - 'method is one of the following: (display) Shows addresses and ' - 'balances. (displayall) Shows ALL addresses and balances. ' - '(summary) Shows a summary of mixing depth balances. (generate) ' - 'Generates a new wallet. (recover) Recovers a wallet from the 12 ' - 'word recovery seed. (showutxos) Shows all utxos in the wallet, ' - 'including the corresponding private keys if -p is chosen; the ' - 'data is also written to a file "walletname.json.utxos" if the ' - 'option -u is chosen (so be careful about private keys). ' - '(showseed) Shows the wallet recovery seed ' - 'and hex seed. (importprivkey) Adds privkeys to this wallet, ' - 'privkeys are spaces or commas separated. (dumpprivkey) Export ' - 'a single private key, specify an hd wallet path (listwallets) ' - 'Lists all wallets with creator and timestamp. (history) Show ' - 'all historical transaction details. Requires Bitcoin Core.\n' - 'signmessage\t\tSign a message with the private key from an address\n' - '\t\t\tin the wallet. Use with -H and specify an HD wallet\n' - '\t\t\tpath for the address.') - -parser = OptionParser(usage='usage: %prog [options] [wallet file] [method]', - description=description) - -parser.add_option('-p', - '--privkey', - action='store_true', - dest='showprivkey', - help='print private key along with address, default false') -parser.add_option('-m', - '--maxmixdepth', - action='store', - type='int', - dest='maxmixdepth', - help='how many mixing depths to display, default=5') -parser.add_option('-g', - '--gap-limit', - type="int", - action='store', - dest='gaplimit', - help='gap limit for wallet, default=6', - default=6) -parser.add_option('-M', - '--mix-depth', - type="int", - action='store', - dest='mixdepth', - help='mixing depth to import private key into', - default=0) -parser.add_option('--csv', - action='store_true', - dest='csv', - default=False, - help=('When using the history method, output as csv')) -parser.add_option('--fast', - action='store_true', - dest='fastsync', - default=False, - help=('choose to do fast wallet sync, only for Core and ' - 'only for previously synced wallet')) -parser.add_option('-H', - '--hd', - action='store', - type='str', - dest='hd_path', - help='hd wallet path (e.g. m/0/0/0/000)') -(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', 'listwallets'] -methods = ['display', 'displayall', 'summary', 'showseed', 'importprivkey', - 'history', 'showutxos'] -methods.extend(noseed_methods) -noscan_methods = ['showseed', 'importprivkey', 'dumpprivkey', 'signmessage'] - -if len(args) < 1: - parser.error('Needs a wallet file or method') - sys.exit(0) - -load_program_config() - -if args[0] in noseed_methods: - method = args[0] -else: - seed = args[0] - method = ('display' if len(args) == 1 else args[1].lower()) - if not os.path.exists(os.path.join('wallets', seed)): - wallet = Wallet(seed, None, options.maxmixdepth, - options.gaplimit, extend_mixdepth= not maxmixdepth_configured, - storepassword=(method == 'importprivkey')) - else: - while True: - try: - pwd = get_password("Enter wallet decryption passphrase: ") - wallet = Wallet(seed, pwd, - options.maxmixdepth, - options.gaplimit, - extend_mixdepth=not maxmixdepth_configured, - storepassword=(method == 'importprivkey')) - 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 - if method == 'history' and not isinstance(jm_single().bc_interface, - BitcoinCoreInterface): - print('showing history only available when using the Bitcoin Core ' + - 'blockchain interface') - sys.exit(0) - 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 - if 'listunspent_args' not in jm_single().config.options('POLICY'): - jm_single().config.set('POLICY','listunspent_args', '[0]') - - sync_wallet(wallet, fast=options.fastsync) - -if method == 'showutxos': - unsp = {} - if options.showprivkey: - for u, av in wallet.unspent.iteritems(): - addr = av['address'] - key = wallet.get_key_from_addr(addr) - wifkey = btc.wif_compressed_privkey(key, vbyte=get_p2pk_vbyte()) - unsp[u] = {'address': av['address'], - 'value': av['value'], 'privkey': wifkey} - else: - unsp = wallet.unspent - print(json.dumps(unsp, indent=4)) - sys.exit(0) - -if method == 'display' or method == 'displayall' or method == 'summary': - - def cus_print(s): - if method != 'summary': - print(s) - - total_balance = 0 - for m in range(wallet.max_mix_depth): - cus_print('mixing depth %d m/0/%d/' % (m, m)) - balance_depth = 0 - for forchange in [0, 1]: - if forchange == 0: - xpub_key = btc.bip32_privtopub(wallet.keys[m][forchange]) - else: - xpub_key = '' - cus_print(' ' + ('external' if forchange == 0 else 'internal') + - ' addresses m/0/%d/%d' % (m, forchange) + ' ' + xpub_key) - - for k in range(wallet.index[m][forchange] + options.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 options.showprivkey: - privkey = btc.wif_compressed_privkey( - wallet.get_key(m, forchange, k), get_p2pk_vbyte()) - else: - privkey = '' - if (method == 'displayall' or balance > 0 or - (used == ' new' and forchange == 0)): - cus_print(' m/0/%d/%d/%03d %-35s%s %.8f btc %s' % - (m, forchange, k, addr, used, balance / 1e8, - privkey)) - if m in wallet.imported_privkeys: - cus_print(' import addresses') - for privkey in wallet.imported_privkeys[m]: - addr = btc.privtoaddr(privkey, magicbyte=get_p2pk_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') - balance_depth += balance - if options.showprivkey: - wip_privkey = btc.wif_compressed_privkey( - privkey, get_p2pk_vbyte()) - else: - wip_privkey = '' - cus_print(' ' * 13 + '%-35s%s %.8f btc %s' % ( - addr, used, balance / 1e8, wip_privkey)) - total_balance += balance_depth - print('for mixdepth=%d balance=%.8fbtc' % (m, balance_depth / 1e8)) - print('total balance = %.8fbtc' % (total_balance / 1e8)) -elif method == 'generate' or method == 'recover': - 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.') - sys.exit(0) - seed = mn_decode(words) - print(seed) - password = getpass.getpass('Enter wallet encryption passphrase: ') - password2 = getpass.getpass('Reenter wallet encryption passphrase: ') - if password != password2: - print('ERROR. Passwords did not match') - sys.exit(0) - password_key = btc.bin_dbl_sha256(password) - encrypted_seed = encryptData(password_key, seed.decode('hex')) - 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): ') - if len(walletname) == 0: - walletname = 'wallet.json' - walletpath = os.path.join('wallets', walletname) - # Does a wallet with the same name exist? - if os.path.isfile(walletpath): - print('ERROR: ' + walletpath + ' already exists. Aborting.') - sys.exit(0) - else: - fd = open(walletpath, 'w') - fd.write(walletfile) - fd.close() - print('saved to ' + walletname) -elif method == 'showseed': - hexseed = wallet.seed - print('hexseed = ' + hexseed) - words = mn_encode(hexseed) - print('Wallet recovery seed\n\n' + ' '.join(words) + '\n') -elif method == 'importprivkey': - print('WARNING: This imported key will not be recoverable with your 12 ' + - 'word mnemonic seed. 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() - # TODO read also one key for each line - for privkey 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': options.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') -elif method == 'dumpprivkey': - if options.hd_path.startswith('m/0/'): - m, forchange, k = [int(y) for y in options.hd_path[4:].split('/')] - key = wallet.get_key(m, forchange, k) - wifkey = btc.wif_compressed_privkey(key, vbyte=get_p2pk_vbyte()) - print(wifkey) - else: - print('%s is not a valid hd wallet path' % options.hd_path) -elif method == 'listwallets': - # Fetch list of wallets - possible_wallets = [] - for (dirpath, dirnames, filenames) in os.walk('wallets'): - possible_wallets.extend(filenames) - # Breaking as we only want the top dir, not subdirs - break - # For each possible wallet file, read json to list - walletjsons = [] - for possible_wallet in possible_wallets: - fd = open(os.path.join('wallets', possible_wallet), 'r') - try: - walletfile = fd.read() - walletjson = json.loads(walletfile) - # Add filename to json format - walletjson['filename'] = possible_wallet - walletjsons.append(walletjson) - except ValueError: - pass - # Sort wallets by date - walletjsons.sort(key=lambda r: r['creation_time']) - i = 1 - print(' ') - for walletjson in walletjsons: - print('Wallet #' + str(i) + ' (' + walletjson['filename'] + '):') - print('Creation time:\t' + walletjson['creation_time']) - print('Creator:\t' + walletjson['creator']) - print('Network:\t' + walletjson['network']) - print(' ') - i += 1 - print(str(i - 1) + ' Wallets have been found.') -elif method == 'signmessage': - message = args[2] - if options.hd_path.startswith('m/0/'): - m, forchange, k = [int(y) for y in options.hd_path[4:].split('/')] - key = wallet.get_key(m, forchange, k) - addr = btc.privkey_to_address(key, magicbyte=get_p2pk_vbyte()) - print('Using address: ' + addr) - else: - print('%s is not a valid hd wallet path' % options.hd_path) - sig = btc.ecdsa_sign(message, key, formsg=True) - print("Signature: " + str(sig)) - print("To verify this in Bitcoin Core use the RPC command 'verifymessage'") -elif method == 'history': - #sort txes in a db because python can be really bad with large lists - con = sqlite3.connect(":memory:") - con.row_factory = sqlite3.Row - tx_db = con.cursor() - tx_db.execute("CREATE TABLE transactions(txid TEXT, " - "blockhash TEXT, blocktime INTEGER);") - jm_single().debug_silence[0] = True - wallet_name = jm_single().bc_interface.get_wallet_name(wallet) - for wn in [wallet_name, ""]: - buf = range(1000) - t = 0 - while len(buf) == 1000: - buf = jm_single().bc_interface.rpc('listtransactions', [wn, - 1000, t, True]) - t += len(buf) - tx_data = ((tx['txid'], tx['blockhash'], tx['blocktime']) for tx - in buf if 'txid' in tx and 'blockhash' in tx and 'blocktime' - 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()) - - def s(): - return ',' if options.csv else ' ' - def sat_to_str(sat): - return '%.8f'%(sat/1e8) - def sat_to_str_p(sat): - return '%+.8f'%(sat/1e8) - def skip_n1(v): - return '% 2s'%(str(v)) if v != -1 else ' #' - def skip_n1_btc(v): - return sat_to_str(v) if v != -1 else '#' + ' '*10 - - field_names = ['tx#', 'timestamp', 'type', 'amount/btc', - 'balance-change/btc', 'balance/btc', 'coinjoin-n', 'total-fees', - 'utxo-count', 'mixdepth-from', 'mixdepth-to'] - if options.csv: - field_names += ['txid'] - l = s().join(field_names) - print(l) - balance = 0 - utxo_count = 0 - deposits = [] - deposit_times = [] - for i, tx in enumerate(txes): - 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_p2pk_vbyte()), sv['value']) for sv in txd['outs'])) - our_output_addrs = wallet_addr_set.intersection( - output_addr_values.keys()) - - from collections import Counter - value_freq_list = sorted(Counter(output_addr_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]) - is_coinjoin = (value_freq_list[0][1] > 1 and value_freq_list[0][1] in - [non_cj_freq, non_cj_freq+1]) - cj_amount = value_freq_list[0][0] - cj_n = value_freq_list[0][1] - - rpc_inputs = [] - for ins in txd['ins']: - try: - wallet_tx = jm_single().bc_interface.rpc('gettransaction', - [ins['outpoint']['hash']]) - except JsonRpcError: - continue - input_dict = btc.deserialize(str(wallet_tx['hex']))['outs'][ins[ - 'outpoint']['index']] - rpc_inputs.append(input_dict) - - rpc_input_addrs = set((btc.script_to_address(ind['script'], - get_p2pk_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_p2pk_vbyte()) in - our_input_addrs] - our_input_value = sum(our_input_values) - utxos_consumed = len(our_input_values) - - tx_type = None - amount = 0 - delta_balance = 0 - fees = -1 - mixdepth_src = -1 - 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: - #payment to us - amount = sum([output_addr_values[a] for a in our_output_addrs]) - tx_type = 'deposit ' - cj_n = -1 - delta_balance = amount - mixdepth_dst = tuple(wallet_addr_cache[a][0] for a in - our_output_addrs) - if len(mixdepth_dst) == 1: - mixdepth_dst = mixdepth_dst[0] - elif len(our_input_addrs) > 0 and len(our_output_addrs) == 0: - #we swept coins elsewhere - if is_coinjoin: - tx_type = 'cj sweepout' - amount = cj_amount - fees = our_input_value - cj_amount - else: - tx_type = 'sweep out ' - amount = sum([v for v in output_addr_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: - #payment out somewhere with our change address getting the remaining - change_value = output_addr_values[list(our_output_addrs)[0]] - if is_coinjoin: - tx_type = 'cj withdraw' - amount = cj_amount - else: - tx_type = 'withdraw' - #TODO does tx_fee go here? not my_tx_fee only? - amount = our_input_value - change_value - 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: - #payment to self - out_value = sum([output_addr_values[a] for a in our_output_addrs]) - 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] - else: - tx_type = 'unknown type' - balance += delta_balance - utxo_count += (len(our_output_addrs) - utxos_consumed) - index = '% 4d'%(i) - timestamp = datetime.datetime.fromtimestamp(rpctx['blocktime'] - ).strftime("%Y-%m-%d %H:%M") - utxo_count_str = '% 3d' % (utxo_count) - printable_data = [index, timestamp, tx_type, sat_to_str(amount), - sat_to_str_p(delta_balance), sat_to_str(balance), skip_n1(cj_n), - skip_n1_btc(fees), utxo_count_str, skip_n1(mixdepth_src), - skip_n1(mixdepth_dst)] - if options.csv: - printable_data += [tx['txid']] - l = s().join(map('"{}"'.format, printable_data)) - print(l) - - if tx_type != 'cj internal': - deposits.append(delta_balance) - deposit_times.append(rpctx['blocktime']) - - bestblockhash = jm_single().bc_interface.rpc('getbestblockhash', []) - try: - #works with pruning enabled, but only after v0.12 - now = jm_single().bc_interface.rpc('getblockheader', [bestblockhash] - )['time'] - except JsonRpcError: - now = jm_single().bc_interface.rpc('getblock', [bestblockhash])['time'] - print(' %s best block is %s' % (datetime.datetime.fromtimestamp(now) - .strftime("%Y-%m-%d %H:%M"), bestblockhash)) - try: - #https://gist.github.com/chris-belcher/647da261ce718fc8ca10 - import numpy as np - from scipy.optimize import brentq - deposit_times = np.array(deposit_times) - now -= deposit_times[0] - deposit_times -= deposit_times[0] - deposits = np.array(deposits) - def f(r, deposits, deposit_times, now, final_balance): - return np.sum(np.exp((now - deposit_times) / 60.0 / 60 / 24 / - 365)**r * deposits) - final_balance - r = brentq(f, a=1, b=-1, args=(deposits, deposit_times, now, - balance)) - print('continuously compounded equivalent annual interest rate = ' + - str(r * 100) + ' %') - print('(as if yield generator was a bank account)') - except ImportError: - print('numpy/scipy not installed, unable to calculate effective ' + - 'interest rate') - - total_wallet_balance = sum(wallet.get_balance_by_mixdepth().values()) - if balance != total_wallet_balance: - 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): - print(('BUG ERROR: wallet utxo count (%d) does not match utxo count from ' + - 'history (%s)') % (len(wallet.unspent), utxo_count)) +if __name__ == "__main__": + load_program_config() + #JMCS follows same convention as JM original; wallet is in "wallets" localdir + print(wallet_tool_main("wallets")) \ No newline at end of file