object-oriented tracking wallet code

updates for oo tx code
updates for test/test.py
This commit is contained in:
The MMGen Project 2016-07-12 22:25:53 +03:00
commit 5ca6412b23
13 changed files with 393 additions and 332 deletions

View file

@ -31,6 +31,12 @@ from mmgen.obj import *
pnm = g.proj_name
addrmsgs = {
'too_many_acct_addresses': """
ERROR: More than one address found for account: '%s'.
Your 'wallet.dat' file appears to have been altered by a non-{pnm} program.
Please restore your tracking wallet from a backup or create a new one and
re-import your addresses.
""".strip().format(pnm=pnm),
'addrfile_header': """
# {pnm} address file
#
@ -228,13 +234,14 @@ class AddrInfoList(MMGenObject):
vmsg_r('Getting account data from wallet...')
accts = c.listaccounts(0,True)
data,i = {},0
for acct in accts:
alists = c.getaddressesbyaccount([[k] for k in accts],batch=True)
for acct,addrlist in zip(accts,alists):
ma,comment = parse_mmgen_label(acct)
if ma:
i += 1
addrlist = c.getaddressesbyaccount(acct)
# addrlist = c.getaddressesbyaccount(acct)
if len(addrlist) != 1:
die(2,wmsg['too_many_acct_addresses'] % acct)
die(2,addrmsgs['too_many_acct_addresses'] % acct)
seed_id,idx = ma.split(':')
if seed_id not in data:
data[seed_id] = []
@ -380,7 +387,6 @@ class AddrInfo(MMGenObject):
# Header
out = []
from mmgen.addr import addrmsgs
k = ('addrfile_header','keyfile_header')[status[0]==0]
out.append(addrmsgs[k]+'\n')
if self.checksum:

View file

@ -39,8 +39,7 @@ opts_data = {
'notes': """
COMMANDS:{}
Type '{} usage <command> for usage information on a particular
command
Type '{} help <command> for help on a particular command
""".format(tool.cmd_help,g.prog_name)
}

View file

@ -25,7 +25,7 @@ from decimal import Decimal
from mmgen.common import *
from mmgen.tx import *
from mmgen.term import get_char
from mmgen.tw import *
pnm = g.proj_name
@ -68,12 +68,6 @@ one address with no amount on the command line.
}
wmsg = {
'too_many_acct_addresses': """
ERROR: More than one address found for account: '%s'.
Your 'wallet.dat' file appears to have been altered by a non-{pnm} program.
Please restore your tracking wallet from a backup or create a new one and
re-import your addresses.
""".strip().format(pnm=pnm),
'addr_in_addrfile_only': """
Warning: output address {mmgenaddr} is not in the tracking wallet, which means
its balance will not be tracked. You're strongly advised to import the address
@ -88,10 +82,6 @@ tracking wallet, or supply an address file for it on the command line.
No data for {pnm} address {mmgenaddr} could be found in the tracking wallet.
Please import this address into your tracking wallet or supply an address file
for it on the command line.
""".strip(),
'no_spendable_outputs': """
No spendable outputs found! Import addresses with balances into your
watch-only wallet using '{pnm}-addrimport' and then re-run this program.
""".strip(),
'mixed_inputs': """
NOTE: This transaction uses a mixture of both {pnm} and non-{pnm} inputs, which
@ -110,163 +100,6 @@ was specified.
""".strip(),
}
def format_unspent_outputs_for_printing(out,sort_info,total):
pfs = ' %-4s %-67s %-34s %-14s %-12s %-8s %-6s %s'
pout = [pfs % ('Num','Tx ID,Vout','Address','{pnm} ID'.format(pnm=pnm),
'Amount(BTC)','Conf.','Age(d)', 'Comment')]
for n,i in enumerate(out):
addr = '=' if i['skip'] == 'addr' and 'grouped' in sort_info else i['address']
tx = ' ' * 63 + '=' \
if i['skip'] == 'txid' and 'grouped' in sort_info else str(i['txid'])
s = pfs % (str(n+1)+')', tx+','+str(i['vout']),addr,
i['mmid'],i['amt'].strip(),i['confirmations'],i['days'],i['comment'])
pout.append(s.rstrip())
return \
'Unspent outputs ({} UTC)\nSort order: {}\n\n{}\n\nTotal BTC: {}\n'.format(
make_timestr(), ' '.join(sort_info), '\n'.join(pout), normalize_btc_amt(total)
)
def sort_and_view(unspent):
def s_amt(i): return i['amount']
def s_txid(i): return '%s %03s' % (i['txid'],i['vout'])
def s_addr(i): return i['address']
def s_age(i): return i['confirmations']
def s_mmgen(i):
if i['mmid']:
return '{}:{:>0{w}}'.format(
*i['mmid'].split(':'), w=g.mmgen_idx_max_digits)
else: return 'G' + i['comment']
sort,group,show_days,show_mmaddr,reverse = 'age',False,False,True,True
unspent.sort(key=s_age,reverse=reverse) # Reverse age sort by default
total = sum([i['amount'] for i in unspent])
max_acct_len = max([len(i['mmid']+' '+i['comment']) for i in unspent])
hdr_fmt = 'UNSPENT OUTPUTS (sort order: %s) Total BTC: %s'
options_msg = """
Sort options: [t]xid, [a]mount, a[d]dress, [A]ge, [r]everse, [M]mgen addr
Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
""".strip()
prompt = \
"('q' = quit sorting, 'p' = print to file, 'v' = pager view, 'w' = wide view): "
mmid_w = max(len(i['mmid']) for i in unspent)
from copy import deepcopy
from mmgen.term import get_terminal_size
written_to_file_msg = ''
msg('')
while True:
cols = get_terminal_size()[0]
if cols < g.min_screen_width:
die(2,
'{pnl}-txcreate requires a screen at least {w} characters wide'.format(
pnl=pnm.lower(),w=g.min_screen_width))
addr_w = min(34+((1+max_acct_len) if show_mmaddr else 0),cols-46)
acct_w = min(max_acct_len, max(24,int(addr_w-10)))
btaddr_w = addr_w - acct_w - 1
tx_w = max(11,min(64, cols-addr_w-32))
txdots = ('','...')[tx_w < 64]
fs = ' %-4s %-' + str(tx_w) + 's %-2s %-' + str(addr_w) + 's %-13s %-s'
table_hdr = fs % ('Num','TX id Vout','','Address','Amount (BTC)',
('Conf.','Age(d)')[show_days])
unsp = deepcopy(unspent)
for i in unsp: i['skip'] = ''
if group and (sort == 'address' or sort == 'txid'):
for a,b in [(unsp[i],unsp[i+1]) for i in range(len(unsp)-1)]:
if sort == 'address' and a['address'] == b['address']: b['skip'] = 'addr'
elif sort == 'txid' and a['txid'] == b['txid']: b['skip'] = 'txid'
for i in unsp:
amt = str(normalize_btc_amt(i['amount']))
lfill = 3 - len(amt.split('.')[0]) if '.' in amt else 3 - len(amt)
i['amt'] = ' '*lfill + amt
i['days'] = int(i['confirmations'] * g.mins_per_block / (60*24))
i['age'] = i['days'] if show_days else i['confirmations']
addr_disp = (i['address'],'|' + '.'*33)[i['skip']=='addr']
mmid_disp = (i['mmid'],'.'*len(i['mmid']))[i['skip']=='addr']
if show_mmaddr:
dots = ('','..')[btaddr_w < len(i['address'])]
i['addr'] = '%s%s %s' % (
addr_disp[:btaddr_w-len(dots)],
dots, (
('{:<{w}} '.format(mmid_disp,w=mmid_w) if i['mmid'] else '')
+ i['comment'])[:acct_w]
)
else:
i['addr'] = addr_disp
i['tx'] = ' ' * (tx_w-4) + '|...' if i['skip'] == 'txid' \
else i['txid'][:tx_w-len(txdots)]+txdots
sort_info = ([],['reverse'])[reverse]
sort_info.append(sort if sort else 'unsorted')
if group and (sort == 'address' or sort == 'txid'):
sort_info.append('grouped')
out = [hdr_fmt % (' '.join(sort_info), normalize_btc_amt(total)), table_hdr]
out += [fs % (str(n+1)+')',i['tx'],i['vout'],i['addr'],i['amt'],i['age'])
for n,i in enumerate(unsp)]
msg('\n'.join(out) +'\n\n' + written_to_file_msg + options_msg)
written_to_file_msg = ''
skip_prompt = False
while True:
reply = get_char(prompt, immed_chars='atDdAMrgmeqpvw')
if reply == 'a': unspent.sort(key=s_amt); sort = 'amount'
elif reply == 't': unspent.sort(key=s_txid); sort = 'txid'
elif reply == 'D': show_days = not show_days
elif reply == 'd': unspent.sort(key=s_addr); sort = 'address'
elif reply == 'A': unspent.sort(key=s_age); sort = 'age'
elif reply == 'M':
unspent.sort(key=s_mmgen); sort = 'mmgen'
show_mmaddr = True
elif reply == 'r':
unspent.reverse()
reverse = not reverse
elif reply == 'g': group = not group
elif reply == 'm': show_mmaddr = not show_mmaddr
elif reply == 'e': pass
elif reply == 'q': pass
elif reply == 'p':
d = format_unspent_outputs_for_printing(unsp,sort_info,total)
of = 'listunspent[%s].out' % ','.join(sort_info)
write_data_to_file(of,d,'unspent outputs listing')
written_to_file_msg = "Data written to '%s'\n\n" % of
elif reply == 'v':
do_pager('\n'.join(out))
continue
elif reply == 'w':
data = format_unspent_outputs_for_printing(unsp,sort_info,total)
do_pager(data)
continue
else:
msg('\nInvalid input')
continue
break
msg('\n')
if reply == 'q': break
return tuple(unspent)
def select_outputs(unspent,prompt):
while True:
@ -285,20 +118,7 @@ def select_outputs(unspent,prompt):
return selected
def mmaddr2btcaddr_unspent(unspent,mmaddr):
vmsg_r('Searching for {pnm} address {m} in wallet...'.format(pnm=pnm,m=mmaddr))
m = [u for u in unspent if u['mmid'] == mmaddr]
if len(m) == 0:
vmsg('not found')
return '',''
elif len(m) > 1:
die(2,wmsg['too_many_acct_addresses'] % acct)
else:
vmsg('success (%s)' % m[0].address)
return m[0].address, m[0].comment
def mmaddr2btcaddr(c,mmaddr,ail_w,ail_f):
def mmaddr2baddr(c,mmaddr,ail_w,ail_f):
# assume mmaddr has already been checked
btc_addr = ail_w.mmaddr2btcaddr(mmaddr)
@ -369,7 +189,7 @@ if not opt.info:
if is_btc_addr(a1):
btc_addr = a1
elif is_mmgen_addr(a1):
btc_addr = mmaddr2btcaddr(c,a1,ail_w,ail_f)
btc_addr = mmaddr2baddr(c,a1,ail_w,ail_f)
else:
die(2,"%s: unrecognized subargument in argument '%s'" % (a1,a))
@ -382,7 +202,7 @@ if not opt.info:
if tx.change_addr:
die(2,'ERROR: More than one change address specified: %s, %s' %
(change_addr, a))
tx.change_addr = a if is_btc_addr(a) else mmaddr2btcaddr(c,a,ail_w,ail_f)
tx.change_addr = a if is_btc_addr(a) else mmaddr2baddr(c,a,ail_w,ail_f)
tx.add_output(tx.change_addr,Decimal('0'))
else:
die(2,'%s: unrecognized argument' % a)
@ -395,24 +215,10 @@ if not opt.info:
fee_estimate = get_fee_estimate()
tw = MMGenTrackingWallet()
tw.view_and_sort()
tw.display_total()
if g.bogus_wallet_data: # for debugging purposes only
us = eval(get_data_from_file(g.bogus_wallet_data))
else:
us = c.listunspent()
# write_data_to_file('bogus_unspent.json', repr(us), 'bogus unspent data')
# sys.exit()
if not us:
die(2,wmsg['no_spendable_outputs'])
for o in us:
o['mmid'],o['comment'] = parse_mmgen_label(o['account'])
del o['account']
unspent = sort_and_view(us)
total = sum([i['amount'] for i in unspent])
msg('Total unspent: %s BTC (%s outputs)' % (normalize_btc_amt(total), len(unspent)))
if opt.info: sys.exit()
tx.send_amt = tx.sum_outputs()
@ -420,13 +226,13 @@ tx.send_amt = tx.sum_outputs()
msg('Total amount to spend: %s' % ('Unknown','%s BTC'%tx.send_amt,)[bool(tx.send_amt)])
while True:
sel_nums = select_outputs(unspent,
sel_nums = select_outputs(tw.unspent,
'Enter a range or space-separated list of outputs to spend: ')
msg('Selected output%s: %s' % (
('s','')[len(sel_nums)==1],
' '.join(str(i) for i in sel_nums)
))
sel_unspent = [unspent[i-1] for i in sel_nums]
sel_unspent = [tw.unspent[i-1] for i in sel_nums]
mmaddrs = set([i['mmid'] for i in sel_unspent])

View file

@ -44,9 +44,7 @@ else: opts.usage()
do_license_msg()
tx = MMGenTX()
tx.parse_tx_file(infile,'signed transaction data')
tx = MMGenTX(infile)
c = bitcoin_connection()

View file

@ -26,7 +26,7 @@ from mmgen.seed import SeedSource
pnm = g.proj_name
# Unneeded, as MMGen transactions cannot contain non-MMGen inputs
# -w is unneeded - use bitcoin-cli walletdump instead
# -w, --use-wallet-dat Get keys from a running bitcoind
opts_data = {
'desc': 'Sign Bitcoin transactions generated by {pnl}-txcreate'.format(pnl=pnm.lower()),
@ -147,32 +147,32 @@ def generate_keys_for_mmgen_addrs(mmgen_addrs,infiles,saved_seeds):
d += [('{}:{}'.format(seed_id,e.idx),e.addr,e.wif) for e in ai.addrdata]
return d
# function unneeded, as MMGen transactions cannot contain non-MMGen inputs
def sign_tx_with_bitcoind_wallet(c,tx,tx_num_str,keys):
ok = tx.sign(c,tx_num_str,keys) # returns false on failure
if ok:
return ok
else:
msg('Using keys in wallet.dat as per user request')
prompt = 'Enter passphrase for bitcoind wallet: '
while True:
passwd = get_bitcoind_passphrase(prompt)
ret = c.walletpassphrase(passwd, 9999,ret_on_error=True)
if rpc_error(ret):
if rpc_errmsg(ret,'unencrypted wallet, but walletpassphrase was called'):
msg('Wallet is unencrypted'); break
else:
msg('Passphrase OK'); break
ok = tx.sign(c,tx_num_str,keys)
msg('Locking wallet')
ret = c.walletlock(ret_on_error=True)
if rpc_error(ret):
msg('Failed to lock wallet')
return ok
# # function unneeded - use bitcoin-cli walletdump instead
# def sign_tx_with_bitcoind_wallet(c,tx,tx_num_str,keys):
# ok = tx.sign(c,tx_num_str,keys) # returns false on failure
# if ok:
# return ok
# else:
# msg('Using keys in wallet.dat as per user request')
# prompt = 'Enter passphrase for bitcoind wallet: '
# while True:
# passwd = get_bitcoind_passphrase(prompt)
# ret = c.walletpassphrase(passwd, 9999,ret_on_error=True)
# if rpc_error(ret):
# if rpc_errmsg(ret,'unencrypted wallet, but walletpassphrase was called'):
# msg('Wallet is unencrypted'); break
# else:
# msg('Passphrase OK'); break
#
# ok = tx.sign(c,tx_num_str,keys)
#
# msg('Locking wallet')
# ret = c.walletlock(ret_on_error=True)
# if rpc_error(ret):
# msg('Failed to lock wallet')
#
# return ok
#
def missing_keys_errormsg(addrs):
Msg("""
A key file must be supplied (or use the '--use-wallet-dat' option)
@ -241,7 +241,7 @@ def check_maps_from_seeds(io_map,desc,infiles,saved_seeds,return_keys=False):
al,bl = 'generated seed:','tx file:'
die(3,wmsg['mm2btc_mapping_error'] % (al,a,m[a],bl,b,io_map[b]))
if return_keys:
vmsg('Added %s wif key%s from seeds' % (len(d),suf(ret,'k')))
vmsg('Added %s wif key%s from seeds' % (len(d),suf(d,'k')))
return [e[2] for e in d]
def get_keys_from_keylist(kldata,addrs):
@ -287,8 +287,7 @@ for tx_num,tx_file in enumerate(tx_files,1):
msg('\nTransaction #%s of %s:' % (tx_num,len(tx_files)))
tx_num_str = ' #%s' % tx_num
tx = MMGenTX()
tx.parse_tx_file(tx_file,('transaction data','')[bool(opt.tx_id)])
tx = MMGenTX(tx_file)
if tx.check_signed(c):
die(1,'Transaction is already signed!')
@ -335,6 +334,6 @@ for tx_num,tx_file in enumerate(tx_files,1):
if ok:
tx.add_comment() # edits an existing comment
tx.write_to_file(ask_write_default_yes=True)
tx.write_to_file(ask_write_default_yes=True,add_desc=tx_num_str)
else:
die(3,'failed\nSome keys were missing. Transaction %scould not be signed.' % tx_num_str)

View file

@ -78,8 +78,17 @@ class BitcoinRPCConnection(object):
dmsg('=== rpc.py debug ===')
dmsg(' RPC POST data ==> %s\n' % p)
from decimal import Decimal
class JSONDecEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, Decimal):
return str(obj)
return json.JSONEncoder.default(self, obj)
# pp_msg(json.dumps(p,cls=JSONDecEncoder))
try:
c.request('POST', '/', json.dumps(p), {
c.request('POST', '/', json.dumps(p,cls=JSONDecEncoder), {
'Host': self.host,
'Authorization': 'Basic ' + base64.b64encode(self.auth_str)
})

View file

@ -75,6 +75,7 @@ cmd_data = OrderedDict([
('listaddresses',["addrs [str='']",'minconf [int=1]','showempty [bool=False]','pager [bool=False]','showbtcaddrs [bool=False]']),
('getbalance', ['minconf [int=1]']),
('txview', ['<{} TX file> [str]'.format(pnm),'pager [bool=False]','terse [bool=False]']),
('twview', ["sort [str='age']",'reverse [bool=False]','wide [bool=False]','pager [bool=False]']),
('add_label', ['<{} address> [str]'.format(pnm),'<label> [str]']),
('remove_label', ['<{} address> [str]'.format(pnm)]),
@ -105,6 +106,7 @@ cmd_help = """
spendable/unspendable balances for individual {pnm} wallets
listaddresses - list {pnm} addresses and their balances
txview - show raw/signed {pnm} transaction in human-readable form
twview - view tracking wallet
General utilities:
hexdump - encode data into formatted hexadecimal form (file or stdin)
@ -157,6 +159,10 @@ cmd_help = """
def tool_usage(prog_name, command):
if command in cmd_data:
for line in cmd_help.split('\n'):
if ' ' + command in line:
c,h = line.split('-',1)
Msg('{}: {}'.format(c.strip(),h.strip()))
Msg('USAGE: %s %s %s' % (prog_name, command, ' '.join(cmd_data[command])))
else:
Msg("'%s': no such tool command" % command)
@ -393,7 +399,7 @@ def listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Fa
die(2,'duplicate BTC address ({}) for this MMGen address! ({})'.format(
(d['address'], addrs[key][2])))
else:
addrs[key] = [0,comment,d['address']]
addrs[key] = [Decimal('0'),comment,d['address']]
addrs[key][0] += d['amount']
total += d['amount']
@ -408,7 +414,7 @@ def listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Fa
key = mmaddr.replace(':','_')
if key not in addrs:
if showbtcaddrs: save_a.append([acct])
addrs[key] = [0,comment,'']
addrs[key] = [Decimal('0'),comment,'']
for acct,addr in zip(save_a,c.getaddressesbyaccount(save_a,batch=True)):
if len(addr) != 1:
@ -468,10 +474,16 @@ def getbalance(minconf=1):
def txview(infile,pager=False,terse=False):
c = bitcoin_connection()
tx = MMGenTX()
tx.parse_tx_file(infile,'transaction data')
tx = MMGenTX(infile)
tx.view(pager,pause=False,terse=terse)
def twview(pager=False,reverse=False,wide=False,sort='age'):
from mmgen.tw import MMGenTrackingWallet
tw = MMGenTrackingWallet()
tw.do_sort(sort,reverse=reverse)
out = tw.format(wide=wide)
do_pager(out) if pager else sys.stdout.write(out)
def add_label(mmaddr,label,remove=False):
if not is_mmgen_addr(mmaddr):
die(1,'{a}: not a valid {pnm} address'.format(pnm=pnm,a=mmaddr))

220
mmgen/tw.py Executable file
View file

@ -0,0 +1,220 @@
#!/usr/bin/env python
#
# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
# Copyright (C)2013-2016 Philemon <mmgen-py@yandex.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
tw: Tracking wallet methods for the MMGen suite
"""
from mmgen.common import *
from mmgen.obj import *
from mmgen.tx import parse_mmgen_label,normalize_btc_amt
from mmgen.term import get_char
class MMGenTrackingWallet(MMGenObject):
wmsg = {
'no_spendable_outputs': """
No spendable outputs found! Import addresses with balances into your
watch-only wallet using '{}-addrimport' and then re-run this program.
""".strip().format(g.proj_name)
}
sort_keys = 'address','age','amount','txid','mmaddr'
def s_address(self,i): return i['address']
def s_age(self,i): return 0 - i['confirmations']
def s_amount(self,i): return i['amount']
def s_txid(self,i): return '%s %03s' % (i['txid'],i['vout'])
def s_mmaddr(self,i):
if i['mmid']:
return '{}:{:>0{w}}'.format(
*i['mmid'].split(':'), w=g.mmgen_idx_max_digits)
else: return 'G' + i['comment']
def do_sort(self,key,reverse=None):
if key not in self.sort_keys:
fs = "'{}': invalid sort key. Valid keys: [{}]"
die(2,fs.format(key,' '.join(self.sort_keys)))
if reverse == None: reverse = self.reverse
self.sort = key
self.unspent.sort(key=getattr(self,'s_'+key),reverse=reverse)
def sort_info(self,include_group=True):
ret = ([],['reverse'])[self.reverse]
ret.append(self.sort)
if include_group and self.group and (self.sort in ('address','txid')):
ret.append('grouped')
return ret
def __init__(self):
if g.bogus_wallet_data: # for debugging purposes only
us = eval(get_data_from_file(g.bogus_wallet_data))
else:
us = bitcoin_connection().listunspent()
# write_data_to_file('bogus_unspent.json', repr(us), 'bogus unspent data')
# sys.exit()
if not us: die(2,self.wmsg['no_spendable_outputs'])
for o in us:
o['mmid'],o['comment'] = parse_mmgen_label(o['account'])
del o['account']
o['skip'] = ''
amt = str(normalize_btc_amt(o['amount']))
lfill = 3 - len(amt.split('.')[0]) if '.' in amt else 3 - len(amt)
o['amt_fmt'] = ' '*lfill + amt
o['days'] = int(o['confirmations'] * g.mins_per_block / (60*24))
self.unspent = us
self.fmt_display = ''
self.fmt_print = ''
self.cols = None
self.reverse = False
self.group = False
self.show_days = True
self.show_mmaddr = True
self.do_sort('age')
self.total = sum([i['amount'] for i in self.unspent])
def set_cols(self):
from mmgen.term import get_terminal_size
self.cols = get_terminal_size()[0]
if self.cols < g.min_screen_width:
m = 'A screen at least {} characters wide is required to display the tracking wallet'
die(2,m.format(g.min_screen_width))
def display(self):
msg(self.format_for_display())
def format(self,wide=False):
return self.format_for_printing() if wide else self.format_for_display()
def format_for_display(self):
unspent = self.unspent
total = sum([i['amount'] for i in unspent])
mmid_w = max(len(i['mmid']) for i in unspent)
self.set_cols()
max_acct_len = max([len(i['mmid']+i['comment'])+1 for i in self.unspent])
addr_w = min(34+((1+max_acct_len) if self.show_mmaddr else 0),self.cols-46)
acct_w = min(max_acct_len, max(24,int(addr_w-10)))
btaddr_w = addr_w - acct_w - 1
tx_w = max(11,min(64, self.cols-addr_w-32))
txdots = ('','...')[tx_w < 64]
fs = ' %-4s %-' + str(tx_w) + 's %-2s %-' + str(addr_w) + 's %-13s %-s'
table_hdr = fs % ('Num','TX id Vout','','Address','Amount (BTC)',
('Conf.','Age(d)')[self.show_days])
from copy import deepcopy
unsp = deepcopy(unspent)
for i in unsp: i['skip'] = ''
if self.group and (self.sort in ('address','txid')):
for a,b in [(unsp[i],unsp[i+1]) for i in range(len(unsp)-1)]:
if self.sort == 'address' and a['address'] == b['address']: b['skip'] = 'addr'
elif self.sort == 'txid' and a['txid'] == b['txid']: b['skip'] = 'txid'
for i in unsp:
addr_disp = (i['address'],'|' + '.'*33)[i['skip']=='addr']
mmid_disp = (i['mmid'],'.'*len(i['mmid']))[i['skip']=='addr']
if self.show_mmaddr:
dots = ('','..')[btaddr_w < len(i['address'])]
i['addr'] = '%s%s %s' % (
addr_disp[:btaddr_w-len(dots)],
dots, (
('{:<{w}} '.format(mmid_disp,w=mmid_w) if i['mmid'] else '')
+ i['comment'])[:acct_w]
)
else:
i['addr'] = addr_disp
i['tx'] = ' ' * (tx_w-4) + '|...' if i['skip'] == 'txid' \
else i['txid'][:tx_w-len(txdots)]+txdots
hdr_fmt = 'UNSPENT OUTPUTS (sort order: %s) Total BTC: %s'
out = [hdr_fmt % (' '.join(self.sort_info()), normalize_btc_amt(total)), table_hdr]
out += [fs % (str(n+1)+')',i['tx'],i['vout'],i['addr'],i['amt_fmt'],
i['days'] if self.show_days else i['confirmations'])
for n,i in enumerate(unsp)]
self.fmt_display = '\n'.join(out)
return self.fmt_display
def format_for_printing(self):
total = sum([i['amount'] for i in self.unspent])
fs = ' %-4s %-67s %-34s %-14s %-12s %-8s %-6s %s'
out = [fs % ('Num','Tx ID,Vout','Address','{} ID'.format(g.proj_name),
'Amount(BTC)','Conf.','Age(d)', 'Comment')]
for n,i in enumerate(self.unspent):
addr = '=' if i['skip'] == 'addr' and self.group else i['address']
tx = ' ' * 63 + '=' if i['skip'] == 'txid' and self.group else str(i['txid'])
s = fs % (str(n+1)+')', tx+','+str(i['vout']),addr,
i['mmid'],i['amt_fmt'].strip(),i['confirmations'],i['days'],i['comment'])
out.append(s.rstrip())
fs = 'Unspent outputs ({} UTC)\nSort order: {}\n\n{}\n\nTotal BTC: {}\n'
self.fmt_print = fs.format(
make_timestr(),
' '.join(self.sort_info(include_group=False)),
'\n'.join(out),
normalize_btc_amt(total))
return self.fmt_print
def display_total(self):
fs = '\nTotal unspent: %s BTC (%s outputs)'
msg(fs % (normalize_btc_amt(self.total), len(self.unspent)))
def view_and_sort(self):
from mmgen.term import do_pager
s = """
Sort options: [t]xid, [a]mount, a[d]dress, [A]ge, [r]everse, [M]mgen addr
Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
""".strip()
self.display()
msg(s)
p = "('q' = quit sorting, 'p' = print to file, 'v' = pager view, 'w' = wide view): "
while True:
reply = get_char(p, immed_chars='atDdAMrgmeqpvw')
if reply == 'a': self.do_sort('amount')
elif reply == 't': self.do_sort('txid')
elif reply == 'D': self.show_days = not self.show_days
elif reply == 'd': self.do_sort('address')
elif reply == 'A': self.do_sort('age')
elif reply == 'M': self.do_sort('mmaddr'); self.show_mmaddr = True
elif reply == 'r': self.unspent.reverse(); self.reverse = not self.reverse
elif reply == 'g': self.group = not self.group
elif reply == 'm': self.show_mmaddr = not self.show_mmaddr
elif reply == 'e': msg("\n%s\n%s\n%s" % (self.fmt_display,s,p))
elif reply == 'q': return self.unspent
elif reply == 'p':
of = 'listunspent[%s].out' % ','.join(self.sort_info(include_group=False))
write_data_to_file(of,self.format_for_printing(),'unspent outputs listing')
m = yellow("Data written to '%s'" % of)
msg('\n%s\n\n%s\n\n%s' % (self.fmt_display,m,s))
continue
elif reply == 'v':
do_pager(self.fmt_display)
continue
elif reply == 'w':
do_pager(self.format_for_printing())
continue
else:
msg('\nInvalid input')
continue
self.display()
msg(s)

View file

@ -176,6 +176,9 @@ def mmaddr2btcaddr_addrdata(mmaddr,addr_data,source=''):
from mmgen.obj import *
class MMGenTX(MMGenObject):
ext = g.rawtx_ext
desc = 'transaction'
def __init__(self,filename=None):
self.inputs = []
self.outputs = {}
@ -191,7 +194,10 @@ class MMGenTX(MMGenObject):
self.chksum = ''
self.fmt_data = ''
self.blockcount = 0
self.isSigned = False
if filename:
if get_extension(filename) == g.sigtx_ext:
self.mark_signed()
self.parse_tx_file(filename)
def add_output(self,btcaddr,amt):
self.outputs[btcaddr] = (amt,)
@ -261,17 +267,8 @@ class MMGenTX(MMGenObject):
return sum([i['amount'] for i in self.inputs])
def create_raw(self,c):
inputs,outputs = [],{}
def dec2str(d):
tmp = {}
for k in d:
tmp[k] = str(d[k]) if type(d[k]) is Decimal else d[k]
return tmp
inputs = [dec2str(d) for d in self.inputs]
for k in self.outputs:
outputs[k] = str(self.outputs[k][0])
# mdie(inputs,outputs)
self.hex = c.createrawtransaction(inputs,outputs)
o = dict([(k,v[0]) for k,v in self.outputs.items()])
self.hex = c.createrawtransaction(self.inputs,o)
self.txid = make_chksum_6(unhexlify(self.hex)).upper()
# def make_b2m_map(self,ail_w,ail_f):
@ -327,17 +324,22 @@ class MMGenTX(MMGenObject):
if sig_tx['complete']:
msg('OK')
self.hex = sig_tx['hex']
self.isSigned = True
self.mark_signed()
return True
else:
msg('failed\nBitcoind returned the following errors:')
pp_msg(sig_tx['errors'])
return False
def mark_signed(self):
self.desc = 'signed transaction'
self.ext = g.sigtx_ext
def check_signed(self,c):
d = c.decoderawtransaction(self.hex)
self.isSigned = bool(d['vin'][0]['scriptSig']['hex'])
return self.isSigned
ret = bool(d['vin'][0]['scriptSig']['hex'])
if ret: self.mark_signed()
return ret
def send(self,c,bogus=False):
if bogus:
@ -354,13 +356,12 @@ class MMGenTX(MMGenObject):
ask_write=ask_write,
ask_write_default_yes=ask_write_default_yes)
def write_to_file(self,ask_write=True,ask_write_default_yes=False):
def write_to_file(self,add_desc='',ask_write=True,ask_write_default_yes=False):
if ask_write == False:
ask_write_default_yes=True
self.format()
fn = '%s[%s].%s' % (self.txid,self.send_amt,
(g.rawtx_ext,g.sigtx_ext)[self.isSigned])
write_data_to_file(fn,self.fmt_data,'transaction',
fn = '%s[%s].%s' % (self.txid,self.send_amt,self.ext)
write_data_to_file(fn,self.fmt_data,self.desc+add_desc,
ask_write=ask_write,
ask_write_default_yes=ask_write_default_yes)
@ -453,9 +454,9 @@ class MMGenTX(MMGenObject):
return out
def parse_tx_file(self,infile,desc):
def parse_tx_file(self,infile):
self.parse_tx_data(get_lines_from_file(infile,desc))
self.parse_tx_data(get_lines_from_file(infile,self.desc+' data'))
def parse_tx_data(self,tx_data):

View file

@ -70,6 +70,18 @@ def die(ev,s):
def Die(ev,s):
sys.stdout.write(s+'\n'); sys.exit(ev)
def pp_format(d):
import pprint
return pprint.PrettyPrinter(indent=4).pformat(d)
def pp_die(d):
import pprint
die(1,pprint.PrettyPrinter(indent=4).pformat(d))
def pp_msg(d):
import pprint
msg(pprint.PrettyPrinter(indent=4).pformat(d))
def is_mmgen_wallet_label(s):
if len(s) > g.max_wallet_label_len:
msg('ERROR: wallet label length (%s chars) > maximum allowed (%s chars)' % (len(s),g.max_wallet_label_len))
@ -804,15 +816,3 @@ def bitcoin_connection():
import mmgen.rpc
return mmgen.rpc.BitcoinRPCConnection(
host,port,cfg[user],cfg[passwd],auth_cookie=auth_cookie)
def pp_format(d):
import pprint
return pprint.PrettyPrinter(indent=4).pformat(d)
def pp_die(d):
import pprint
die(1,pprint.PrettyPrinter(indent=4).pformat(d))
def pp_msg(d):
import pprint
msg(pprint.PrettyPrinter(indent=4).pformat(d))

View file

@ -99,18 +99,17 @@ tx.send_amt = Decimal(send_amt)
c = bitcoin_connection()
tx.blockcount = find_block_by_time(c,tx.timestamp)
tx.copy_inputs(inputs)
if tx.check_signed(c):
msg('Transaction is signed')
dec_tx = c.decoderawtransaction(tx.hex)
tx.copy_inputs(inputs)
tx.outputs = dict([(i['scriptPubKey']['addresses'][0],(i['value'],)) for i in dec_tx['vout']])
tx.blockcount = find_block_by_time(c,tx.timestamp)
for k in tx.outputs:
if k in b2m_map:
tx.outputs[k] += b2m_map[k]
#tx.view_with_prompt('View decoded transaction?')
#tx.format()
#print(str(tx))
# do_pager('OLD:\n' + pp_format(inputs) + '\nNEW:\n' + pp_format(tx.inputs))
tx.write_to_file(ask_write=False)

View file

@ -47,6 +47,7 @@ setup(
'mmgen.test',
'mmgen.tool',
'mmgen.tx',
'mmgen.tw',
'mmgen.util',
'mmgen.main',

View file

@ -77,8 +77,8 @@ cfgs = {
pwfile: 'walletgen',
'mmdat': 'walletgen',
'addrs': 'addrgen',
'raw': 'txcreate',
'sig': 'txsign',
'rawtx': 'txcreate',
'sigtx': 'txsign',
'mmwords': 'export_mnemonic',
'mmseed': 'export_seed',
'mmincog': 'export_incog',
@ -96,8 +96,8 @@ cfgs = {
'dep_generators': {
'mmdat': 'walletgen2',
'addrs': 'addrgen2',
'raw': 'txcreate2',
'sig': 'txsign2',
'rawtx': 'txcreate2',
'sigtx': 'txsign2',
'mmwords': 'export_mnemonic2',
},
},
@ -108,8 +108,8 @@ cfgs = {
'dep_generators': {
'mmdat': 'walletgen3',
'addrs': 'addrgen3',
'raw': 'txcreate3',
'sig': 'txsign3'
'rawtx': 'txcreate3',
'sigtx': 'txsign3'
},
},
'4': {
@ -121,8 +121,8 @@ cfgs = {
'mmdat': 'walletgen4',
'mmbrain': 'walletgen4',
'addrs': 'addrgen4',
'raw': 'txcreate4',
'sig': 'txsign4',
'rawtx': 'txcreate4',
'sigtx': 'txsign4',
},
'bw_filename': 'brainwallet.mmbrain',
'bw_params': '192,1',
@ -203,7 +203,7 @@ cfgs = {
'ref_keyaddrfile_chksum': '9F2D D781 1812 8BAD C396 9DEB',
# 'ref_fake_unspent_data':'98831F3A_unspent.json',
'ref_tx_file': 'tx_FFB367[1.234].raw',
'ref_tx_file': 'FFB367[1.234].rawtx',
'ic_wallet': '98831F3A-5482381C-18460FB1[256,1].mmincog',
'ic_wallet_hex': '98831F3A-1630A9F2-870376A9[256,1].mmincox',
@ -256,8 +256,8 @@ cmd_group['main'] = OrderedDict([
['addrgen', (1,'address generation', [[['mmdat',pwfile],1]],1)],
['addrimport', (1,'address import', [[['addrs'],1]],1)],
['txcreate', (1,'transaction creation', [[['addrs'],1]],1)],
['txsign', (1,'transaction signing', [[['mmdat','raw',pwfile],1]],1)],
['txsend', (1,'transaction sending', [[['sig'],1]])],
['txsign', (1,'transaction signing', [[['mmdat','rawtx',pwfile],1]],1)],
['txsend', (1,'transaction sending', [[['sigtx'],1]])],
['export_seed', (1,'seed export to mmseed format', [[['mmdat'],1]])],
['export_mnemonic', (1,'seed export to mmwords format', [[['mmdat'],1]])],
@ -272,23 +272,23 @@ cmd_group['main'] = OrderedDict([
['addrgen_incog_hidden',(1,'address generation from hidden mmincog file', [[[hincog_fn,'addrs'],1]])],
['keyaddrgen', (1,'key-address file generation', [[['mmdat',pwfile],1]])],
['txsign_keyaddr',(1,'transaction signing with key-address file', [[['akeys.mmenc','raw'],1]])],
['txsign_keyaddr',(1,'transaction signing with key-address file', [[['akeys.mmenc','rawtx'],1]])],
['walletgen2',(2,'wallet generation (2), 128-bit seed', [])],
['addrgen2', (2,'address generation (2)', [[['mmdat'],2]])],
['txcreate2', (2,'transaction creation (2)', [[['addrs'],2]])],
['txsign2', (2,'transaction signing, two transactions',[[['mmdat','raw'],1],[['mmdat','raw'],2]])],
['txsign2', (2,'transaction signing, two transactions',[[['mmdat','rawtx'],1],[['mmdat','rawtx'],2]])],
['export_mnemonic2', (2,'seed export to mmwords format (2)',[[['mmdat'],2]])],
['walletgen3',(3,'wallet generation (3)', [])],
['addrgen3', (3,'address generation (3)', [[['mmdat'],3]])],
['txcreate3', (3,'tx creation with inputs and outputs from two wallets', [[['addrs'],1],[['addrs'],3]])],
['txsign3', (3,'tx signing with inputs and outputs from two wallets',[[['mmdat'],1],[['mmdat','raw'],3]])],
['txsign3', (3,'tx signing with inputs and outputs from two wallets',[[['mmdat'],1],[['mmdat','rawtx'],3]])],
['walletgen4',(4,'wallet generation (4) (brainwallet)', [])],
['addrgen4', (4,'address generation (4)', [[['mmdat'],4]])],
['txcreate4', (4,'tx creation with inputs and outputs from four seed sources, plus non-MMGen inputs and outputs', [[['addrs'],1],[['addrs'],2],[['addrs'],3],[['addrs'],4]])],
['txsign4', (4,'tx signing with inputs and outputs from incog file, mnemonic file, wallet and brainwallet, plus non-MMGen inputs and outputs', [[['mmincog'],1],[['mmwords'],2],[['mmdat'],3],[['mmbrain','raw'],4]])],
['txsign4', (4,'tx signing with inputs and outputs from incog file, mnemonic file, wallet and brainwallet, plus non-MMGen inputs and outputs', [[['mmincog'],1],[['mmwords'],2],[['mmdat'],3],[['mmbrain','rawtx'],4]])],
])
cmd_group['tool'] = OrderedDict([
@ -428,6 +428,7 @@ meta_cmds = OrderedDict([
])
del cmd_group
log_file = 'test.py_log'
opts_data = {
# 'sets': [('non_interactive',bool,'verbose',None)],
@ -441,15 +442,17 @@ opts_data = {
debugging only).
-e, --exact-output Show the exact output of the MMGen script(s) being run.
-l, --list-cmds List and describe the commands in the test suite.
-L, --log Log commands to file {lf}
-n, --names Display command names instead of descriptions.
-I, --non-interactive Non-interactive operation (MS Windows mode)
-p, --pause Pause between tests, resuming on keypress.
-q, --quiet Produce minimal output. Suppress dependency info.
-s, --system Test scripts and modules installed on system rather
than those in the repo root.
-S, --skip-deps Skip dependency checking for command
-t, --traceback Run the command inside the '{tb_cmd}' script.
-v, --verbose Produce more verbose output.
""".format(tb_cmd=tb_cmd),
""".format(tb_cmd=tb_cmd,lf=log_file),
'notes': """
If no command is given, the whole suite of tests is run.
@ -457,6 +460,9 @@ If no command is given, the whole suite of tests is run.
}
cmd_args = opts.init(opts_data)
if opt.log:
log_fd = open(log_file,'a')
log_fd.write('\nLog started: %s\n' % make_timestr())
if opt.system: sys.path.pop(0)
ni = bool(opt.non_interactive)
@ -616,11 +622,11 @@ class MMGenExpect(object):
mmgen_cmd = os.path.join(os.curdir,mmgen_cmd)
desc = (cmd_data[name][1],name)[bool(opt.names)]
if extra_desc: desc += ' ' + extra_desc
cmd_str = mmgen_cmd + ' ' + ' '.join(cmd_args)
if opt.log:
log_fd.write(cmd_str+'\n')
if opt.verbose or opt.exact_output:
sys.stderr.write(
green('Testing: %s\nExecuting ' % desc) +
cyan("'%s %s'\n" % (mmgen_cmd,' '.join(cmd_args)))
)
sys.stderr.write(green('Testing: %s\nExecuting %s\n' % (desc,cyan(cmd_str))))
else:
m = 'Testing %s: ' % desc
msg_r((m,yellow(m))[ni])
@ -845,7 +851,8 @@ def check_needs_rerun(
if rerun:
for fn in fns:
if not root: os.unlink(fn)
ts.do_cmd(cmd)
if not (dpy and opt.skip_deps):
ts.do_cmd(cmd)
if not root: do_between()
else:
# If prog produces multiple files:
@ -888,6 +895,7 @@ def check_deps(cmds):
def clean(usr_dirs=[]):
if opt.skip_deps: return
all_dirs = MMGenTestSuite().list_tmp_dirs()
dirs = (usr_dirs or all_dirs)
for d in sorted(dirs):
@ -1149,14 +1157,15 @@ class MMGenTestSuite(object):
t.written_to_file('Transaction')
ok()
def txsign_end(self,t,tnum=None):
def txsign_end(self,t,tnum=None,has_comment=False):
t.expect('Signing transaction')
t.expect('Edit transaction comment? (y/N): ','\n')
t.expect('Save signed transaction? (Y/n): ','y')
cprompt = ('Add a comment to transaction','Edit transaction comment')[has_comment]
t.expect('%s? (y/N): ' % cprompt,'\n')
t.expect('Save signed transaction.*?\? \(Y/n\): ','y',regex=True)
add = ' #' + tnum if tnum else ''
t.written_to_file('Signed transaction' + add, oo=True)
def txsign(self,name,txfile,wf,pf='',save=True):
def txsign(self,name,txfile,wf,pf='',save=True,has_comment=False):
add_args = ([],['-q','-P',pf])[ni]
if ni:
m = '\nAnswer the interactive prompts as follows:\n ENTER, ENTER, ENTER'
@ -1167,9 +1176,10 @@ class MMGenTestSuite(object):
t.tx_view()
t.passphrase('MMGen wallet',cfg['wpasswd'])
if save:
self.txsign_end(t)
self.txsign_end(t,has_comment=has_comment)
else:
t.expect('Edit transaction comment? (y/N): ','\n')
cprompt = ('Add a comment to transaction','Edit transaction comment')[has_comment]
t.expect('%s? (y/N): ' % cprompt,'\n')
t.close()
ok()
@ -1177,7 +1187,7 @@ class MMGenTestSuite(object):
t = MMGenExpect(name,'mmgen-txsend', ['-d',cfg['tmpdir'],sigfile])
t.license()
t.tx_view()
t.expect('Edit transaction comment? (y/N): ','\n')
t.expect('Add a comment to transaction? (y/N): ','\n')
t.expect('broadcast this transaction to the network?')
t.expect("'YES, I REALLY WANT TO DO THIS' to confirm: ",'\n')
t.expect('Exiting at user request')
@ -1601,7 +1611,7 @@ class MMGenTestSuite(object):
wf = os.path.join(ref_dir,cfg['ref_wallet'])
write_to_tmpfile(cfg,pwfile,cfg['wpasswd'])
pf = get_tmpfile_fn(cfg,pwfile)
self.txsign(name,tf,wf,pf,save=False)
self.txsign(name,tf,wf,pf,save=False,has_comment=True)
def ref_tool_decrypt(self,name):
f = os.path.join(ref_dir,ref_enc_fn)
@ -1760,21 +1770,22 @@ ts = MMGenTestSuite()
# Laggy flash media cause pexpect to crash, so read and write all temporary
# files to volatile memory in '/dev/shm'
if sys.platform[:3] == 'win':
for cfg in sorted(cfgs): mk_tmpdir(cfgs[cfg])
else:
d,pfx = '/dev/shm','mmgen-test-'
try:
import subprocess
subprocess.call('rm -rf %s/%s*'%(d,pfx),shell=True)
except Exception as e:
die(2,'Unable to delete directory tree %s/%s* (%s)'%(d,pfx,e))
try:
import tempfile
shm_dir = tempfile.mkdtemp('',pfx,d)
except Exception as e:
die(2,'Unable to create temporary directory in %s (%s)'%(d,e))
for cfg in sorted(cfgs): mk_tmpdir_path(shm_dir,cfgs[cfg])
if not opt.skip_deps:
if sys.platform[:3] == 'win':
for cfg in sorted(cfgs): mk_tmpdir(cfgs[cfg])
else:
d,pfx = '/dev/shm','mmgen-test-'
try:
import subprocess
subprocess.call('rm -rf %s/%s*'%(d,pfx),shell=True)
except Exception as e:
die(2,'Unable to delete directory tree %s/%s* (%s)'%(d,pfx,e))
try:
import tempfile
shm_dir = tempfile.mkdtemp('',pfx,d)
except Exception as e:
die(2,'Unable to create temporary directory in %s (%s)'%(d,e))
for cfg in sorted(cfgs): mk_tmpdir_path(shm_dir,cfgs[cfg])
try:
if cmd_args: