@ -3,13 +3,14 @@ import json
import os
import pprint
import sys
import sqlite3
import datetime
import binascii
from mnemonic import Mnemonic
from optparse import OptionParser
import getpass
from jmclient import ( get_network , Wallet , Bip39Wallet , podle ,
encryptData , get_p2pk _vbyte , jm_single ,
encryptData , get_p2sh _vbyte , jm_single ,
mn_decode , mn_encode , BitcoinCoreInterface ,
JsonRpcError , sync_wallet , WalletError , SegwitWallet )
from jmbase . support import get_password
@ -17,19 +18,20 @@ import jmclient.btc as btc
def get_wallettool_parser ( ) :
description = (
' Use this script to monitor and manage your Joinmarket wallet. The '
' method is one of the following: \n (display) Shows addresses and '
' balances. \n (displayall) Shows ALL addresses and balances. '
' \n (summary) Shows a summary of mixing depth balances. \n (generate) '
' Generates a new wallet. \n (recover) Recovers a wallet from the 12 '
' word recovery seed. \n (showutxos) Shows all utxos in the wallet. '
' \n (showseed) Shows the wallet recovery seed '
' and hex seed. \n (importprivkey) Adds privkeys to this wallet, '
' privkeys are spaces or commas separated. \n (dumpprivkey) Export '
' a single private key, specify an hd wallet path \n '
' (signmessage) Sign a message with the private key from an address '
' in the wallet. Use with -H and specify an HD wallet '
' path for the address. ' )
' Use this script to monitor and manage your Joinmarket wallet. \n '
' The method is one of the following: \n '
' (display) Shows addresses and balances. \n '
' (displayall) Shows ALL addresses and balances. \n '
' (summary) Shows a summary of mixing depth balances. \n '
' (generate) Generates a new wallet. \n '
' (history) Show all historical transaction details. Requires Bitcoin Core. '
' (recover) Recovers a wallet from the 12 word recovery seed. \n '
' (showutxos) Shows all utxos in the wallet. \n '
' (showseed) Shows the wallet recovery seed and hex seed. \n '
' (importprivkey) Adds privkeys to this wallet, privkeys are spaces or commas separated. \n '
' (dumpprivkey) Export a single private key, specify an hd wallet path \n '
' (signmessage) Sign a message with the private key from an address in \n '
' the wallet. Use with -H and specify an HD wallet path for the address. ' )
parser = OptionParser ( usage = ' usage: % prog [options] [wallet file] [method] ' ,
description = description )
parser . add_option ( ' -p ' ,
@ -75,7 +77,7 @@ def get_wallettool_parser():
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 ,
@ -260,7 +262,7 @@ 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 ( ) )
addr = btc . privtoaddr ( privkey , magicbyte = get_p2sh _vbyte ( ) )
balance = 0.0
for addrvalue in wallet . unspent . values ( ) :
if addr == addrvalue [ ' address ' ] :
@ -268,7 +270,7 @@ def get_imported_privkey_branch(wallet, m, showprivkey):
used = ( ' used ' if balance > 0.0 else ' empty ' )
if showprivkey :
wip_privkey = btc . wif_compressed_privkey (
privkey , get_p2pk _vbyte ( ) )
privkey , get_p2sh _vbyte ( ) )
else :
wip_privkey = ' '
entries . append ( WalletViewEntry ( " m/0 " , m , - 1 ,
@ -288,7 +290,7 @@ def wallet_showutxos(wallet, showprivkey):
' tries ' : tries , ' tries_remaining ' : tries_remaining ,
' external ' : False }
if showprivkey :
wifkey = btc . wif_compressed_privkey ( key , vbyte = get_p2pk _vbyte ( ) )
wifkey = btc . wif_compressed_privkey ( key , vbyte = get_p2sh _vbyte ( ) )
unsp [ u ] [ ' privkey ' ] = wifkey
used_commitments , external_commitments = podle . get_podle_commitments ( )
@ -327,7 +329,7 @@ def wallet_display(wallet, gaplimit, showprivkey, displayall=False,
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 ( ) )
wallet . get_key ( m , forchange , k ) , get_p2sh _vbyte ( ) )
else :
privkey = ' '
if ( displayall or balance > 0 or
@ -443,6 +445,210 @@ def wallet_generate_recover(method, walletspath,
encrypted_seed = encryptData ( password_key , seed . decode ( ' hex ' ) )
return persist_walletfile ( walletspath , default_wallet_name , encrypted_seed )
def wallet_fetch_history ( wallet , options ) :
# 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_p2sh_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_p2sh_vbyte ( ) ) for ind in rpc_inputs ) )
our_input_addrs = wallet_addr_set . intersection ( rpc_input_addrs )
our_input_values = [ ind [ ' value ' ] for ind in rpc_inputs if btc .
script_to_address ( ind [ ' script ' ] , get_p2sh_vbyte ( ) ) in
our_input_addrs ]
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 ) )
print ( ' total profit = ' + str ( float ( balance - sum ( deposits ) ) / float ( 100000000 ) ) + ' BTC ' )
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 ) )
def wallet_showseed ( wallet ) :
if isinstance ( wallet , Bip39Wallet ) :
if not wallet . entropy :
@ -467,7 +673,7 @@ def wallet_importprivkey(wallet, mixdepth):
# 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 ]
vbyte = get_p2sh _vbyte ( ) ) . decode ( ' hex ' ) [ : - 1 ]
encrypted_privkey = encryptData ( wallet . password_key , privkey_bin )
if ' imported_keys ' not in wallet . walletdata :
wallet . walletdata [ ' imported_keys ' ] = [ ]
@ -486,7 +692,7 @@ def wallet_dumpprivkey(wallet, hdpath):
if pathlist and len ( pathlist ) == 5 :
cointype , purpose , m , forchange , k = pathlist
key = wallet . get_key ( m , forchange , k )
wifkey = btc . wif_compressed_privkey ( key , vbyte = get_p2pk _vbyte ( ) )
wifkey = btc . wif_compressed_privkey ( key , vbyte = get_p2sh _vbyte ( ) )
return wifkey
else :
return hdpath + " is not a valid hd wallet path "
@ -495,7 +701,7 @@ def wallet_signmessage(wallet, hdpath, message):
if hdpath . startswith ( wallet . get_root_path ( ) ) :
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 ( ) )
addr = btc . privkey_to_address ( key , magicbyte = get_p2sh _vbyte ( ) )
print ( ' Using address: ' + addr )
else :
print ( ' %s is not a valid hd wallet path ' % hdpath )
@ -521,7 +727,7 @@ def wallet_tool_main(wallet_root_path):
noseed_methods = [ ' generate ' , ' recover ' ]
methods = [ ' display ' , ' displayall ' , ' summary ' , ' showseed ' , ' importprivkey ' ,
' showutxos ' ]
' history ' , ' showutxos' ]
methods . extend ( noseed_methods )
noscan_methods = [ ' showseed ' , ' importprivkey ' , ' dumpprivkey ' , ' signmessage ' ]
@ -568,6 +774,13 @@ def wallet_tool_main(wallet_root_path):
elif method == " displayall " :
return wallet_display ( wallet , options . gaplimit , options . showprivkey ,
displayall = True )
elif method == " history " :
if not isinstance ( jm_single ( ) . bc_interface , BitcoinCoreInterface ) :
print ( ' showing history only available when using the Bitcoin Core ' +
' blockchain interface ' )
sys . exit ( 0 )
else :
return wallet_fetch_history ( wallet , options )
elif method == " generate " :
retval = wallet_generate_recover ( " generate " , wallet_root_path )
return retval if retval else " Failed "
@ -610,4 +823,4 @@ if __name__ == "__main__":
wallet = WalletView ( rootpath + " / " + str ( walletbranch ) ,
accounts = acctlist )
print ( wallet . serialize ( ) )