From d4cae73cf03d1f38458922077b900495d46df2f2 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Mon, 23 May 2022 16:28:57 +0000 Subject: [PATCH] tw.common,tw.unspent: various changes and cleanups --- mmgen/base_proto/bitcoin/tw/unspent.py | 25 +++- mmgen/base_proto/ethereum/tw/unspent.py | 28 ++-- mmgen/data/version | 2 +- mmgen/tool/rpc.py | 15 +- mmgen/tw/common.py | 191 ++++++++++++++---------- mmgen/tw/unspent.py | 56 ++++--- 6 files changed, 190 insertions(+), 127 deletions(-) diff --git a/mmgen/base_proto/bitcoin/tw/unspent.py b/mmgen/base_proto/bitcoin/tw/unspent.py index 0a772e14..b68e18db 100755 --- a/mmgen/base_proto/bitcoin/tw/unspent.py +++ b/mmgen/base_proto/bitcoin/tw/unspent.py @@ -36,13 +36,26 @@ Display options: toggle [D]ays/date, show [g]roup, show [m]mgen addr, 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', - 'q':'a_quit','p':'a_print','v':'a_view','w':'a_view_wide','l':'a_lbl_add' } + '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', + 'q':'a_quit', + 'p':'a_print_detail', + 'v':'a_view', + 'w':'a_view_detail', + 'l':'a_lbl_add' } + col_adj = 38 - display_fs_fs = ' {{n:{cw}}} {{t:{tw}}} {{v:2}} {{a}} {{A}} {{c:<}}' - display_hdr_fs_fs = ' {{n:{cw}}} {{t:{tw}}} {{a}} {{A}} {{c:<}}' - print_fs_fs = ' {{n:4}} {{t:{tw}}} {{a}} {{m}} {{A:{aw}}} {cf}{{b:<8}} {{D:<19}} {{l}}' + squeezed_fs_fs = ' {{n:{cw}}} {{t:{tw}}} {{v:2}} {{a}} {{A}} {{c:<}}' + squeezed_hdr_fs_fs = ' {{n:{cw}}} {{t:{tw}}} {{a}} {{A}} {{c:<}}' + wide_fs_fs = ' {{n:4}} {{t:{tw}}} {{a}} {{m}} {{A:{aw}}} {cf}{{b:<8}} {{D:<19}} {{l}}' async def get_rpc_data(self): # bitcoin-cli help listunspent: diff --git a/mmgen/base_proto/ethereum/tw/unspent.py b/mmgen/base_proto/ethereum/tw/unspent.py index 477ee223..926306fa 100755 --- a/mmgen/base_proto/ethereum/tw/unspent.py +++ b/mmgen/base_proto/ethereum/tw/unspent.py @@ -44,13 +44,22 @@ Actions: [q]uit view, [p]rint to file, pager [v]iew, [w]ide view, add [l]abel, [D]elete address, [R]efresh balance: """ key_mappings = { - 'a':'s_amt','d':'s_addr','r':'d_reverse','M':'s_twmmid', - 'm':'d_mmid','e':'d_redraw', - 'q':'a_quit','p':'a_print','v':'a_view','w':'a_view_wide', - 'l':'a_lbl_add','D':'a_addr_delete','R':'a_balance_refresh' } - display_fs_fs = ' {{n:{cw}}} {{a}} {{A}}' - print_fs_fs = ' {{n:4}} {{a}} {{m}} {{A:{aw}}} {{l}}' - display_hdr_fs_fs = display_fs_fs + 'a':'s_amt', + 'd':'s_addr', + 'r':'d_reverse', + 'M':'s_twmmid', + 'm':'d_mmid', + 'e':'d_redraw', + 'q':'a_quit', + 'p':'a_print_detail', + 'v':'a_view', + 'w':'a_view_detail', + 'l':'a_lbl_add', + 'D':'a_addr_delete', + 'R':'a_balance_refresh' } + + squeezed_fs_fs = squeezed_hdr_fs_fs = ' {{n:{cw}}} {{a}} {{A}}' + wide_fs_fs = ' {{n:4}} {{a}} {{m}} {{A:{aw}}} {{l}}' no_data_errmsg = 'No accounts in tracking wallet!' async def __init__(self,proto,*args,**kwargs): @@ -82,9 +91,8 @@ class EthereumTokenTwUnspentOutputs(EthereumTwUnspentOutputs): prompt_fs = 'Total to spend: {} {}\n\n' col_adj = 37 - display_fs_fs = ' {{n:{cw}}} {{a}} {{A}} {{A2}}' - print_fs_fs = ' {{n:4}} {{a}} {{m}} {{A:{aw}}} {{A2:{aw}}} {{l}}' - display_hdr_fs_fs = display_fs_fs + squeezed_fs_fs = squeezed_hdr_fs_fs = ' {{n:{cw}}} {{a}} {{A}} {{A2}}' + wide_fs_fs = ' {{n:4}} {{a}} {{m}} {{A:{aw}}} {{A2:{aw}}} {{l}}' async def __init__(self,proto,*args,**kwargs): await super().__init__(proto,*args,**kwargs) diff --git a/mmgen/data/version b/mmgen/data/version index e6ba3513..74e58831 100644 --- a/mmgen/data/version +++ b/mmgen/data/version @@ -1 +1 @@ -13.1.0 +13.2.dev1 diff --git a/mmgen/tool/rpc.py b/mmgen/tool/rpc.py index 5976061b..657a6998 100755 --- a/mmgen/tool/rpc.py +++ b/mmgen/tool/rpc.py @@ -97,7 +97,7 @@ class tool_cmd(tool_cmd_base): return await al.format( showbtcaddrs, sort, show_age, age_fmt or 'confs' ) async def twops(self, - obj,pager,reverse,wide,sort,age_fmt,show_mmid,wide_show_confs,interactive): + obj,pager,reverse,detail,sort,age_fmt,interactive,show_mmid): obj.interactive = interactive obj.reverse = reverse @@ -109,10 +109,10 @@ class tool_cmd(tool_cmd_base): if interactive: await obj.view_and_sort() return True - elif wide: - return await obj.format_for_printing( color=True, show_confs=wide_show_confs ) + elif detail: + return await obj.format_detail( color=True ) else: - return await obj.format_for_display() + return await obj.format_squeezed() async def twview(self, pager = False, @@ -121,15 +121,14 @@ class tool_cmd(tool_cmd_base): minconf = 1, sort = 'age', age_fmt: options_annot_str(TwCommon.age_fmts) = 'confs', - show_mmid = True, - wide_show_confs = True, - interactive = False ): + interactive = False, + show_mmid = True ): "view tracking wallet unspent outputs" from ..tw.unspent import TwUnspentOutputs obj = await TwUnspentOutputs(self.proto,minconf=minconf) ret = await self.twops( - obj,pager,reverse,wide,sort,age_fmt,show_mmid,wide_show_confs,interactive) + obj,pager,reverse,wide,sort,age_fmt,interactive,show_mmid) del obj.wallet return ret diff --git a/mmgen/tw/common.py b/mmgen/tw/common.py index d4050b26..6468a1fa 100755 --- a/mmgen/tw/common.py +++ b/mmgen/tw/common.py @@ -25,15 +25,13 @@ import sys,time from ..globalvars import g from ..objmethods import Hilite,InitErrors,MMGenObject from ..obj import TwComment,get_obj,MMGenIdx,MMGenList -from ..color import nocolor,red,yellow,green +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: class TwCommon: - fmt_display = '' - fmt_print = '' cols = None reverse = False group = False @@ -45,6 +43,14 @@ class TwCommon: age_fmts_interactive = ('confs','block','days','date') _age_fmt = 'confs' + age_col_params = { + 'confs': (7, 'Confs'), + 'block': (8, 'Block'), + 'days': (6, 'Age(d)'), + 'date': (8, 'Date'), + 'date_time': (16, 'Date/Time'), + } + date_formatter = { 'days': lambda rpc,secs: (rpc.cur_date - secs) // 86400 if secs else 0, 'date': ( @@ -72,7 +78,8 @@ class TwCommon: lbl_id = ('account','label')['label_api' in self.rpc.caps] - self.data = MMGenList(self.gen_data(rpc_data,lbl_id)) + res = self.gen_data(rpc_data,lbl_id) + self.data = MMGenList(await res if type(res).__name__ == 'coroutine' else res) if not self.data: die(1,self.no_data_errmsg) @@ -88,6 +95,14 @@ class TwCommon: for idx,o in enumerate(us): o.date = dates[idx] + @property + def age_w(self): + return self.age_col_params[self.age_fmt][0] + + @property + def age_hdr(self): + return self.age_col_params[self.age_fmt][1] + @property def age_fmt(self): return self._age_fmt @@ -124,9 +139,17 @@ class TwCommon: else: return min_cols + sort_disp = { + 'addr': 'Addr', + 'age': 'Age', + 'amt': 'Amt', + 'txid': 'TxID', + 'twmmid': 'MMGenID', + } + def sort_info(self,include_group=True): ret = ([],['Reverse'])[self.reverse] - ret.append(capfirst(self.sort_key).replace('Twmmid','MMGenID')) + ret.append(self.sort_disp[self.sort_key]) if include_group and self.group and (self.sort_key in ('addr','txid','twmmid')): ret.append('Grouped') return ret @@ -147,49 +170,54 @@ class TwCommon: assert type(reverse) == bool self.data.sort(key=self.sort_funcs[key],reverse=reverse or self.reverse) - async def format_for_display(self): - data = self.data - if self.has_age and self.age_fmt in self.age_fmts_date_dependent: - await self.set_dates(self.rpc,data) + async def format_squeezed(self,color=True,cached=False): - if not getattr(self,'column_params',None): - self.set_column_params() + if not cached: + data = self.data + if self.has_age and self.age_fmt in self.age_fmts_date_dependent: + await self.set_dates(self.rpc,data) - if self.group and (self.sort_key in ('addr','txid','twmmid')): - for a,b in [(data[i],data[i+1]) for i in range(len(data)-1)]: - for k in ('addr','txid','twmmid'): - if self.sort_key == k and getattr(a,k) == getattr(b,k): - b.skip = (k,'addr')[k=='twmmid'] + if not getattr(self,'column_params',None): + self.set_column_params() - self.fmt_display = ( - self.hdr_fmt.format( - a = ' '.join(self.sort_info()), - b = self.proto.dcoin, - c = self.total.hl() if hasattr(self,'total') else None ) - + ('\nChain: '+green(self.proto.chain_name.upper()) if self.proto.chain_name != 'mainnet' else '') - + '\n' + '\n'.join(self.gen_display_output(self.column_params)) - + '\n' - ) + if self.group and (self.sort_key in ('addr','txid','twmmid')): + for a,b in [(data[i],data[i+1]) for i in range(len(data)-1)]: + for k in ('addr','txid','twmmid'): + if self.sort_key == k and getattr(a,k) == getattr(b,k): + b.skip = (k,'addr')[k=='twmmid'] - return self.fmt_display + self._format_squeezed_display_data = ( + self.hdr_fmt.format( + a = ' '.join(self.sort_info()), + b = self.proto.dcoin, + c = self.total.hl() if hasattr(self,'total') else None ) + + '\nNetwork: {}'.format((nocolor,green)[color]( + self.proto.coin + ' ' + + self.proto.chain_name.upper() )) + + '\n' + '\n'.join(self.gen_squeezed_display(self.column_params,color=color)) + + '\n' + ) - async def format_for_printing(self,color=False,show_confs=True): + return self._format_squeezed_display_data + + async def format_detail(self,color): if self.has_age: await self.set_dates(self.rpc,self.data) - self.fmt_print = self.print_hdr_fs.format( + sep = self.detail_display_separator + + return self.print_hdr_fs.format( a = capfirst(self.desc), b = self.rpc.blockcount, c = make_timestr(self.rpc.cur_date), - d = ('' if self.proto.chain_name == 'mainnet' else - 'Chain: {}\n'.format((nocolor,green)[color](self.proto.chain_name.upper())) ), + d = 'Network: {}\n'.format((nocolor,green)[color]( + self.proto.coin + ' ' + + self.proto.chain_name.upper() )), e = ' '.join(self.sort_info(include_group=False)), - f = '\n'.join(self.gen_print_output(color,show_confs)), + f = sep.join(self.gen_detail_display(color)), g = self.proto.dcoin, h = self.total.hl(color=color) if hasattr(self,'total') else None ) - return self.fmt_print - async def view_and_sort(self): from ..opts import opt from ..term import get_char @@ -201,10 +229,10 @@ class TwCommon: ERASE_ALL = '\033[0J' while True: - msg_r('' if self.no_output else '\n\n' if opt.no_blank else CUR_HOME+ERASE_ALL) + msg_r('' if self.no_output else '\n\n' if (opt.no_blank or g.test_suite) else CUR_HOME+ERASE_ALL) reply = get_char( '' if self.no_output else ( - await self.format_for_display() + await self.format_squeezed() + '\n' + (self.oneshot_msg or '') + self.prompt @@ -219,67 +247,73 @@ class TwCommon: continue action = self.key_mappings[reply] - if action.startswith('s_'): + if hasattr(self.action,action): + await self.action().run(self,action) + elif action.startswith('s_'): # put here to allow overriding by action method self.do_sort(action[2:]) - if action == 's_twmmid': - self.show_mmid = True - elif action == 'd_days': - af = self.age_fmts_interactive - self.age_fmt = af[(af.index(self.age_fmt) + 1) % len(af)] - if self.update_params_on_age_toggle: - self.set_column_params() - 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_redraw': + elif hasattr(self.item_action,action): + await self.item_action().run(self,action) self.set_column_params() - elif action == 'd_reverse': - self.data.reverse() - self.reverse = not self.reverse elif action == 'a_quit': msg('') return self.data - elif hasattr(self.action,action): - await self.action(self).run(action) - elif hasattr(self.item_action,action): - await self.item_action(self).run(action) - self.set_column_params() class action: - def __init__(self,parent): - self.parent = parent + async def run(self,parent,action): + ret = getattr(self,action)(parent) + if type(ret).__name__ == 'coroutine': + await ret - async def run(self,action): - await getattr(self,action)(self.parent) + def d_days(self,parent): + af = parent.age_fmts_interactive + parent.age_fmt = af[(af.index(parent.age_fmt) + 1) % len(af)] + if parent.update_params_on_age_toggle: + parent.set_column_params() - async def a_print(self,parent): - outfile = '{}-{}{}[{}].out'.format( + def d_redraw(self,parent): + parent.set_column_params() + + def d_reverse(self,parent): + parent.data.reverse() + parent.reverse = not parent.reverse + + async def a_print_detail(self,parent): + return await self._print(parent,output_type='detail') + + async def a_print_squeezed(self,parent): + return await self._print(parent,output_type='squeezed') + + async def _print(self,parent,output_type): + outfile = '{}{}-{}{}[{}].out'.format( parent.dump_fn_pfx, + f'-{output_type}' if len(parent.print_output_types) > 1 else '', parent.proto.dcoin, ('' if parent.proto.network == 'mainnet' else '-'+parent.proto.network.upper()), - ','.join(parent.sort_info(include_group=False)).lower() ) + ','.join(parent.sort_info(include_group=False)).replace(' ','') ) msg('') from ..fileutil import write_data_to_file from ..exception import UserNonConfirmation + hdr = { + 'squeezed': f'[screen print truncated to width {parent.cols}]\n', + 'detail': '', + }[output_type] try: write_data_to_file( - outfile, - await parent.format_for_printing(color=False), - desc = f'{parent.desc} listing' ) + outfile = outfile, + data = hdr + await getattr(parent,f'format_{output_type}')(color=False), + desc = f'{parent.desc} listing' ) except UserNonConfirmation as e: - parent.oneshot_msg = red(f'File {outfile!r} not overwritten by user request\n\n') + parent.oneshot_msg = yellow(f'File {outfile!r} not overwritten by user request\n\n') else: - parent.oneshot_msg = yellow(f'Data written to {outfile!r}\n\n') + parent.oneshot_msg = green(f'Data written to {outfile!r}\n\n') async def a_view(self,parent): - do_pager(parent.fmt_display) + do_pager( await parent.format_squeezed(color=True,cached=True) ) self.post_view(parent) - async def a_view_wide(self,parent): - do_pager( await parent.format_for_printing(color=True) ) + async def a_view_detail(self,parent): + do_pager( await parent.format_detail(color=True) ) self.post_view(parent) def post_view(self,parent): @@ -290,19 +324,16 @@ class TwCommon: class item_action: - def __init__(self,parent): - self.parent = parent - - async def run(self,action): + async def run(self,parent,action): msg('') while True: - ret = line_input(f'Enter {self.parent.item_desc} number (or RETURN to return to main menu): ') + ret = line_input(f'Enter {parent.item_desc} number (or RETURN to return to main menu): ') if ret == '': return None idx = get_obj(MMGenIdx,n=ret,silent=True) - if not idx or idx < 1 or idx > len(self.parent.data): - msg(f'Choice must be a single number between 1 and {len(self.parent.data)}') - elif (await getattr(self,action)(self.parent,idx)) != 'redo': + if not idx or idx < 1 or idx > len(parent.data): + msg(f'Choice must be a single number between 1 and {len(parent.data)}') + elif (await getattr(self,action)(parent,idx)) != 'redo': break class TwMMGenID(str,Hilite,InitErrors,MMGenObject): diff --git a/mmgen/tw/unspent.py b/mmgen/tw/unspent.py index 9d3c06e9..1354a90d 100755 --- a/mmgen/tw/unspent.py +++ b/mmgen/tw/unspent.py @@ -54,6 +54,8 @@ class TwUnspentOutputs(MMGenObject,TwCommon,metaclass=AsyncInit): watch-only wallet using 'mmgen-addrimport' and then re-run this program. """ update_params_on_age_toggle = False + detail_display_separator = '\n' + print_output_types = ('detail',) class MMGenTwUnspentOutput(MMGenListItem): txid = ListItemAttr(CoinTxID) @@ -136,23 +138,16 @@ class TwUnspentOutputs(MMGenObject,TwCommon,metaclass=AsyncInit): ['col1_w','mmid_w','addr_w','btaddr_w','label_w','tx_w','txdots'] )(col1_w, mmid_w, addr_w, btaddr_w, label_w, tx_w, txdots) - def gen_display_output(self,c): - fs = self.display_fs_fs.format( cw=c.col1_w, tw=c.tx_w ) - hdr_fs = self.display_hdr_fs_fs.format( cw=c.col1_w, tw=c.tx_w ) + def gen_squeezed_display(self,c,color): + fs = self.squeezed_fs_fs.format( cw=c.col1_w, tw=c.tx_w ) + hdr_fs = self.squeezed_hdr_fs_fs.format( cw=c.col1_w, tw=c.tx_w ) yield hdr_fs.format( n = 'Num', t = 'TXid'.ljust(c.tx_w - 2) + ' Vout', a = 'Address'.ljust(c.addr_w), A = f'Amt({self.proto.dcoin})'.ljust(self.disp_prec+5), A2 = f' Amt({self.proto.coin})'.ljust(self.disp_prec+4), - c = { - 'confs': 'Confs', - 'block': 'Block', - 'days': 'Age(d)', - 'date': 'Date', - 'date_time': 'Date', - }[self.age_fmt], - ).rstrip() + c = self.age_hdr ).rstrip() for n,i in enumerate(self.data): addr_dots = '|' + '.'*(c.addr_w-1) @@ -163,20 +158,20 @@ class TwUnspentOutputs(MMGenObject,TwCommon,metaclass=AsyncInit): f'Non-{g.proj_name}' ), width = c.mmid_w, - color = True ) + color = color ) if self.show_mmid: addr_out = '{} {}{}'.format(( - type(i.addr).fmtc(addr_dots,width=c.btaddr_w,color=True) if i.skip == 'addr' else - i.addr.fmt(width=c.btaddr_w,color=True) + type(i.addr).fmtc(addr_dots,width=c.btaddr_w,color=color) if i.skip == 'addr' else + i.addr.fmt(width=c.btaddr_w,color=color) ), mmid_disp, - (' ' + i.label.fmt(width=c.label_w,color=True)) if c.label_w > 0 else '' + (' ' + i.label.fmt(width=c.label_w,color=color)) if c.label_w > 0 else '' ) else: addr_out = ( - type(i.addr).fmtc(addr_dots,width=c.addr_w,color=True) if i.skip=='addr' else - i.addr.fmt(width=c.addr_w,color=True) ) + type(i.addr).fmtc(addr_dots,width=c.addr_w,color=color) if i.skip=='addr' else + i.addr.fmt(width=c.addr_w,color=color) ) yield fs.format( n = str(n+1)+')', @@ -186,18 +181,21 @@ class TwUnspentOutputs(MMGenObject,TwCommon,metaclass=AsyncInit): i.txid[:c.tx_w-len(c.txdots)] + c.txdots ), v = i.vout, 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 ''), + A = i.amt.fmt(color=color,prec=self.disp_prec), + A2 = (i.amt2.fmt(color=color,prec=self.disp_prec) if i.amt2 is not None else ''), c = self.age_disp(i,self.age_fmt), ).rstrip() - def gen_print_output(self,color,show_confs): + def gen_detail_display(self,color): + addr_w = max(len(i.addr) for i in self.data) mmid_w = max(len(('',i.twmmid)[i.twmmid.type=='mmgen']) for i in self.data) or 12 # DEADBEEF:S:1 - fs = self.print_fs_fs.format( + + fs = self.wide_fs_fs.format( tw = self.txid_w + 3, - cf = '{c:<8} ' if show_confs else '', + cf = '{c:<8} ', aw = self.proto.coin_amt.max_prec + 5 ) + yield fs.format( n = 'Num', t = 'Tx ID,Vout', @@ -211,6 +209,7 @@ class TwUnspentOutputs(MMGenObject,TwCommon,metaclass=AsyncInit): l = 'Label' ) max_lbl_len = max([len(i.label) for i in self.data if i.label] or [2]) + for n,i in enumerate(self.data): yield fs.format( n = str(n+1) + ')', @@ -244,6 +243,19 @@ class TwUnspentOutputs(MMGenObject,TwCommon,metaclass=AsyncInit): len(self.data), suf(self.data) )) + class action(TwCommon.action): + + def s_twmmid(self,parent): + parent.do_sort('twmmid') + parent.show_mmid = True + + def d_mmid(self,parent): + parent.show_mmid = not parent.show_mmid + + def d_group(self,parent): + if parent.can_group: + parent.group = not parent.group + class item_action(TwCommon.item_action): async def a_balance_refresh(self,uo,idx):