new feature: transaction history via mmgen-tool txhist

Display nicely formatted info about all transactions known to the tracking
wallet.

Interactive mode presents the user with an interface similar to `mmgen-tool
twview` or `mmgen-txcreate -i`, providing various sort, filter, column format
and printing options.

`--coin=ltc` and `--coin=bch` are also supported.  Use of `--rpc-backend=aio`
speeds up operation significantly under Linux.

Usage examples:

    # Non-interactive mode, tabular output:
    $ mmgen-tool txhist

    # Non-interactive mode, full output:
    $ mmgen-tool txhist detail=1

    # Show only transactions newer than 100000 blocks from chain tip:
    $ mmgen-tool txhist sinceblock=-100000

    # Interactive mode:
    $ mmgen-tool txhist interactive=1

Testing/demo:

    $ test/test.py -n -X bob_txhist1 regtest
    $ test/test.py -Se regtest:bob_txhist1
    $ test/test.py -Se regtest:bob_txhist2
    $ test/test.py -Se regtest:bob_txhist3
    $ test/test.py -Se regtest:bob_txhist4
    $ test/test.py -Se regtest:bob_txhist_interactive
This commit is contained in:
The MMGen Project 2022-05-23 16:28:57 +00:00
commit 7d2165641f
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
9 changed files with 681 additions and 6 deletions

View file

@ -0,0 +1,366 @@
#!/usr/bin/env python3
#
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
# Licensed under the GNU General Public License, Version 3:
# https://www.gnu.org/licenses
# Public project repositories:
# https://github.com/mmgen/mmgen
# https://gitlab.com/mmgen/mmgen
"""
base_proto.bitcoin.tw.txhistory: Bitcoin base protocol tracking wallet transaction history class
"""
from collections import namedtuple
from ....globalvars import g
from ....tw.txhistory import TwTxHistory
from ....tw.common import get_tw_label,TwMMGenID
from ....addr import CoinAddr
from ....util import msg,msg_r,remove_dups
from ....color import nocolor,red,pink,gray
from ....obj import TwComment,CoinTxID,Int
from .common import BitcoinTwCommon
class BitcoinTwTransaction(BitcoinTwCommon):
def __init__(self,parent,proto,rpc,
idx, # unique numeric identifier of this transaction in listing
unspent_info, # addrs in wallet with balances: { 'mmid': {'addr','label','amt'} }
mm_map, # all addrs in wallet: ['addr', ['twmmid','label']]
tx, # the decoded transaction data
wallet_vouts, # list of ints - wallet-related vouts
prevouts, # list of (txid,vout) pairs
prevout_txs # decoded transaction data for prevouts
):
self.parent = parent
self.proto = proto
self.rpc = rpc
self.unspent_info = unspent_info
self.tx = tx
def gen_prevouts_data():
_d = namedtuple('prevout_data',['txid','data'])
for tx in prevout_txs:
for e in prevouts:
if e.txid == tx['txid']:
yield _d( e.txid, tx['vout'][e.vout] )
def gen_wallet_vouts_data():
_d = namedtuple('wallet_vout_data',['txid','data'])
txid = self.tx['txid']
vouts = self.tx['decoded']['vout']
for n in wallet_vouts:
yield _d( txid, vouts[n] )
def gen_vouts_info(data):
_d = namedtuple('vout_info',['txid','coin_addr','twlabel','data'])
def gen():
for d in data:
addr = d.data['scriptPubKey'].get('address') or d.data['scriptPubKey']['addresses'][0]
yield _d(
txid = d.txid,
coin_addr = addr,
twlabel = mm_map[addr] if (addr in mm_map and mm_map[addr].twmmid) else None,
data = d.data )
return sorted(
gen(),
key = lambda d: d.twlabel.twmmid.sort_key if d.twlabel else 'zz_' + d.coin_addr )
def gen_all_addrs(src):
for e in self.vouts_info[src]:
if e.twlabel:
mmid = e.twlabel.twmmid
yield (
(mmid if mmid.type == 'mmgen' else mmid.split(':',1)[1]) +
('*' if mmid in self.unspent_info else '')
)
else:
yield e.coin_addr
def total(data):
return self.proto.coin_amt( sum(d.data['value'] for d in data) )
def get_best_label():
"""
find the most relevant label for tabular (squeezed) display
"""
def vouts_labels(src):
return [ d.twlabel.label for d in self.vouts_info[src] if d.twlabel and d.twlabel.label ]
ret = vouts_labels('outputs') or vouts_labels('inputs')
return ret[0] if ret else TwComment('')
# 'outputs' refers to wallet-related outputs only
self.vouts_info = {
'inputs': gen_vouts_info( gen_prevouts_data() ),
'outputs': gen_vouts_info( gen_wallet_vouts_data() )
}
self.max_addrlen = {
'inputs': max(len(addr) for addr in gen_all_addrs('inputs')),
'outputs': max(len(addr) for addr in gen_all_addrs('outputs'))
}
self.inputs_total = total( self.vouts_info['inputs'] )
self.outputs_total = self.proto.coin_amt( sum(i['value'] for i in self.tx['decoded']['vout']) )
self.wallet_outputs_total = total( self.vouts_info['outputs'] )
self.fee = self.inputs_total - self.outputs_total
self.nOutputs = len(self.tx['decoded']['vout'])
self.confirmations = self.tx['confirmations']
self.label = get_best_label()
self.vsize = self.tx['decoded'].get('vsize') or self.tx['decoded']['size']
self.txid = CoinTxID(self.tx['txid'])
self.time = self.tx['time']
def blockheight_disp(self,color):
return (
# old/altcoin daemons return no 'blockheight' field, so use confirmations instead
Int( self.rpc.blockcount + 1 - self.confirmations ).hl(color=color)
if self.confirmations > 0 else None )
def age_disp(self,age_fmt,width,color):
if age_fmt == 'confs':
ret_str = str(self.confirmations).rjust(width)
return gray(ret_str) if self.confirmations < 0 and color else ret_str
elif age_fmt == 'block':
ret = (self.rpc.blockcount - (abs(self.confirmations) - 1)) * (-1 if self.confirmations < 0 else 1)
ret_str = str(ret).rjust(width)
return gray(ret_str) if ret < 0 and color else ret_str
else:
return self.parent.date_formatter[age_fmt](self.rpc,self.tx.get('blocktime',0))
def txdate_disp(self,age_fmt):
return self.parent.date_formatter[age_fmt](self.rpc,self.time)
def txid_disp(self,width,color):
return self.txid.truncate(width=width,color=color)
def vouts_list_disp(self,src,color,indent=''):
fs1,fs2 = {
'inputs': ('{i},{n} {a} {A}', '{i},{n} {a} {A} {l}'),
'outputs': ( '{n} {a} {A}', '{n} {a} {A} {l}')
}[src]
def gen_output():
for e in self.vouts_info[src]:
mmid = e.twlabel.twmmid if e.twlabel else None
if not mmid:
yield fs1.format(
i = CoinTxID(e.txid).hl(color=color),
n = (nocolor,red)[color](str(e.data['n']).ljust(3)),
a = CoinAddr(self.proto,e.coin_addr).fmt( width=self.max_addrlen[src], color=color ),
A = self.proto.coin_amt( e.data['value'] ).fmt(color=color)
).rstrip()
else:
bal_star,co = ('*','melon') if mmid in self.unspent_info else ('','brown')
addr_out = mmid if mmid.type == 'mmgen' else mmid.split(':',1)[1]
yield fs2.format(
i = CoinTxID(e.txid).hl(color=color),
n = (nocolor,red)[color](str(e.data['n']).ljust(3)),
a = TwMMGenID.hlc(
'{:{w}}'.format( addr_out + bal_star, w=self.max_addrlen[src] ),
color = color,
color_override = co ),
A = self.proto.coin_amt( e.data['value'] ).fmt(color=color),
l = e.twlabel.label.hl(color=color)
).rstrip()
return f'\n{indent}'.join( gen_output() ).strip()
def vouts_disp(self,src,width,color):
class x: space_left = width or 0
def gen_output():
for e in self.vouts_info[src]:
mmid = e.twlabel.twmmid if e.twlabel else None
bal_star,addr_w,co = ('*',16,'melon') if mmid in self.unspent_info else ('',15,'brown')
if not mmid:
if width and x.space_left < addr_w:
break
yield CoinAddr( self.proto, e.coin_addr ).fmt(width=addr_w,color=color)
x.space_left -= addr_w
elif mmid.type == 'mmgen':
mmid_disp = mmid + bal_star
if width and x.space_left < len(mmid_disp):
break
yield TwMMGenID.hlc( mmid_disp, color=color, color_override=co )
x.space_left -= len(mmid_disp)
else:
if width and x.space_left < addr_w:
break
yield TwMMGenID.hlc(
CoinAddr.fmtc( mmid.split(':',1)[1] + bal_star, width=addr_w ),
color = color,
color_override = co )
x.space_left -= addr_w
x.space_left -= 1
return ' '.join(gen_output()) + ' ' * (x.space_left + 1 if width else 0)
def amt_disp(self,show_total_amt):
return (
self.outputs_total if show_total_amt else
self.wallet_outputs_total )
def fee_disp(self,color):
atomic_unit = self.proto.coin_amt.units[0]
return '{} {}'.format(
self.fee.hl(color=color),
(nocolor,pink)[color]('({:,} {}s/byte)'.format(
self.fee.to_unit(atomic_unit) // self.vsize,
atomic_unit )) )
class BitcoinTwTxHistory(TwTxHistory,BitcoinTwCommon):
has_age = True
hdr_fmt = 'TRANSACTION HISTORY (sort order: {a})'
desc = 'transaction history'
item_desc = 'transaction'
no_data_errmsg = 'No transactions in tracking wallet!'
prompt = """
Sort options: [t]xid, [a]mt, total a[m]t, [A]ge, [b]locknum, [r]everse
Column options: toggle [D]ays/date/confs/block, tx[i]d, [T]otal amt
Filter options: show [u]nconfirmed
View/Print: pager [v]iew, full [V]iew, screen [p]rint, full [P]rint
Actions: [q]uit, r[e]draw:
"""
key_mappings = {
'A':'s_age',
'b':'s_blockheight',
'a':'s_amt',
'm':'s_total_amt',
't':'s_txid',
'r':'d_reverse',
'D':'d_days',
'e':'d_redraw',
'u':'d_show_unconfirmed',
'i':'d_show_txid',
'T':'d_show_total_amt',
'q':'a_quit',
'v':'a_view',
'V':'a_view_detail',
'p':'a_print_squeezed',
'P':'a_print_detail' }
squeezed_fs_fs = ' {{n:>{nw}}} {{d:>{dw}}} {txid_fs}{{a1}} {{A}} {{a2}} {{l}}'
squeezed_hdr_fs_fs = ' {{n:>{nw}}} {{d:{dw}}} {txid_fs}{{a1:{aw}}} {{A}} {{a2:{a2w}}} {{l}}'
async def get_rpc_data(self):
blockhash = (
await self.rpc.call( 'getblockhash', self.sinceblock )
if self.sinceblock else '' )
# bitcoin-cli help listsinceblock:
# Arguments:
# 1. blockhash (string, optional) If set, the block hash to list transactions since,
# otherwise list all transactions.
# 2. target_confirmations (numeric, optional, default=1) Return the nth block hash from the main
# chain. e.g. 1 would mean the best block hash. Note: this is not used
# as a filter, but only affects [lastblock] in the return value
# 3. include_watchonly (boolean, optional, default=true for watch-only wallets, otherwise
# false) Include transactions to watch-only addresses (see
# 'importaddress')
# 4. include_removed (boolean, optional, default=true) Show transactions that were removed
# due to a reorg in the "removed" array (not guaranteed to work on
# pruned nodes)
return (await self.rpc.call('listsinceblock',blockhash,1,True,False))['transactions']
async def gen_data(self,rpc_data,lbl_id):
def gen_parsed_data():
for o in rpc_data:
if lbl_id in o:
l = get_tw_label(self.proto,o[lbl_id])
else:
assert o['category'] == 'send', f"{o['address']}: {o['category']} != 'send'"
l = None
o.update({
'twmmid': l.mmid if l else None,
'label': (l.comment or '') if l else None,
})
yield o
data = list(gen_parsed_data())
if g.debug_tw:
import json
from ....rpc import json_encoder
def do_json_dump(*data):
nw = f'{self.proto.coin.lower()}-{self.proto.network}'
for d,fn_stem in data:
open(f'/tmp/{fn_stem}-{nw}.json','w').write(json.dumps(d,cls=json_encoder))
_mmp = namedtuple('mmap_datum',['twmmid','label'])
mm_map = {
i['address']: (
_mmp( TwMMGenID(self.proto,i['twmmid']), TwComment(i['label']) )
if i['twmmid'] else _mmp(None,None)
)
for i in data }
if self.sinceblock: # mapping data may be incomplete for inputs, so update from 'listlabels'
mm_map.update(
{ addr: _mmp(lbl.mmid, lbl.comment) if lbl else _mmp(None,None) for lbl,addr in
[(get_tw_label(self.proto,a), b) for a,b in await self.get_addr_label_pairs()] }
)
msg_r('Getting wallet transactions...')
_wallet_txs = await self.rpc.gathered_icall(
'gettransaction',
[ (i,True,True) for i in {d['txid'] for d in data} ] )
msg('done')
if not 'decoded' in _wallet_txs[0]:
_decoded_txs = iter(
await self.rpc.gathered_call(
'decoderawtransaction',
[ (d['hex'],) for d in _wallet_txs ] ))
for tx in _wallet_txs:
tx['decoded'] = next(_decoded_txs)
if g.debug_tw:
do_json_dump((_wallet_txs, 'wallet-txs'),)
_wip = namedtuple('prevout',['txid','vout'])
txdata = [
{
'tx': tx,
'wallet_vouts': sorted({i.vout for i in
[_wip( CoinTxID(d['txid']), d['vout'] ) for d in data]
if i.txid == tx['txid']}),
'prevouts': [_wip( CoinTxID(vin['txid']), vin['vout'] ) for vin in tx['decoded']['vin']]
}
for tx in _wallet_txs]
_prevout_txids = {i.txid for d in txdata for i in d['prevouts']}
msg_r('Getting input transactions...')
_prevout_txs = await self.rpc.gathered_call('getrawtransaction', [ (i,True) for i in _prevout_txids ])
msg('done')
_prevout_txs_dict = dict(zip(_prevout_txids,_prevout_txs))
for d in txdata:
d['prevout_txs'] = [_prevout_txs_dict[txid] for txid in {i.txid for i in d['prevouts']} ]
if g.debug_tw:
do_json_dump(
(rpc_data, 'txhist-rpc'),
(data, 'txhist'),
(mm_map, 'mmap'),
(_prevout_txs, 'prevout-txs'),
(txdata, 'txdata'),
)
unspent_info = await self.get_unspent_by_mmid()
return (
BitcoinTwTransaction(
parent = self,
proto = self.proto,
rpc = self.rpc,
idx = idx,
unspent_info = unspent_info,
mm_map = mm_map,
**d ) for idx,d in enumerate(txdata) )

View file

@ -33,7 +33,8 @@ _colors = {
'gray': ( 246, (30,1) ),
'purple': ( 141, (35,1) ),
'brown': ( 208, (33,0) ),
'melon': ( 222, (33,1) ),
'brown': ( 173, (33,0) ),
'grndim': ( 108, (32,0) ),
'redbg': ( (232,210), (30,101) ),
'grnbg': ( (232,121), (30,102) ),

View file

@ -1 +1 @@
13.2.dev1
13.2.dev2

View file

@ -81,6 +81,7 @@ class GlobalContext(Lockable):
debug_rpc = False
debug_addrlist = False
debug_subseed = False
debug_tw = False
quiet = False
no_license = False
force_256_color = False
@ -245,6 +246,7 @@ class GlobalContext(Lockable):
'MMGEN_DEBUG_OPTS',
'MMGEN_DEBUG_RPC',
'MMGEN_DEBUG_ADDRLIST',
'MMGEN_DEBUG_TW',
'MMGEN_DEBUG_UTF8',
'MMGEN_DEBUG_SUBSEED',
'MMGEN_QUIET',

View file

@ -162,6 +162,7 @@ mods = {
'remove_address',
'remove_label',
'twview',
'txhist',
),
}

View file

@ -132,6 +132,21 @@ class tool_cmd(tool_cmd_base):
del obj.wallet
return ret
async def txhist(self,
pager = False,
reverse = False,
detail = False,
sinceblock = 0,
sort = 'age',
age_fmt: options_annot_str(TwCommon.age_fmts) = 'confs',
interactive = False ):
"view transaction history"
from ..tw.txhistory import TwTxHistory
obj = await TwTxHistory(self.proto,sinceblock=sinceblock)
return await self.twops(
obj,pager,reverse,detail,sort,age_fmt,interactive,show_mmid=None)
async def add_label(self,mmgen_or_coin_addr:str,label:str):
"add descriptive label for address in tracking wallet"
from ..tw.ctl import TrackingWallet

View file

@ -29,7 +29,7 @@ from ..color import nocolor,yellow,green
from ..util import msg,msg_r,fmt,die,line_input,do_pager,capfirst,make_timestr
from ..addr import MMGenID
# mixin class for TwUnspentOutputs,TwAddrList:
# mixin class for TwUnspentOutputs,TwAddrList,TwTxHistory:
class TwCommon:
cols = None

235
mmgen/tw/txhistory.py Executable file
View file

@ -0,0 +1,235 @@
#!/usr/bin/env python3
#
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
# Licensed under the GNU General Public License, Version 3:
# https://www.gnu.org/licenses
# Public project repositories:
# https://github.com/mmgen/mmgen
# https://gitlab.com/mmgen/mmgen
"""
tw.txhistory: Tracking wallet transaction history class for the MMGen suite
"""
from collections import namedtuple
from ..util import base_proto_subclass,fmt
from ..base_obj import AsyncInit
from ..objmethods import MMGenObject
from ..obj import CoinTxID,MMGenList,Int
from ..rpc import rpc_init
from .common import TwCommon
class TwTxHistory(MMGenObject,TwCommon,metaclass=AsyncInit):
def __new__(cls,proto,*args,**kwargs):
return MMGenObject.__new__(base_proto_subclass(cls,proto,'tw','txhistory'))
txid_w = 64
show_txid = False
show_unconfirmed = False
show_total_amt = False
print_hdr_fs = '{a} (block #{b}, {c} UTC)\n{d}Sort order: {e}\n{f}\n'
age_fmts_interactive = ('confs','block','days','date','date_time')
update_params_on_age_toggle = True
detail_display_separator = '\n\n'
print_output_types = ('squeezed','detail')
async def __init__(self,proto,sinceblock=0):
self.proto = proto
self.data = MMGenList()
self.rpc = await rpc_init(proto)
self.sinceblock = Int( sinceblock if sinceblock >= 0 else self.rpc.blockcount + sinceblock )
@property
def no_rpcdata_errmsg(self):
return 'No transaction history {}found!'.format(
f'from block {self.sinceblock} ' if self.sinceblock else '')
def set_column_params(self):
data = self.data
show_txid = self.show_txid
for d in data:
d.skip = ''
if not hasattr(self,'varcol_maxwidths'):
self.varcol_maxwidths = {
'addr1': max(len(d.vouts_disp('inputs',width=None,color=False)) for d in data),
'addr2': max(len(d.vouts_disp('outputs',width=None,color=False)) for d in data),
'lbl': max(len(d.label) for d in data),
}
# var cols: addr1 addr2 comment [txid]
maxw = self.varcol_maxwidths
if show_txid:
txid_adj = 40 # we don't need much of the txid, so weight it less than other columns
maxw.update({'txid': self.txid_w - txid_adj})
elif 'txid' in maxw:
del maxw['txid']
minw = {
'addr1': 15,
'addr2': 15,
'lbl': len('Comment'),
}
if show_txid:
minw.update({'txid': 8})
# fixed cols: num age amt
col1_w = max(2,len(str(len(data)))+1) # num + ')'
amt_w = self.disp_prec + 5
fixed_w = col1_w + self.age_w + amt_w + sum(minw.values()) + (6 + show_txid) # one leading space in fs
var_w = sum(maxw.values()) - sum(minw.values())
# get actual screen width:
self.all_maxw = fixed_w + var_w + (txid_adj if show_txid else 0)
self.cols = min( self.get_term_columns(fixed_w), self.all_maxw )
total_freew = self.cols - fixed_w
varw = {k: max(maxw[k] - minw[k],0) for k in maxw}
freew = {k: int(min(total_freew * (varw[k] / var_w), varw[k])) for k in maxw}
varcols = set(maxw.keys())
for k in maxw:
freew[k] = min( total_freew - sum(freew[k2] for k2 in varcols-{k}), varw[k] )
self.column_params = namedtuple('column_params',
['col1','txid','addr1','amt','addr2','lbl'])(
col1_w,
min(
# max txid was reduced by txid_adj, so stretch to fill available space, if any
minw['txid'] + freew['txid'] + total_freew - sum(freew.values()),
self.txid_w ) if 'txid' in minw else 0,
minw['addr1'] + freew['addr1'],
amt_w,
minw['addr2'] + freew['addr2'],
minw['lbl'] + freew['lbl'] )
def gen_squeezed_display(self,cw,color):
if self.sinceblock:
yield f'Displaying transactions since block {self.sinceblock.hl(color=color)}'
yield 'Only wallet-related outputs are shown'
yield 'Comment is from first wallet address in outputs or inputs'
if (cw.addr1 < self.varcol_maxwidths['addr1'] or
cw.addr2 < self.varcol_maxwidths['addr2'] ):
yield 'Due to screen width limitations, not all addresses could be displayed'
yield ''
hdr_fs = self.squeezed_hdr_fs_fs.format(
nw = cw.col1,
dw = self.age_w,
txid_fs = f'{{i:{cw.txid}}} ' if self.show_txid else '',
aw = cw.addr1,
a2w = cw.addr2 )
fs = self.squeezed_fs_fs.format(
nw = cw.col1,
dw = self.age_w,
txid_fs = f'{{i:{cw.txid}}} ' if self.show_txid else '' )
yield hdr_fs.format(
n = '',
i = 'TxID',
d = self.age_hdr,
a1 = 'Inputs',
A = 'Amt({})'.format('TX' if self.show_total_amt else 'Wallet').ljust(cw.amt),
a2 = 'Outputs',
l = 'Comment' ).rstrip()
n = 0
for d in self.data:
if d.confirmations > 0 or self.show_unconfirmed:
n += 1
yield fs.format(
n = str(n) + ')',
i = d.txid_disp( width=cw.txid, color=color ),
d = d.age_disp( self.age_fmt, width=self.age_w, color=color ),
a1 = d.vouts_disp( 'inputs', width=cw.addr1, color=color ),
A = d.amt_disp(self.show_total_amt).fmt( prec=self.disp_prec, color=color ),
a2 = d.vouts_disp( 'outputs', width=cw.addr2, color=color ),
l = d.label.fmt( width=cw.lbl, color=color ) ).rstrip()
def gen_detail_display(self,color):
yield (
(f'Displaying transactions since block {self.sinceblock.hl(color=color)}\n'
if self.sinceblock else '')
+ 'Only wallet-related outputs are shown'
)
fs = fmt("""
{n}
Block: [{d}] {b}
TxID: [{D}] {i}
Value: {A1}
Wallet Value: {A2}
Fee: {f}
Inputs:
{a1}
Outputs ({oc}):
{a2}
""",strip_char='\t').strip()
n = 0
for d in self.data:
if d.confirmations > 0 or self.show_unconfirmed:
n += 1
yield fs.format(
n = str(n) + ')',
d = d.age_disp( 'date_time', width=None, color=None ),
b = d.blockheight_disp(color=color),
D = d.txdate_disp( 'date_time' ),
i = d.txid_disp( width=None, color=color ),
A1 = d.amt_disp(True).hl( color=color ),
A2 = d.amt_disp(False).hl( color=color ),
f = d.fee_disp( color=color ),
a1 = d.vouts_list_disp( 'inputs', color=color, indent=' '*8 ),
oc = d.nOutputs,
a2 = d.vouts_list_disp( 'outputs', color=color, indent=' '*8 ),
)
sort_disp = {
'age': 'Age',
'blockheight': 'Block Height',
'amt': 'Wallet Amt',
'total_amt': 'TX Amt',
'txid': 'TxID',
}
sort_funcs = {
'age': lambda i: i.time,
'blockheight': lambda i: 0 - abs(i.confirmations), # old/altcoin daemons return no 'blockheight' field
'amt': lambda i: i.wallet_outputs_total,
'total_amt': lambda i: i.outputs_total,
'txid': lambda i: i.txid,
}
@staticmethod
async def set_dates(rpc,us):
pass
@property
def dump_fn_pfx(self):
return 'transaction-history' + (f'-since-block-{self.sinceblock}' if self.sinceblock else '')
class action(TwCommon.action):
def s_amt(self,parent):
parent.do_sort('amt')
parent.show_total_amt = False
def s_total_amt(self,parent):
parent.do_sort('total_amt')
parent.show_total_amt = True
def d_show_txid(self,parent):
parent.show_txid = not parent.show_txid
parent.set_column_params()
def d_show_unconfirmed(self,parent):
parent.show_unconfirmed = not parent.show_unconfirmed
def d_show_total_amt(self,parent):
parent.show_total_amt = not parent.show_total_amt

View file

@ -48,13 +48,13 @@ rt_data = {
'rtBals': {
'btc': ('499.9999488','399.9998282','399.9998147','399.9996877',
'52.99980410','946.99933647','999.99914057','52.9999',
'946.99933647','0.4169328'),
'946.99933647','0.4169328','6.24987417'),
'bch': ('499.9999484','399.9999194','399.9998972','399.9997692',
'46.78890380','953.20966920','999.99857300','46.789',
'953.2096692','0.4169328'),
'953.2096692','0.4169328','39.58187387'),
'ltc': ('5499.99744','5399.994425','5399.993885','5399.987535',
'52.98520500','10946.93753500','10999.92274000','52.99',
'10946.937535','0.41364'),
'10946.937535','0.41364','6.24846787'),
},
'rtBals_gb': {
'btc': {
@ -230,6 +230,16 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
('bob_twview4', "viewing Bob's tracking wallet"),
('bob_alice_bal', "Bob and Alice's balances"),
('bob_nochg_burn', 'zero-change transaction to burn address'),
('generate', 'mining a block'),
('bob_txhist1', "viewing Bob's transaction history (sort=age)"),
('bob_txhist2', "viewing Bob's transaction history (sort=blockheight reverse=1)"),
('bob_txhist3', "viewing Bob's transaction history (sort=blockheight sinceblock=-7)"),
('bob_txhist4', "viewing Bob's transaction history (sinceblock=439 detail=1)"),
('bob_txhist_interactive', "viewing Bob's transaction history (age_fmt=date_time interactive=true)"),
('alice_bal2', "Alice's balance"),
('alice_add_label1', 'adding a label'),
@ -592,6 +602,45 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
sid = self._user_sid('bob')
return self.user_twview('bob',chk=(sid+':L:5',rtBals[9]),sort='twmmid')
def user_txhist(self,user,args,expect):
t = self.spawn('mmgen-tool',['--'+user,'txhist'] + args)
res = strip_ansi_escapes(t.read()).replace('\r','')
m = re.search(expect,res,re.DOTALL)
assert m, m
return t
def bob_txhist1(self):
return self.user_txhist('bob',
args = ['sort=age'],
expect = fr'\s1\).*\s{rtFundAmt}\s' )
def bob_txhist2(self):
return self.user_txhist('bob',
args = ['sort=blockheight','reverse=1','age_fmt=block'],
expect = fr'\s1\).*:{self.dfl_mmtype}:1\s' )
def bob_txhist3(self):
return self.user_txhist('bob',
args = ['sort=blockheight','sinceblock=-7','age_fmt=block'],
expect = fr'Displaying transactions since block 439.*\s6\).*:C:2\s.*\s{rtBals[9]}\s.*:L:5.*\s7\)'
)
def bob_txhist4(self):
return self.user_txhist('bob',
args = ['sort=blockheight','sinceblock=439','age_fmt=block','detail=1'],
expect = fr'Displaying transactions since block 439.*\s7\).*Block:.*446.*Value:.*{rtBals[10]}'
)
def bob_txhist_interactive(self):
self.get_file_with_ext('out',delete_all=True)
t = self.spawn('mmgen-tool',
['--bob',f'--outdir={self.tmpdir}','txhist','age_fmt=date_time','interactive=true'] )
for resp in ('u','i','t','a','m','T','A','r','r','D','D','D','D','p','P','b','V'):
t.expect('draw:\b',resp,regex=True)
txnum,idx = (8,1) if self.proto.coin == 'BCH' else (9,3)
t.expect(f'\s{txnum}\).*Inputs:.*:L:{idx}.*Outputs \(3\):.*:C:2.*\s10\)','q',regex=True)
return t
def bob_getbalance(self,bals,confs=1):
for i in (0,1,2):
assert Decimal(bals['mmgen'][i]) + Decimal(bals['nonmm'][i]) == Decimal(bals['total'][i])
@ -611,6 +660,12 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
def bob_1conf1_getbalance(self): return self.bob_getbalance(rtBals_gb['1conf1'],confs=1)
def bob_1conf2_getbalance(self): return self.bob_getbalance(rtBals_gb['1conf2'],confs=2)
def bob_nochg_burn(self):
return self.user_txdo('bob',
fee = '0.00009713',
outputs_cl = [f'{make_burn_addr(self.proto)}'],
outputs_list = '1' )
def bob_alice_bal(self):
t = self.spawn('mmgen-regtest',['balances'])
ret = t.expect_getend("Bob's balance:").strip()