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:
parent
de7fba0c19
commit
b671453c13
9 changed files with 241 additions and 67 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
23
mmgen/rpc.py
23
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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
165
mmgen/tw.py
165
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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -116,7 +116,6 @@ tests = {
|
|||
'addr': (0b001, CoinAddr),
|
||||
'confs': (0b001, int),
|
||||
'scriptPubKey': (0b001, HexStr),
|
||||
'days': (0b001, int),
|
||||
'skip': (0b101, str),
|
||||
},
|
||||
[],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue