Browse Source

adopt wallet_utils for new wallet

master
undeath 8 years ago
parent
commit
914a40e3a1
  1. 2
      jmclient/jmclient/__init__.py
  2. 457
      jmclient/jmclient/wallet_utils.py

2
jmclient/jmclient/__init__.py

@ -46,7 +46,7 @@ from .taker_utils import (tumbler_taker_finished_update, restart_waiter,
from .wallet_utils import ( from .wallet_utils import (
wallet_tool_main, wallet_generate_recover_bip39, open_wallet, wallet_tool_main, wallet_generate_recover_bip39, open_wallet,
open_test_wallet_maybe, create_wallet, get_wallet_cls, get_wallet_path, open_test_wallet_maybe, create_wallet, get_wallet_cls, get_wallet_path,
wallet_display, SewgitTestWallet) wallet_display)
from .maker import Maker from .maker import Maker
from .yieldgenerator import YieldGenerator, YieldGeneratorBasic, ygmain from .yieldgenerator import YieldGenerator, YieldGeneratorBasic, ygmain
# Set default logging handler to avoid "No handler found" warnings. # Set default logging handler to avoid "No handler found" warnings.

457
jmclient/jmclient/wallet_utils.py

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

Loading…
Cancel
Save