txcreate,twview,listaddresses: display transaction date

- The `mmgen-tool` `listaddresses` and `twview` commands, as well as the
  unspent outputs menu of `mmgen-txcreate`, can now display the date/time of
  each output in addition to its number of confirmations, block, or age in
  days.  Two date display precisions are available: “approx”, an estimate based
  on the historical average block confirmation interval, and “exact”, the
  timestamp of the block containing the output.  Since display of the exact
  date requires an extra RPC call per output, precision defaults to “approx”.
  The setting also affects the precision of the age-in-days output.

Usage:

    $ mmgen-txcreate -i # 'D' to cycle through display opts, 'x' for exact age
    $ mmgen-tool listaddresses age_fmt=date exact_age=1 # (or age_fmt=date_time)
    $ mmgen-tool twview age_fmt=date exact_age=1        # (or age_fmt=date_time)

Testing:

    $ test/test.py -ne regtest
    $ test/test.py -ne --coin=eth ethdev
This commit is contained in:
The MMGen Project 2020-04-04 13:18:44 +00:00
commit b671453c13
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
9 changed files with 241 additions and 67 deletions

View file

@ -304,9 +304,11 @@ Actions: [q]uit view, [p]rint to file, pager [v]iew, [w]ide view,
twmmid = MMGenImmutableAttr('twmmid','TwMMGenID')
addr = MMGenImmutableAttr('addr','CoinAddr')
confs = MMGenImmutableAttr('confs',int,typeconv=False)
days = MMGenListItemAttr('days',int,typeconv=False)
skip = MMGenListItemAttr('skip',str,typeconv=False,reassign_ok=True)
def age_disp(self,o,age_fmt): # TODO
return None
class EthereumTokenTwUnspentOutputs(EthereumTwUnspentOutputs):
disp_type = 'token'
@ -322,7 +324,7 @@ class EthereumTokenTwUnspentOutputs(EthereumTwUnspentOutputs):
class EthereumTwAddrList(TwAddrList):
def __init__(self,usr_addr_list,minconf,showempty,showbtcaddrs,all_labels,wallet=None):
def __init__(self,usr_addr_list,minconf,showempty,showbtcaddrs,all_labels,exact_age,wallet=None):
self.wallet = wallet or TrackingWallet(mode='w')
tw_dict = self.wallet.mmid_ordered_dict

View file

@ -103,6 +103,7 @@ class BitcoinProtocol(MMGenObject):
sign_mode = 'daemon'
secp256k1_ge = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141
privkey_len = 32
avg_bdi = int(9.7 * 60) # average block discovery interval (historical)
@classmethod
def addr_fmt_to_ver_bytes(cls,req_fmt,return_hex=False):
@ -292,6 +293,7 @@ class LitecoinProtocol(BitcoinProtocol):
base_coin = 'LTC'
forks = []
bech32_hrp = 'ltc'
avg_bdi = 2 * 60
class LitecoinTestnetProtocol(LitecoinProtocol):
# addr ver nums same as Bitcoin testnet, except for 'p2sh'

View file

@ -232,6 +232,7 @@ class RPCConnection(MMGenObject):
'getblockchaininfo',
'getblockcount',
'getblockhash',
'getblockheader',
'getmempoolinfo',
'getmempoolentry',
'getnettotals',
@ -257,6 +258,8 @@ class EthereumRPCConnection(RPCConnection):
auth = False
db_fs = ' host [{h}] port [{p}]\n'
_blockcount = None
_cur_date = None
rpcmethods = (
'eth_accounts',
@ -268,7 +271,6 @@ class EthereumRPCConnection(RPCConnection):
'eth_gasPrice',
'eth_getBalance',
'eth_getBlockByHash',
'eth_getBlockByNumber',
'eth_getCode',
'eth_getTransactionByHash',
'eth_getTransactionReceipt',
@ -285,6 +287,7 @@ class EthereumRPCConnection(RPCConnection):
'parity_composeTransaction',
'parity_gasCeilTarget',
'parity_gasFloorTarget',
'parity_getBlockHeaderByNumber',
'parity_localTransactions',
'parity_minGasPrice',
'parity_mode',
@ -297,6 +300,19 @@ class EthereumRPCConnection(RPCConnection):
'parity_versionInfo',
)
# blockcount and cur_date require network RPC calls, so evaluate lazily
@property
def blockcount(self):
if self._blockcount == None:
self._blockcount = int(self.eth_blockNumber(),16)
return self._blockcount
@property
def cur_date(self):
if self._cur_date == None:
self._cur_date = int(self.parity_getBlockHeaderByNumber(hex(self.blockcount))['timestamp'],16)
return self._cur_date
class MoneroWalletRPCConnection(RPCConnection):
rpcmethods = (
@ -390,7 +406,6 @@ def init_daemon_parity():
conn = EthereumRPCConnection(
g.rpc_host or 'localhost',
g.rpc_port or g.proto.rpc_port)
conn.daemon_version = conn.parity_versionInfo()['version'] # fail immediately if daemon is geth
conn.coin_amt_type = str
g.chain = conn.parity_chain().replace(' ','_')
@ -415,7 +430,7 @@ def init_daemon_bitcoind():
def check_chainfork_mismatch(conn):
block0 = conn.getblockhash(0)
latest = conn.getblockcount()
latest = conn.blockcount
try:
assert block0 == g.proto.block0,'Incorrect Genesis block for {}'.format(g.proto.__name__)
for fork in g.proto.forks:
@ -446,6 +461,8 @@ def init_daemon_bitcoind():
from mmgen.regtest import MMGenRegtest
MMGenRegtest(g.coin).switch_user(('alice','bob')[g.bob],quiet=True)
conn.daemon_version = int(conn.getnetworkinfo()['version'])
conn.blockcount = conn.getblockcount()
conn.cur_date = conn.getblockheader(conn.getblockhash(conn.blockcount))['time']
conn.coin_amt_type = (float,str)[conn.daemon_version>=120000]
g.chain = conn.getblockchaininfo()['chain']
if g.chain != 'regtest': g.chain += 'net'

View file

@ -27,6 +27,9 @@ from mmgen.addr import *
NL = ('\n','\r\n')[g.platform=='win']
def _options_annot_str(l):
return '(valid options: {})'.format(','.join(l))
def _create_call_sig(cmd,parsed=False):
m = getattr(MMGenToolCmd,cmd)
@ -803,6 +806,8 @@ class MMGenToolCmdWallet(MMGenToolCmdBase):
ret = d.sec.wif if target=='wif' else d.addr
return ret
from mmgen.tw import TwAddrList,TwUnspentOutputs
class MMGenToolCmdRPC(MMGenToolCmdBase):
"tracking wallet commands using the JSON-RPC interface"
@ -817,14 +822,18 @@ class MMGenToolCmdRPC(MMGenToolCmdBase):
pager = False,
showempty = True,
showbtcaddr = True,
age_fmt:'(valid options: days,confs)' = ''):
age_fmt: _options_annot_str(TwAddrList.age_fmts) = 'confs',
exact_age = False,
):
"list the specified MMGen address and its balance"
return self.listaddresses( mmgen_addrs = mmgen_addr,
minconf = minconf,
pager = pager,
showempty = showempty,
showbtcaddrs = showbtcaddr,
age_fmt = age_fmt)
age_fmt = age_fmt,
exact_age = exact_age,
)
def listaddresses( self,
mmgen_addrs:'(range or list)' = '',
@ -834,7 +843,9 @@ class MMGenToolCmdRPC(MMGenToolCmdBase):
showbtcaddrs = True,
all_labels = False,
sort:'(valid options: reverse,age)' = '',
age_fmt:'(valid options: days,confs)' = ''):
age_fmt: _options_annot_str(TwAddrList.age_fmts) = 'confs',
exact_age = False,
):
"list MMGen addresses and their balances"
show_age = bool(age_fmt)
@ -853,11 +864,10 @@ class MMGenToolCmdRPC(MMGenToolCmdBase):
usr_addr_list = [MMGenID('{}:{}'.format(a[0],i)) for i in AddrIdxList(a[1])]
rpc_init()
from mmgen.tw import TwAddrList
al = TwAddrList(usr_addr_list,minconf,showempty,showbtcaddrs,all_labels)
al = TwAddrList(usr_addr_list,minconf,showempty,showbtcaddrs,all_labels,exact_age)
if not al:
die(0,('No tracked addresses with balances!','No tracked addresses!')[showempty])
return al.format(showbtcaddrs,sort,show_age,age_fmt or 'days')
return al.format(showbtcaddrs,sort,show_age,age_fmt or 'confs')
def twview( self,
pager = False,
@ -865,16 +875,18 @@ class MMGenToolCmdRPC(MMGenToolCmdBase):
wide = False,
minconf = 1,
sort = 'age',
age_fmt:'(valid options: days,confs)' = 'days',
show_mmid = True):
age_fmt: _options_annot_str(TwUnspentOutputs.age_fmts) = 'confs',
exact_age = False,
show_mmid = True,
wide_show_confs = True):
"view tracking wallet"
rpc_init()
from mmgen.tw import TwUnspentOutputs
twuo = TwUnspentOutputs(minconf=minconf)
twuo.do_sort(sort,reverse=reverse)
twuo.age_fmt = age_fmt
twuo.age_prec = 'exact' if exact_age else 'approx'
twuo.show_mmid = show_mmid
ret = twuo.format_for_printing(color=True) if wide else twuo.format_for_display()
ret = twuo.format_for_printing(color=True,show_confs=wide_show_confs) if wide else twuo.format_for_display()
del twuo.wallet
return ret

View file

@ -34,6 +34,34 @@ def get_tw_label(s):
except BadTwComment: raise
except: return None
_date2days = lambda date: (g.rpch.cur_date - date) // 86400
_confs2date_approx = lambda o: g.rpch.cur_date - int(g.proto.avg_bdi * (o.confs - 1))
_confs2date_exact = lambda o: (
# g.rpch.getblockheader(g.rpch.getblockhash(g.rpch.blockcount - (o.confs - 1)))['time']
g.rpch.gettransaction(o.txid)['blocktime'] # same as above, differs from 'time'
if o.confs
else g.rpch.cur_date )
if os.getenv('MMGEN_BOGUS_WALLET_DATA'):
# 1831006505 (09 Jan 2028) = projected time of block 1000000
_date2days = lambda date: (1831006505 - date) // 86400
_confs2date_approx = lambda o: 1831006505 - (10 * 60 * (o.confs - 1))
_confs2date_exact = lambda o: 1831006505 - int(9.7 * 60 * (o.confs - 1))
def _format_date(secs):
t = time.gmtime(secs)
return '{}-{:02}-{:02}'.format(str(t.tm_year)[2:],t.tm_mon,t.tm_mday)
def _format_date_time(secs):
t = time.gmtime(secs)
return '{}-{:02}-{:02} {:02}:{:02}'.format(
t.tm_year,
t.tm_mon,
t.tm_mday,
t.tm_hour,
t.tm_min,
)
class TwUnspentOutputs(MMGenObject):
def __new__(cls,*args,**kwargs):
@ -49,14 +77,23 @@ class TwUnspentOutputs(MMGenObject):
prompt_fs = 'Total to spend, excluding fees: {} {}\n\n'
prompt = """
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
Display options: show [D]ays, [g]roup, [m]mgen addr, e[x]act age; r[e]draw
Actions: [q]uit view, [p]rint to file, pager [v]iew, [w]ide view, add [l]abel:
"""
key_mappings = {
't':'s_txid','a':'s_amt','d':'s_addr','A':'s_age','r':'d_reverse','M':'s_twmmid',
'D':'d_days','g':'d_group','m':'d_mmid','e':'d_redraw',
'D':'d_days','g':'d_group','m':'d_mmid','x':'d_exact_age','e':'d_redraw',
'q':'a_quit','p':'a_print','v':'a_view','w':'a_view_wide','l':'a_lbl_add' }
col_adj = 38
age_fmts = ('confs','block','days','date','date_time')
age_fmts_ia = ('confs','block','days','date')
_age_fmt = 'confs'
age_precs = ('approx','exact')
age_prec = 'approx'
age_prec_disp = {
'approx': '(≈)',
'exact': '',
}
class MMGenTwOutputList(list,MMGenObject): pass
@ -69,8 +106,8 @@ Actions: [q]uit view, [p]rint to file, pager [v]iew, [w]ide view, add [l]abel:
twmmid = MMGenImmutableAttr('twmmid','TwMMGenID')
addr = MMGenImmutableAttr('addr','CoinAddr')
confs = MMGenImmutableAttr('confs',int,typeconv=False)
date = MMGenListItemAttr('date',int,typeconv=False,reassign_ok=True)
scriptPubKey = MMGenImmutableAttr('scriptPubKey','HexStr')
days = MMGenListItemAttr('days',int,typeconv=False)
skip = MMGenListItemAttr('skip',str,typeconv=False,reassign_ok=True)
wmsg = {
@ -90,7 +127,6 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
self.show_mmid = True
self.minconf = minconf
self.addrs = addrs
self.age_fmt = 'days'
self.sort_key = 'age'
self.disp_prec = self.get_display_precision()
@ -98,16 +134,14 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
self.get_unspent_data()
self.do_sort()
@property
def age_fmt(self):
return self._age_fmt
@age_fmt.setter
def age_fmt(self,val):
age_fmts = ('days','confs')
if val not in age_fmts:
raise BadAgeFormat("'{}': invalid age format (must be one of {!r})".format(val,age_fmts))
if val not in self.age_fmts:
raise BadAgeFormat("'{}': invalid age format (must be one of {!r})".format(val,self.age_fmts))
self._age_fmt = val
def get_display_precision(self):
@ -137,7 +171,6 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
us_rpc = self.get_unspent_rpc()
if not us_rpc: die(0,self.wmsg['no_spendable_outputs'])
confs_per_day = 60*60*24 // g.proto.secs_per_block
tr_rpc = []
lbl_id = ('account','label')['label_api' in g.rpch.caps]
for o in us_rpc:
@ -147,7 +180,6 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
o.update({
'twmmid': l.mmid,
'label': l.comment,
'days': int(o['confirmations'] // confs_per_day),
'amt': g.proto.coin_amt(o['amount']),
'addr': CoinAddr(o['address']),
'confs': o['confirmations']
@ -207,7 +239,7 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
acct_w = min(max_acct_w, max(24,addr_w-10))
btaddr_w = addr_w - acct_w - 1
label_w = acct_w - mmid_w - 1
tx_w = min(self.txid_w,self.cols-addr_w-28-col1_w) # min=7
tx_w = min(self.txid_w,self.cols-addr_w-29-col1_w) # min=6 TODO
txdots = ('','..')[tx_w < self.txid_w]
for i in unsp: i.skip = ''
@ -222,14 +254,22 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
fs = { 'btc': ' {n:%s} {t:%s} {v:2} {a} {A} {c:<}' % (col1_w,tx_w),
'eth': ' {n:%s} {a} {A}' % col1_w,
'token': ' {n:%s} {a} {A} {A2}' % col1_w }[self.disp_type]
out += [fs.format( n='Num',
t='TXid'.ljust(tx_w - 5) + ' Vout',
v='',
fs_hdr = ' {n:%s} {t:%s} {a} {A} {c:<}' % (col1_w,tx_w) if self.disp_type == 'btc' else fs
date_hdr = {
'confs': lambda: 'Confs',
'block': lambda: 'Block',
'days': lambda: 'Age({}d)'.format(self.age_prec_disp[self.age_prec][1:2]),
'date': lambda: 'Date'+self.age_prec_disp[self.age_prec],
'date_time': lambda: 'Date'+self.age_prec_disp[self.age_prec],
}
out += [fs_hdr.format(
n='Num',
t='TXid'.ljust(tx_w - 2) + ' Vout',
a='Address'.ljust(addr_w),
A='Amt({})'.format(g.dcoin).ljust(self.disp_prec+3),
A='Amt({})'.format(g.dcoin).ljust(self.disp_prec+5),
A2=' Amt({})'.format(g.coin).ljust(self.disp_prec+4),
c=('Confs','Age(d)')[self.age_fmt=='days']
).rstrip()]
c = date_hdr[self.age_fmt](),
).rstrip()]
for n,i in enumerate(unsp):
addr_dots = '|' + '.'*(addr_w-1)
@ -254,29 +294,31 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
a=addr_out,
A=i.amt.fmt(color=True,prec=self.disp_prec),
A2=(i.amt2.fmt(color=True,prec=self.disp_prec) if i.amt2 is not None else ''),
c=i.days if self.age_fmt == 'days' else i.confs
c=self.age_disp(i,self.age_fmt),
).rstrip())
self.fmt_display = '\n'.join(out) + '\n'
return self.fmt_display
def format_for_printing(self,color=False):
def format_for_printing(self,color=False,show_confs=True):
addr_w = max(len(i.addr) for i in self.unspent)
mmid_w = max(len(('',i.twmmid)[i.twmmid.type=='mmgen']) for i in self.unspent) or 12 # DEADBEEF:S:1
amt_w = g.proto.coin_amt.max_prec + 4
fs = { 'btc': ' {n:4} {t:%s} {a} {m} {A:%s} {c:<8} {g:<6} {l}' % (self.txid_w+3,amt_w),
amt_w = g.proto.coin_amt.max_prec + 5
cfs = '{c:<8} ' if show_confs else ''
fs = { 'btc': (' {n:4} {t:%s} {a} {m} {A:%s} ' + cfs + '{b:<8} {D:<19} {l}') % (self.txid_w+3,amt_w),
'eth': ' {n:4} {a} {m} {A:%s} {l}' % amt_w,
'token': ' {n:4} {a} {m} {A:%s} {A2:%s} {l}' % (amt_w,amt_w)
}[self.disp_type]
out = [fs.format( n='Num',
t='Tx ID,Vout',
a='Address'.ljust(addr_w),
m='MMGen ID'.ljust(mmid_w+1),
A='Amount({})'.format(g.dcoin).ljust(amt_w+1),
m='MMGen ID'.ljust(mmid_w),
A='Amount({})'.format(g.dcoin),
A2='Amount({})'.format(g.coin),
c='Confs', # skipped for eth
g='Age(d)', # skipped for eth
b='Block', # skipped for eth
D='Date'+self.age_prec_disp[self.age_prec], # skipped for eth
l='Label')]
max_lbl_len = max([len(i.label) for i in self.unspent if i.label] or [2])
@ -291,14 +333,16 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
A=i.amt.fmt(color=color),
A2=(i.amt2.fmt(color=color) if i.amt2 is not None else ''),
c=i.confs,
g=i.days,
b=g.rpch.blockcount - (i.confs - 1),
D=self.age_disp(i,'date_time'),
l=i.label.hl(color=color) if i.label else
TwComment.fmtc('',color=color,nullrepl='-',width=max_lbl_len)).rstrip())
fs = '{} ({} UTC)\nSort order: {}\n{}\n\nTotal {}: {}\n'
fs = '{} (block #{}, {} UTC)\nSort order: {}\n{}\n\nTotal {}: {}\n'
self.fmt_print = fs.format(
capfirst(self.desc),
make_timestr(),
g.rpch.blockcount,
make_timestr(g.rpch.cur_date),
' '.join(self.sort_info(include_group=False)),
'\n'.join(out),
g.dcoin,
@ -359,11 +403,16 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
if action[:2] == 's_':
self.do_sort(action[2:])
if action == 's_twmmid': self.show_mmid = True
elif action == 'd_days': self.age_fmt = ('days','confs')[self.age_fmt=='days']
elif action == 'd_days':
af = self.age_fmts_ia
self.age_fmt = af[(af.index(self.age_fmt) + 1) % len(af)]
elif action == 'd_mmid': self.show_mmid = not self.show_mmid
elif action == 'd_group':
if self.can_group:
self.group = not self.group
elif action == 'd_exact_age':
ap = self.age_precs
self.age_prec = ap[(ap.index(self.age_prec) + 1) % len(ap)]
elif action == 'd_redraw': pass
elif action == 'd_reverse': self.unspent.reverse(); self.reverse = not self.reverse
elif action == 'a_quit': msg(''); return self.unspent
@ -412,12 +461,33 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
msg_r(CUR_RIGHT(len(prompt.split('\n')[-1])-2))
no_output = True
def age_disp(self,o,age_fmt):
if age_fmt == 'confs':
return o.confs
elif age_fmt == 'block':
return g.rpch.blockcount - (o.confs - 1)
else:
if self.age_prec == 'approx':
date = _confs2date_approx(o)
else:
if o.date == None:
o.date = _confs2date_exact(o)
date = o.date
return {
'days': _date2days(date),
'date': _format_date(date),
'date_time': _format_date_time(date),
}[age_fmt]
class TwAddrList(MMGenDict):
age_fmts = TwUnspentOutputs.age_fmts
age_disp = TwUnspentOutputs.age_disp
def __new__(cls,*args,**kwargs):
return MMGenDict.__new__(altcoin_subclass(cls,'tw','TwAddrList'),*args,**kwargs)
def __init__(self,usr_addr_list,minconf,showempty,showbtcaddrs,all_labels,wallet=None):
def __init__(self,usr_addr_list,minconf,showempty,showbtcaddrs,all_labels,exact_age,wallet=None):
def check_dup_mmid(acct_labels):
mmid_prev,err = None,False
@ -440,6 +510,7 @@ class TwAddrList(MMGenDict):
msg("'{}': more than one {} address in account!".format(addrs,g.coin))
if err: rdie(3,'Tracking wallet is corrupted!')
self.age_prec = 'exact' if exact_age else 'approx'
self.total = g.proto.coin_amt('0')
rpc_init()
@ -449,17 +520,21 @@ class TwAddrList(MMGenDict):
if d['confirmations'] < minconf: continue
label = get_tw_label(d[lbl_id])
if label:
if usr_addr_list and (label.mmid not in usr_addr_list): continue
if label.mmid in self:
if self[label.mmid]['addr'] != d['address']:
lm = label.mmid
if usr_addr_list and (lm not in usr_addr_list):
continue
if lm in self:
if self[lm]['addr'] != d['address']:
die(2,'duplicate {} address ({}) for this MMGen address! ({})'.format(
g.coin,d['address'],self[label.mmid]['addr']))
g.coin,d['address'],self[lm]['addr']))
else:
self[label.mmid] = {'amt': g.proto.coin_amt('0'),
'lbl': label,
'addr': CoinAddr(d['address'])}
self[label.mmid]['lbl'].mmid.confs = d['confirmations']
self[label.mmid]['amt'] += d['amount']
lm.confs = d['confirmations']
lm.txid = d['txid']
lm.date = None
self[lm] = {'amt': g.proto.coin_amt('0'),
'lbl': label,
'addr': CoinAddr(d['address'])}
self[lm]['amt'] += d['amount']
self.total += d['amount']
# We use listaccounts only for empty addresses, as it shows false positive balances
@ -493,9 +568,8 @@ class TwAddrList(MMGenDict):
def coinaddr_list(self): return [self[k]['addr'] for k in self]
def format(self,showbtcaddrs,sort,show_age,age_fmt):
age_fmts = ('days','confs')
if age_fmt not in age_fmts:
raise BadAgeFormat("'{}': invalid age format (must be one of {!r})".format(age_fmt,age_fmts))
if age_fmt not in self.age_fmts:
raise BadAgeFormat("'{}': invalid age format (must be one of {!r})".format(age_fmt,self.age_fmts))
out = ['Chain: '+green(g.chain.upper())] if g.chain != 'mainnet' else []
fs = '{mid}' + ('',' {addr}')[showbtcaddrs] + ' {cmt} {amt}' + ('',' {age}')[show_age]
mmaddrs = [k for k in self.keys() if k.type == 'mmgen']
@ -510,7 +584,7 @@ class TwAddrList(MMGenDict):
addr=(CoinAddr.fmtc('ADDRESS',width=addr_width) if showbtcaddrs else None),
cmt=TwComment.fmtc('COMMENT',width=max_cmt_width+1),
amt='BALANCE'.ljust(max_fp_len+4),
age=('CONFS','DAYS')[age_fmt=='days'],
age=age_fmt.upper()
).rstrip()]
def sort_algo(j):
@ -518,13 +592,12 @@ class TwAddrList(MMGenDict):
return '{}_{:>012}_{}'.format(
j.obj.rsplit(':',1)[0],
# Hack, but OK for the foreseeable future:
(1000000000-j.confs if hasattr(j,'confs') and j.confs != None else 0),
(1000000000-(j.confs or 0) if hasattr(j,'confs') else 0),
j.sort_key)
else:
return j.sort_key
al_id_save = None
confs_per_day = 60*60*24 // g.proto.secs_per_block
for mmid in sorted(self,key=sort_algo,reverse=bool(sort and 'reverse' in sort)):
if mmid.type == 'mmgen':
if al_id_save and al_id_save != mmid.obj.al_id:
@ -542,7 +615,7 @@ class TwAddrList(MMGenDict):
addr=(e['addr'].fmt(color=True,width=addr_width) if showbtcaddrs else None),
cmt=e['lbl'].comment.fmt(width=max_cmt_width,color=True,nullrepl='-'),
amt=e['amt'].fmt('4.{}'.format(max(max_fp_len,3)),color=True),
age=mmid.confs // (1,confs_per_day)[age_fmt=='days'] if hasattr(mmid,'confs') and mmid.confs != None else '-'
age=self.age_disp(mmid,age_fmt) if hasattr(mmid,'confs') and mmid.confs != None else '-'
).rstrip())
return '\n'.join(out + ['\nTOTAL: {} {}'.format(self.total.hl(color=True),g.dcoin)])
@ -729,7 +802,7 @@ class TrackingWallet(MMGenObject):
msg('Data is unchanged\n')
def is_in_wallet(self,addr):
return addr in TwAddrList([],0,True,True,True,wallet=self).coinaddr_list()
return addr in TwAddrList([],0,True,True,True,False,wallet=self).coinaddr_list()
@write_mode
def set_label(self,coinaddr,lbl):

View file

@ -297,13 +297,11 @@ def decode_timestamp(s):
def make_timestamp(secs=None):
t = int(secs) if secs else time.time()
tv = time.gmtime(t)[:6]
return '{:04d}{:02d}{:02d}_{:02d}{:02d}{:02d}'.format(*tv)
return '{:04d}{:02d}{:02d}_{:02d}{:02d}{:02d}'.format(*time.gmtime(t)[:6])
def make_timestr(secs=None):
t = int(secs) if secs else time.time()
tv = time.gmtime(t)[:6]
return '{:04d}/{:02d}/{:02d} {:02d}:{:02d}:{:02d}'.format(*tv)
return '{}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}'.format(*time.gmtime(t)[:6])
def secs_to_dhms(secs):
dsecs = secs//3600

View file

@ -116,7 +116,6 @@ tests = {
'addr': (0b001, CoinAddr),
'confs': (0b001, int),
'scriptPubKey': (0b001, HexStr),
'days': (0b001, int),
'skip': (0b101, str),
},
[],

View file

@ -309,7 +309,7 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared):
'address': coinaddr,
'spendable': False,
'scriptPubKey': '{}{}{}'.format(s_beg,coinaddr.hex,s_end),
'confirmations': getrandnum(3) // 2 # max: 8388608 (7 digits)
'confirmations': getrandnum(3) // 20 # max: 838860 (6 digits)
}
return ret

View file

@ -31,6 +31,9 @@ from mmgen.seed import Wallet
from ..include.common import *
from .common import *
pat_date = r'\b\d\d-\d\d-\d\d\b'
pat_date_time = r'\b\d\d\d\d-\d\d-\d\d\s+\d\d:\d\d\b'
dfl_wcls = Wallet
rt_pw = 'abc-α'
rt_data = {
@ -225,6 +228,17 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
('alice_add_label_badaddr2', 'adding a label with invalid address for this chain'),
('alice_add_label_badaddr3', 'adding a label with wrong MMGen address'),
('alice_add_label_badaddr4', 'adding a label with wrong coin address'),
('alice_listaddresses', 'listaddresses'),
('alice_listaddresses_days', 'listaddresses (age_fmt=days)'),
('alice_listaddresses_date', 'listaddresses (age_fmt=date)'),
('alice_listaddresses_date_time', 'listaddresses (age_fmt=date_time)'),
('alice_listaddresses_date_time_exact', 'listaddresses (age_fmt=date_time exact_age=1)'),
('alice_twview', 'twview'),
('alice_twview_days', 'twview (age_fmt=days)'),
('alice_twview_date', 'twview (age_fmt=date)'),
('alice_twview_date_time', 'twview (age_fmt=date_time)'),
('alice_twview_date_time_exact', 'twview (age_fmt=date_time exact_age=1)'),
('alice_txcreate_info', 'txcreate -i'),
('stop', 'stopping regtest daemon'),
)
@ -841,6 +855,63 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
t.expect(r'\[q\]uit view, .*?:.','q',regex=True)
return t
def alice_listaddresses(self,args=[],expect=r'500\b'):
t = self.spawn('mmgen-tool',['--alice','listaddresses','showempty=1'] + args)
t.expect(expect,regex=True)
t.read()
return t
def alice_listaddresses_days(self):
return self.alice_listaddresses(args=['age_fmt=days'],expect=r'500\s+\d+\b')
def alice_listaddresses_date(self):
return self.alice_listaddresses(args=['age_fmt=date'],expect=r'500\s+'+pat_date)
def alice_listaddresses_date_time(self):
return self.alice_listaddresses(
args=['age_fmt=date_time'],
expect=r'500\s+'+pat_date_time)
def alice_listaddresses_date_time_exact(self):
return self.alice_listaddresses(
args=['age_fmt=date_time','exact_age=1'],
expect=r'500\s+'+pat_date_time)
def alice_twview(self,args=[],expect=r'500\s+\d+\b'):
t = self.spawn('mmgen-tool',['--alice','twview'] + args)
t.expect(expect,regex=True)
t.read()
return t
def alice_twview_days(self):
return self.alice_twview(args=['age_fmt=days'],expect=r'500\s+\d+\b')
def alice_twview_date(self):
return self.alice_twview(args=['age_fmt=date'],expect=r'500\s+'+pat_date)
def alice_twview_date_time(self):
return self.alice_twview(args=['age_fmt=date_time'],expect=r'500\s+'+pat_date_time)
def alice_twview_date_time_exact(self):
return self.alice_twview(
args=['age_fmt=date_time','exact_age=1'],
expect=r'500\s+'+pat_date_time)
def alice_txcreate_info(self,args=[]):
t = self.spawn('mmgen-txcreate',['--alice','-Bi'])
for e,s in (
(r'500\s+\d+\b','D'),
(r'500\s+\d+\b','D'),
(r'500\s+\d+\b','D'),
(r'500\s+'+pat_date,'w'),
(r'500\s+\d+\s+\d+\s+'+pat_date_time,'x'),
(r'500\s+'+pat_date,'w'),
(r'500\s+\d+\s+\d+\s+'+pat_date_time,'q'),
):
t.expect(e,s,regex=True)
t.read()
return t
def stop(self):
if opt.no_daemon_stop:
self.spawn('',msg_only=True)