From b671453c13ca7d551f9bf42db1e722ba29ec0229 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Sat, 4 Apr 2020 13:18:44 +0000 Subject: [PATCH] txcreate,twview,listaddresses: display transaction date MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- mmgen/altcoins/eth/tw.py | 6 +- mmgen/protocol.py | 2 + mmgen/rpc.py | 23 +++- mmgen/tool.py | 32 +++-- mmgen/tw.py | 165 ++++++++++++++++------- mmgen/util.py | 6 +- test/objattrtest_py_d/oat_btc_mainnet.py | 1 - test/test_py_d/ts_main.py | 2 +- test/test_py_d/ts_regtest.py | 71 ++++++++++ 9 files changed, 241 insertions(+), 67 deletions(-) diff --git a/mmgen/altcoins/eth/tw.py b/mmgen/altcoins/eth/tw.py index 965a8792..8e36c1a6 100755 --- a/mmgen/altcoins/eth/tw.py +++ b/mmgen/altcoins/eth/tw.py @@ -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 diff --git a/mmgen/protocol.py b/mmgen/protocol.py index 823a2ac2..bd8003e3 100755 --- a/mmgen/protocol.py +++ b/mmgen/protocol.py @@ -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' diff --git a/mmgen/rpc.py b/mmgen/rpc.py index a38096ca..536833c4 100755 --- a/mmgen/rpc.py +++ b/mmgen/rpc.py @@ -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' diff --git a/mmgen/tool.py b/mmgen/tool.py index c1260f26..87c54cc4 100755 --- a/mmgen/tool.py +++ b/mmgen/tool.py @@ -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 diff --git a/mmgen/tw.py b/mmgen/tw.py index 43a7e50a..4d0f0439 100755 --- a/mmgen/tw.py +++ b/mmgen/tw.py @@ -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): diff --git a/mmgen/util.py b/mmgen/util.py index f159d7ae..6f4986d3 100755 --- a/mmgen/util.py +++ b/mmgen/util.py @@ -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 diff --git a/test/objattrtest_py_d/oat_btc_mainnet.py b/test/objattrtest_py_d/oat_btc_mainnet.py index 89f7d2a6..d6b3b78f 100755 --- a/test/objattrtest_py_d/oat_btc_mainnet.py +++ b/test/objattrtest_py_d/oat_btc_mainnet.py @@ -116,7 +116,6 @@ tests = { 'addr': (0b001, CoinAddr), 'confs': (0b001, int), 'scriptPubKey': (0b001, HexStr), - 'days': (0b001, int), 'skip': (0b101, str), }, [], diff --git a/test/test_py_d/ts_main.py b/test/test_py_d/ts_main.py index d2130719..f00c9d84 100755 --- a/test/test_py_d/ts_main.py +++ b/test/test_py_d/ts_main.py @@ -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 diff --git a/test/test_py_d/ts_regtest.py b/test/test_py_d/ts_regtest.py index cef9ea0c..fff022bf 100755 --- a/test/test_py_d/ts_regtest.py +++ b/test/test_py_d/ts_regtest.py @@ -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)