Browse Source

refactor wallet-tool for reusability

master
Adam Gibson 9 years ago
parent
commit
0f0d965c97
No known key found for this signature in database
GPG Key ID: B3AE09F1E9A3197A
  1. 1
      jmclient/jmclient/__init__.py
  2. 531
      jmclient/jmclient/wallet_utils.py
  3. 536
      scripts/wallet-tool.py

1
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:

531
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())

536
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"))
Loading…
Cancel
Save