From b21864fd082f9f65dfcb0bf5a46da9374f2eab80 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Wed, 9 Nov 2022 13:05:09 +0000 Subject: [PATCH] mmgen.tw.{ctl,common,txhistory,unspent}: cleanups --- mmgen/addrdata.py | 6 +- mmgen/obj.py | 4 +- mmgen/proto/btc/tw/bal.py | 3 +- mmgen/proto/btc/tw/common.py | 7 +- mmgen/proto/btc/tw/ctl.py | 7 +- mmgen/proto/eth/tw/bal.py | 3 +- mmgen/proto/eth/tw/ctl.py | 17 ++- mmgen/proto/eth/tw/unspent.py | 2 +- mmgen/tool/rpc.py | 7 +- mmgen/tw/common.py | 129 +++++++++++++++------- mmgen/tw/ctl.py | 59 +++++----- mmgen/tw/txhistory.py | 58 +++++----- mmgen/tw/unspent.py | 75 ++----------- mmgen/tx/new.py | 2 +- test/objtest_py_d/ot_btc_mainnet.py | 7 +- test/overlay/fakemods/mmgen/tw/common.py | 8 -- test/overlay/fakemods/mmgen/tw/unspent.py | 12 ++ test/test_py_d/ts_ethdev.py | 44 +++++--- test/test_py_d/ts_regtest.py | 17 +-- 19 files changed, 247 insertions(+), 220 deletions(-) create mode 100644 test/overlay/fakemods/mmgen/tw/unspent.py diff --git a/mmgen/addrdata.py b/mmgen/addrdata.py index 51e2f9ce..8ade8a8b 100755 --- a/mmgen/addrdata.py +++ b/mmgen/addrdata.py @@ -97,4 +97,8 @@ class TwAddrData(AddrData,metaclass=AsyncInit): vmsg(f'{i} {g.proj_name} addresses found, {len(twd)} accounts total') for al_id in out: - self.add(AddrList(self.proto,al_id=al_id,adata=AddrListData(sorted(out[al_id],key=lambda a: a.idx)))) + self.add(AddrList( + self.proto, + al_id = al_id, + adata = AddrListData(sorted( out[al_id], key=lambda a: a.idx )) + )) diff --git a/mmgen/obj.py b/mmgen/obj.py index 320b3e0e..db67531f 100755 --- a/mmgen/obj.py +++ b/mmgen/obj.py @@ -46,8 +46,8 @@ def get_obj(objname,*args,**kwargs): ret = objname(**kwargs) except Exception as e: if not silent: - from .util import msg - msg(f'{e!s}') + from .util import rmsg + rmsg(f'{e!s}') return False else: return True if return_bool else ret diff --git a/mmgen/proto/btc/tw/bal.py b/mmgen/proto/btc/tw/bal.py index 24c85322..14454fd4 100755 --- a/mmgen/proto/btc/tw/bal.py +++ b/mmgen/proto/btc/tw/bal.py @@ -22,13 +22,14 @@ class BitcoinTwGetBalance(TwGetBalance): async def create_data(self): # 0: unconfirmed, 1: below minconf, 2: confirmed, 3: spendable (privkey in wallet) lbl_id = ('account','label')['label_api' in self.rpc.caps] + amt0 = self.proto.coin_amt('0') for d in await self.rpc.call('listunspent',0): lbl = get_tw_label(self.proto,d[lbl_id]) if lbl: if lbl.mmid.type == 'mmgen': key = lbl.mmid.obj.sid if key not in self.data: - self.data[key] = [self.proto.coin_amt('0')] * 4 + self.data[key] = [amt0] * 4 else: key = 'Non-MMGen' else: diff --git a/mmgen/proto/btc/tw/common.py b/mmgen/proto/btc/tw/common.py index 818cfc51..a01b2589 100755 --- a/mmgen/proto/btc/tw/common.py +++ b/mmgen/proto/btc/tw/common.py @@ -9,7 +9,7 @@ # https://gitlab.com/mmgen/mmgen """ -proto.btc.tw: Bitcoin base protocol tracking wallet dependency classes +proto.btc.tw.common: Bitcoin base protocol tracking wallet dependency classes """ from ....addr import CoinAddr @@ -26,7 +26,7 @@ class BitcoinTwCommon: """ def check_dup_mmid(acct_labels): mmid_prev,err = None,False - for mmid in sorted(a.mmid for a in acct_labels if a): + for mmid in sorted(label.mmid for label in acct_labels if label): if mmid == mmid_prev: err = True msg(f'Duplicate MMGen ID ({mmid}) discovered in tracking wallet!\n') @@ -72,6 +72,7 @@ class BitcoinTwCommon: """ data = {} lbl_id = ('account','label')['label_api' in self.rpc.caps] + amt0 = self.proto.coin_amt('0') for d in await self.rpc.call('listunspent',0): @@ -99,7 +100,7 @@ class BitcoinTwCommon: lm.vout = d['vout'] lm.date = None data[lm] = { - 'amt': self.proto.coin_amt('0'), + 'amt': amt0, 'lbl': label, 'addr': CoinAddr(self.proto,d['address']) } amt = self.proto.coin_amt(d['amount']) diff --git a/mmgen/proto/btc/tw/ctl.py b/mmgen/proto/btc/tw/ctl.py index 840ee359..89d845bc 100755 --- a/mmgen/proto/btc/tw/ctl.py +++ b/mmgen/proto/btc/tw/ctl.py @@ -40,10 +40,11 @@ class BitcoinTrackingWallet(TrackingWallet): raise NotImplementedError(f'address removal not implemented for coin {self.proto.coin}') @write_mode - async def set_comment(self,coinaddr,lbl): + async def set_label(self,coinaddr,lbl): args = self.rpc.daemon.set_comment_args( self.rpc, coinaddr, lbl ) try: - return await self.rpc.call(*args) + await self.rpc.call(*args) + return True except Exception as e: rmsg(e.args[0]) return False @@ -91,7 +92,7 @@ class BitcoinTrackingWallet(TrackingWallet): @write_mode async def rescan_address(self,addrspec): - res = await self.resolve_address(addrspec,None) + res = await self.resolve_address(addrspec) if not res: return False return await self.rescan_addresses([res.coinaddr]) diff --git a/mmgen/proto/eth/tw/bal.py b/mmgen/proto/eth/tw/bal.py index 2a1dd749..eeb4f6db 100755 --- a/mmgen/proto/eth/tw/bal.py +++ b/mmgen/proto/eth/tw/bal.py @@ -33,11 +33,12 @@ class EthereumTwGetBalance(TwGetBalance): async def create_data(self): data = self.wallet.mmid_ordered_dict + amt0 = self.proto.coin_amt('0') for d in data: if d.type == 'mmgen': key = d.obj.sid if key not in self.data: - self.data[key] = [self.proto.coin_amt('0')] * 4 + self.data[key] = [amt0] * 4 else: key = 'Non-MMGen' diff --git a/mmgen/proto/eth/tw/ctl.py b/mmgen/proto/eth/tw/ctl.py index 046a1b23..8ef0f75c 100755 --- a/mmgen/proto/eth/tw/ctl.py +++ b/mmgen/proto/eth/tw/ctl.py @@ -127,12 +127,12 @@ class EthereumTrackingWallet(TrackingWallet): return None @write_mode - async def set_comment(self,coinaddr,lbl): + async def set_label(self,coinaddr,lbl): for addr,d in list(self.data_root.items()): if addr == coinaddr: d['comment'] = lbl.comment self.write() - return None + return True else: msg(f'Address {coinaddr!r} not found in {self.data_root_desc!r} section of tracking wallet') return False @@ -156,6 +156,19 @@ class EthereumTrackingWallet(TrackingWallet): return self.data['tokens'][token]['params'].get(param) return None + @property + def sorted_list(self): + return sorted( + [ { 'addr':x[0], + 'mmid':x[1]['mmid'], + 'comment':x[1]['comment'] } + for x in self.data_root.items() if x[0] not in ('params','coin') ], + key=lambda x: x['mmid'].sort_key+x['addr'] ) + + @property + def mmid_ordered_dict(self): + return dict((x['mmid'],{'addr':x['addr'],'comment':x['comment']}) for x in self.sorted_list) + class EthereumTokenTrackingWallet(EthereumTrackingWallet): desc = 'Ethereum token tracking wallet' diff --git a/mmgen/proto/eth/tw/unspent.py b/mmgen/proto/eth/tw/unspent.py index d3f1cd96..14cb0683 100755 --- a/mmgen/proto/eth/tw/unspent.py +++ b/mmgen/proto/eth/tw/unspent.py @@ -42,7 +42,7 @@ class EthereumTwUnspentOutputs(TwUnspentOutputs): Sort options: [a]mount, a[d]dress, [r]everse, [M]mgen addr Display options: show [m]mgen addr, r[e]draw screen Actions: [q]uit view, [p]rint to file, pager [v]iew, [w]ide view, - add [l]abel, [D]elete address, [R]efresh balance: + [D]elete address, add [l]abel, [R]efresh balance: """ key_mappings = { 'a':'s_amt', diff --git a/mmgen/tool/rpc.py b/mmgen/tool/rpc.py index 47bb1ec2..7bf9e899 100755 --- a/mmgen/tool/rpc.py +++ b/mmgen/tool/rpc.py @@ -110,7 +110,7 @@ class tool_cmd(tool_cmd_base): await obj.get_data(sort_key=sort,reverse_sort=reverse) if interactive: - await obj.view_and_sort() + await obj.view_filter_and_sort() return True else: return await obj.format('detail' if detail else 'squeezed') @@ -151,8 +151,7 @@ class tool_cmd(tool_cmd_base): async def add_label(self,mmgen_or_coin_addr:str,label:str): "add descriptive label for address in tracking wallet" from ..tw.ctl import TrackingWallet - await (await TrackingWallet(self.proto,mode='w')).add_comment( mmgen_or_coin_addr, label, on_fail='raise' ) - return True + return await (await TrackingWallet(self.proto,mode='w')).set_comment(mmgen_or_coin_addr,label) async def remove_label(self,mmgen_or_coin_addr:str): "remove descriptive label for address in tracking wallet" @@ -176,7 +175,7 @@ class tool_cmd(tool_cmd_base): if ret: from ..util import Msg from ..addr import is_coin_addr - return ret.mmaddr if is_coin_addr(self.proto,mmgen_or_coin_addr) else ret.coinaddr + return ret.twmmid if is_coin_addr(self.proto,mmgen_or_coin_addr) else ret.coinaddr else: return False diff --git a/mmgen/tw/common.py b/mmgen/tw/common.py index d9b7ee98..7bd6d0dd 100755 --- a/mmgen/tw/common.py +++ b/mmgen/tw/common.py @@ -92,9 +92,9 @@ class TwCommon: def age_disp(self,o,age_fmt): if age_fmt == 'confs': - return o.confs + return o.confs or '-' elif age_fmt == 'block': - return self.rpc.blockcount - (o.confs - 1) + return self.rpc.blockcount + 1 - o.confs if o.confs else '-' else: return self.date_formatter[age_fmt](self.rpc,o.date) @@ -109,21 +109,13 @@ class TwCommon: res = self.gen_data(rpc_data,lbl_id) self.data = MMGenList(await res if type(res).__name__ == 'coroutine' else res) + self.disp_data = list(self.filter_data()) if not self.data: die(1,self.no_data_errmsg) self.do_sort(key=sort_key,reverse=reverse_sort) - async def set_dates(self,us): - if not self.dates_set: - # 'blocktime' differs from 'time', is same as getblockheader['time'] - dates = [ o.get('blocktime',0) - for o in await self.rpc.gathered_icall('gettransaction',[(o.txid,True,False) for o in us]) ] - for idx,o in enumerate(us): - o.date = dates[idx] - self.dates_set = True - @property def age_w(self): return self.age_col_params[self.age_fmt][0] @@ -258,10 +250,10 @@ class TwCommon: def header(self,color): Blue,Green = (blue,green) if color else (nocolor,nocolor) - yes,no = green('yes'),red('no') if color else ('yes','no') + Yes,No = (green('yes'),red('no')) if color else ('yes','no') def fmt_filter(k): - return '{}:{}'.format(k,yes if getattr(self,k) else no) + return '{}:{}'.format(k,{0:No,1:Yes}[getattr(self,k)]) return '{h} (sort order: {s}){f}\nNetwork: {n}\nBlock {b} [{d}]\n{t}'.format( h = self.hdr_lbl.upper(), @@ -277,29 +269,26 @@ class TwCommon: return '' def filter_data(self): - return self.data + return self.data.copy() async def format(self,display_type,color=True,cached=False,interactive=False): if not cached: - data = list(self.filter_data()) # method could be a generator + dt = getattr(self.display_type,display_type) - if data: + if self.has_age and (self.age_fmt in self.age_fmts_date_dependent or dt.detail): + await self.set_dates(self.data) - dt = getattr(self.display_type,display_type) + data = self.disp_data = list(self.filter_data()) # method could be a generator - cw = self.get_column_widths(data,wide=dt.detail) if dt.need_column_widths else None + cw = self.get_column_widths(data,wide=dt.detail) if data and dt.need_column_widths else None - if self.has_age and (self.age_fmt in self.age_fmts_date_dependent or dt.detail): - await self.set_dates(data) - - self._display_data[display_type] = ( - self.header(color) + self.subheader(color) + '\n' - + ( - dt.item_separator.join(getattr(self,dt.fmt_method)(data,cw,color=color)) + '\n' - if data else (nocolor,yellow)[color]('[no data for requested parameters]') + '\n' - ) + self._display_data[display_type] = '{a}{b}\n{c}\n'.format( + a = self.header(color), + b = self.subheader(color), + c = dt.item_separator.join(getattr(self,dt.fmt_method)(data,cw,color=color)) + if data else (nocolor,yellow)[color]('[no data for requested parameters]') ) return self._display_data[display_type] + ('' if interactive else self.footer(color)) @@ -310,7 +299,7 @@ class TwCommon: self.proto.dcoin ) if hasattr(self,'total') else '' - async def view_and_sort(self): + async def view_filter_and_sort(self): from ..opts import opt from ..term import get_char prompt = self.prompt.strip() + '\b' @@ -351,7 +340,7 @@ class TwCommon: await self.item_action().run(self,action) elif action == 'a_quit': msg('') - return self.data + return self.disp_data class action: @@ -421,15 +410,75 @@ class TwCommon: msg('') from ..ui import line_input while True: - ret = line_input(f'Enter {parent.item_desc} number (or RETURN to return to main menu): ') + ret = line_input(f'Enter {parent.item_desc} number (or ENTER 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(parent.data): - msg(f'Choice must be a single number between 1 and {len(parent.data)}') + if not idx or idx < 1 or idx > len(parent.disp_data): + msg(f'Choice must be a single number between 1 and {len(parent.disp_data)}') elif (await getattr(self,action)(parent,idx)) != 'redo': break + async def a_balance_refresh(self,parent,idx): + from ..ui import keypress_confirm + if not keypress_confirm( + f'Refreshing tracking wallet {parent.item_desc} #{idx}. Is this what you want?'): + return 'redo' + await parent.wallet.get_balance( parent.disp_data[idx-1].addr, force_rpc=True ) + await parent.get_data() + parent.oneshot_msg = yellow(f'{parent.proto.dcoin} balance for account #{idx} refreshed\n\n') + + async def a_addr_delete(self,parent,idx): + from ..ui import keypress_confirm + if not keypress_confirm( + 'Removing {} {} from tracking wallet. Is this what you want?'.format( + parent.item_desc, red(f'#{idx}') )): + return 'redo' + if await parent.wallet.remove_address( parent.disp_data[idx-1].addr ): + await parent.get_data() + parent.oneshot_msg = yellow(f'{capfirst(parent.item_desc)} #{idx} removed\n\n') + else: + await asyncio.sleep(3) + parent.oneshot_msg = red('Address could not be removed\n\n') + + async def a_comment_add(self,parent,idx): + + async def do_comment_add(comment): + if await parent.wallet.set_comment( entry.twmmid, comment, entry.addr ): + await parent.get_data() + parent.oneshot_msg = yellow('Label {a} {b}{c}\n\n'.format( + a = 'for' if cur_comment and comment else 'added to' if comment else 'removed from', + b = desc, + c = ' edited' if cur_comment and comment else '' )) + return True + else: + await asyncio.sleep(3) + parent.oneshot_msg = red('Label for {desc} could not be {action}\n\n'.format( + desc = desc, + action = 'edited' if cur_comment and comment else 'added' if comment else 'removed' + )) + return False + + entry = parent.disp_data[idx-1] + desc = f'{parent.item_desc} #{idx}' + cur_comment = parent.disp_data[idx-1].comment + msg('Current label: {}'.format(cur_comment.hl() if cur_comment else '(none)')) + + from ..ui import line_input + res = line_input( + 'Enter label text for {} {}: '.format(parent.item_desc,red(f'#{idx}')), + insert_txt = cur_comment ) + + if res == cur_comment: + parent.oneshot_msg = green(f'Label for {desc} unchanged\n\n') + return None + elif res == '': + from ..ui import keypress_confirm + if not keypress_confirm(f'Removing label for {desc}. Is this what you want?'): + return None + + return await do_comment_add(res) + class TwMMGenID(str,Hilite,InitErrors,MMGenObject): color = 'orange' width = 0 @@ -437,22 +486,24 @@ class TwMMGenID(str,Hilite,InitErrors,MMGenObject): def __new__(cls,proto,id_str): if type(id_str) == cls: return id_str - ret = None try: - ret = MMGenID(proto,id_str) - sort_key,idtype = ret.sort_key,'mmgen' + ret = addr = disp = MMGenID(proto,id_str) + sort_key,idtype = (ret.sort_key,'mmgen') except Exception as e: try: - assert id_str.split(':',1)[0] == proto.base_coin.lower(),( + coin,addr = id_str.split(':',1) + assert coin == proto.base_coin.lower(),( f'not a string beginning with the prefix {proto.base_coin.lower()!r}:' ) - assert id_str.isascii() and id_str[4:].isalnum(), 'not an ASCII alphanumeric string' - assert len(id_str) > 4,'not more that four characters long' - ret,sort_key,idtype = str(id_str),'z_'+id_str,'non-mmgen' + assert addr.isascii() and addr.isalnum(), 'not an ASCII alphanumeric string' + ret,sort_key,idtype,disp = (id_str,'z_'+id_str,'non-mmgen','non-MMGen') + addr = proto.coin_addr(addr) except Exception as e2: return cls.init_fail(e,id_str,e2=e2) me = str.__new__(cls,ret) me.obj = ret + me.disp = disp + me.addr = addr me.sort_key = sort_key me.type = idtype me.proto = proto diff --git a/mmgen/tw/ctl.py b/mmgen/tw/ctl.py index 2d54db4b..673cf174 100755 --- a/mmgen/tw/ctl.py +++ b/mmgen/tw/ctl.py @@ -38,6 +38,8 @@ from ..addr import CoinAddr,is_mmgen_id,is_coin_addr from ..rpc import rpc_init from .common import TwMMGenID,TwLabel +addr_info = namedtuple('addr_info',['twmmid','coinaddr']) + # decorator for TrackingWallet def write_mode(orig_func): def f(self,*args,**kwargs): @@ -179,19 +181,6 @@ class TrackingWallet(MMGenObject,metaclass=AsyncInit): self.cache_balance(addr,ret,self.cur_balances,self.data_root) return ret - @property - def sorted_list(self): - return sorted( - [ { 'addr':x[0], - 'mmid':x[1]['mmid'], - 'comment':x[1]['comment'] } - for x in self.data_root.items() if x[0] not in ('params','coin') ], - key=lambda x: x['mmid'].sort_key+x['addr'] ) - - @property - def mmid_ordered_dict(self): - return dict((x['mmid'],{'addr':x['addr'],'comment':x['comment']}) for x in self.sorted_list) - def force_write(self): mode_save = self.mode self.mode = 'w' @@ -261,45 +250,49 @@ class TrackingWallet(MMGenObject,metaclass=AsyncInit): if not mmaddr: mmaddr = f'{self.proto.base_coin.lower()}:{coinaddr}' - return namedtuple('addr_info',['mmaddr','coinaddr'])( - TwMMGenID(self.proto,mmaddr), - coinaddr ) + return addr_info( TwMMGenID(self.proto,mmaddr), coinaddr ) # returns on failure @write_mode - async def add_comment(self,addrspec,comment='',coinaddr=None,silent=False,on_fail='return'): - assert on_fail in ('return','raise'), 'add_comment_chk1' + async def set_comment(self,addrspec,comment='',trusted_coinaddr=None,silent=False): + + res = ( + addr_info(addrspec,trusted_coinaddr) if trusted_coinaddr + else await self.resolve_address(addrspec) ) - res = await self.resolve_address(addrspec,coinaddr) if not res: return False - cmt = TwComment(comment) if on_fail=='raise' else get_obj(TwComment,s=comment) - if cmt in (False,None): + comment = get_obj(TwComment,s=comment) + + if comment == False: return False - lbl_txt = res.mmaddr + (' ' + cmt if cmt else '') - lbl = ( - TwLabel(self.proto,lbl_txt) if on_fail == 'raise' else - get_obj(TwLabel,proto=self.proto,text=lbl_txt) ) + lbl = get_obj( + TwLabel, + proto = self.proto, + text = res.twmmid + (' ' + comment if comment else '')) - if await self.set_comment(res.coinaddr,lbl) == False: - if not silent: - msg( 'Label could not be {}'.format('added' if comment else 'removed') ) + if lbl == False: return False - else: + + if await self.set_label(res.coinaddr,lbl): desc = '{} address {} in tracking wallet'.format( - res.mmaddr.type.replace('mmgen','MMGen'), - res.mmaddr.replace(self.proto.base_coin.lower()+':','') ) + res.twmmid.type.replace('mmgen','MMGen'), + res.twmmid.addr.hl() ) if comment: - msg(f'Added label {comment!r} to {desc}') + msg('Added label {} to {}'.format(comment.hl(encl="''"),desc)) else: msg(f'Removed label from {desc}') return True + else: + if not silent: + msg( 'Label could not be {}'.format('added' if comment else 'removed') ) + return False @write_mode async def remove_comment(self,mmaddr): - await self.add_comment(mmaddr,'') + await self.set_comment(mmaddr,'') async def import_address_common(self,data,batch=False,gather=False): diff --git a/mmgen/tw/txhistory.py b/mmgen/tw/txhistory.py index 5bd7d8e2..eac5df59 100755 --- a/mmgen/tw/txhistory.py +++ b/mmgen/tw/txhistory.py @@ -43,7 +43,6 @@ class TwTxHistory(MMGenObject,TwCommon,metaclass=AsyncInit): async def __init__(self,proto,sinceblock=0): self.proto = proto - self.data = MMGenList() self.rpc = await rpc_init(proto) self.sinceblock = Int( sinceblock if sinceblock >= 0 else self.rpc.blockcount + sinceblock ) @@ -52,6 +51,9 @@ class TwTxHistory(MMGenObject,TwCommon,metaclass=AsyncInit): return 'No transaction history {}found!'.format( f'from block {self.sinceblock} ' if self.sinceblock else '') + def filter_data(self): + return (d for d in self.data if d.confirmations > 0 or self.show_unconfirmed) + def get_column_widths(self,data,wide=False): # var cols: addr1 addr2 comment [txid] @@ -116,18 +118,15 @@ class TwTxHistory(MMGenObject,TwCommon,metaclass=AsyncInit): a2 = 'Outputs', l = 'Comment' ).rstrip() - n = 0 - for d in data: - if d.confirmations > 0 or self.show_unconfirmed: - n += 1 - yield fs.format( - n = str(n) + ')', - i = d.txid_disp( width=cw.txid, color=color ) if hasattr(cw,'txid') else None, - d = d.age_disp( self.age_fmt, width=self.age_w, color=color ), - a1 = d.vouts_disp( 'inputs', width=cw.addr1, color=color ), - A = d.amt_disp(self.show_total_amt).fmt( prec=self.disp_prec, color=color ), - a2 = d.vouts_disp( 'outputs', width=cw.addr2, color=color ), - l = d.comment.fmt( width=cw.comment, color=color ) ).rstrip() + for n,d in enumerate(data,1): + yield fs.format( + n = str(n) + ')', + i = d.txid_disp( width=cw.txid, color=color ) if hasattr(cw,'txid') else None, + d = d.age_disp( self.age_fmt, width=self.age_w, color=color ), + a1 = d.vouts_disp( 'inputs', width=cw.addr1, color=color ), + A = d.amt_disp(self.show_total_amt).fmt( prec=self.disp_prec, color=color ), + a2 = d.vouts_disp( 'outputs', width=cw.addr2, color=color ), + l = d.comment.fmt( width=cw.comment, color=color ) ).rstrip() def gen_detail_display(self,data,cw,color): @@ -150,23 +149,20 @@ class TwTxHistory(MMGenObject,TwCommon,metaclass=AsyncInit): {a2} """,strip_char='\t').strip() - n = 0 - for d in data: - if d.confirmations > 0 or self.show_unconfirmed: - n += 1 - yield fs.format( - n = str(n) + ')', - d = d.age_disp( 'date_time', width=None, color=None ), - b = d.blockheight_disp(color=color), - D = d.txdate_disp( 'date_time' ), - i = d.txid_disp( width=None, color=color ), - A1 = d.amt_disp(True).hl( color=color ), - A2 = d.amt_disp(False).hl( color=color ), - f = d.fee_disp( color=color ), - a1 = d.vouts_list_disp( 'inputs', color=color, indent=' '*8 ), - oc = d.nOutputs, - a2 = d.vouts_list_disp( 'outputs', color=color, indent=' '*8 ), - ) + for n,d in enumerate(data,1): + yield fs.format( + n = str(n) + ')', + d = d.age_disp( 'date_time', width=None, color=None ), + b = d.blockheight_disp(color=color), + D = d.txdate_disp( 'date_time' ), + i = d.txid_disp( width=None, color=color ), + A1 = d.amt_disp(True).hl( color=color ), + A2 = d.amt_disp(False).hl( color=color ), + f = d.fee_disp( color=color ), + a1 = d.vouts_list_disp( 'inputs', color=color, indent=' '*8 ), + oc = d.nOutputs, + a2 = d.vouts_list_disp( 'outputs', color=color, indent=' '*8 ), + ) sort_disp = { 'age': 'Age', @@ -184,7 +180,7 @@ class TwTxHistory(MMGenObject,TwCommon,metaclass=AsyncInit): 'txid': lambda i: i.txid, } - async def set_dates(self,us): + async def set_dates(self,foo): pass @property diff --git a/mmgen/tw/unspent.py b/mmgen/tw/unspent.py index db3307b7..220d6131 100755 --- a/mmgen/tw/unspent.py +++ b/mmgen/tw/unspent.py @@ -71,7 +71,6 @@ class TwUnspentOutputs(MMGenObject,TwCommon,metaclass=AsyncInit): async def __init__(self,proto,minconf=1,addrs=[]): self.proto = proto - self.data = MMGenList() self.show_mmid = True self.minconf = minconf self.addrs = addrs @@ -104,7 +103,7 @@ class TwUnspentOutputs(MMGenObject,TwCommon,metaclass=AsyncInit): def filter_data(self): - data = self.data + data = self.data.copy() for d in data: d.skip = '' @@ -220,10 +219,7 @@ class TwUnspentOutputs(MMGenObject,TwCommon,metaclass=AsyncInit): a = ( '|'+'.' * addr_w if i.skip == 'addr' and self.group else i.addr.fmt(color=color,width=addr_w) ), - m = MMGenID.fmtc( - (i.twmmid if i.twmmid.type == 'mmgen' else f'Non-{g.proj_name}'), - width = mmid_w, - color = color ), + m = MMGenID.fmtc( i.twmmid.disp, width=mmid_w, color=color ), A = i.amt.fmt(color=color), A2 = ( i.amt2.fmt(color=color) if i.amt2 is not None else '' ), c = i.confs, @@ -244,6 +240,15 @@ class TwUnspentOutputs(MMGenObject,TwCommon,metaclass=AsyncInit): len(self.data), suf(self.data) )) + async def set_dates(self,us): + if not self.dates_set: + # 'blocktime' differs from 'time', is same as getblockheader['time'] + dates = [ o.get('blocktime',0) + for o in await self.rpc.gathered_icall('gettransaction',[(o.txid,True,False) for o in us]) ] + for idx,o in enumerate(us): + o.date = dates[idx] + self.dates_set = True + class action(TwCommon.action): def s_twmmid(self,parent): @@ -256,61 +261,3 @@ class TwUnspentOutputs(MMGenObject,TwCommon,metaclass=AsyncInit): 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): - from ..ui import keypress_confirm - if not keypress_confirm( - f'Refreshing tracking wallet {uo.item_desc} #{idx}. Is this what you want?'): - return 'redo' - await uo.wallet.get_balance( uo.data[idx-1].addr, force_rpc=True ) - await uo.get_data() - uo.oneshot_msg = yellow(f'{uo.proto.dcoin} balance for account #{idx} refreshed\n\n') - - async def a_addr_delete(self,uo,idx): - from ..ui import keypress_confirm - if not keypress_confirm( - f'Removing {uo.item_desc} #{idx} from tracking wallet. Is this what you want?'): - return 'redo' - if await uo.wallet.remove_address( uo.data[idx-1].addr ): - await uo.get_data() - uo.oneshot_msg = yellow(f'{capfirst(uo.item_desc)} #{idx} removed\n\n') - else: - await asyncio.sleep(3) - uo.oneshot_msg = red('Address could not be removed\n\n') - - async def a_comment_add(self,uo,idx): - - async def do_comment_add(comment): - e = uo.data[idx-1] - if await uo.wallet.add_comment( e.twmmid, comment, coinaddr=e.addr ): - await uo.get_data() - uo.oneshot_msg = yellow('Label {a} {b}{c}\n\n'.format( - a = 'to' if cur_comment and comment else 'added to' if comment else 'removed from', - b = desc, - c = ' edited' if cur_comment and comment else '' )) - else: - await asyncio.sleep(3) - uo.oneshot_msg = red('Label could not be {}\n\n'.format( - 'edited' if cur_comment and comment else - 'added' if comment else - 'removed' )) - - desc = f'{uo.item_desc} #{idx}' - cur_comment = uo.data[idx-1].comment - msg('Current label: {}'.format(cur_comment.hl() if cur_comment else '(none)')) - - from ..ui import line_input - res = line_input( - "Enter label text (or ENTER to return to main menu): ", - insert_txt = cur_comment ) - - if res == cur_comment: - return None - elif res == '': - from ..ui import keypress_confirm - return (await do_comment_add('')) if keypress_confirm( - f'Removing label for {desc}. Is this what you want?') else 'redo' - else: - return (await do_comment_add(res)) if get_obj(TwComment,s=res) else 'redo' diff --git a/mmgen/tx/new.py b/mmgen/tx/new.py index 3f85e1d4..10d97901 100755 --- a/mmgen/tx/new.py +++ b/mmgen/tx/new.py @@ -340,7 +340,7 @@ class New(Base): do_license_msg() if not opt.inputs: - await self.twuo.view_and_sort() + await self.twuo.view_filter_and_sort() self.twuo.display_total() diff --git a/test/objtest_py_d/ot_btc_mainnet.py b/test/objtest_py_d/ot_btc_mainnet.py index 95c91060..36776616 100755 --- a/test/objtest_py_d/ot_btc_mainnet.py +++ b/test/objtest_py_d/ot_btc_mainnet.py @@ -18,6 +18,7 @@ from .ot_common import * from mmgen.protocol import init_proto proto = init_proto('btc',need_amt=True) tw_pfx = proto.base_coin.lower() + ':' +zero_addr = '1111111111111111111114oLvT2' ssm = str(SeedShareCount.max_val) privkey = PrivKey(proto=proto,s=bytes.fromhex('deadbeef'*8),compressed=True,pubkey_type='std') @@ -162,9 +163,10 @@ tests = { {'id_str':'F00BAA12:Z:99', 'proto':proto}, {'id_str':tw_pfx, 'proto':proto}, {'id_str':tw_pfx+'я', 'proto':proto}, + {'id_str':tw_pfx+'x', 'proto':proto}, ), 'good': ( - {'id_str':tw_pfx+'x', 'proto':proto}, + {'id_str':tw_pfx+zero_addr, 'proto':proto}, {'id_str':'F00BAA12:99', 'proto':proto, 'ret':'F00BAA12:L:99'}, {'id_str':'F00BAA12:L:99', 'proto':proto}, {'id_str':'F00BAA12:S:9999999', 'proto':proto}, @@ -188,13 +190,14 @@ tests = { {'text':tw_pfx+'я x', 'proto':proto}, {'text':utf8_ctrl[:40], 'proto':proto}, {'text':'F00BAA12:S:1 ' + utf8_ctrl[:40], 'proto':proto, 'exc_name': 'BadTwComment'}, + {'text':tw_pfx+'x comment','proto':proto}, ), 'good': ( {'text':'F00BAA12:99 a comment', 'proto':proto, 'ret':'F00BAA12:L:99 a comment'}, {'text':'F00BAA12:L:99 a comment', 'proto':proto}, {'text': 'F00BAA12:L:99 comment (UTF-8) α', 'proto':proto}, {'text':'F00BAA12:S:9999999 comment', 'proto':proto}, - {'text':tw_pfx+'x comment', 'proto':proto}, + {'text':tw_pfx+zero_addr+' comment', 'proto':proto}, ), }, 'MMGenTxID': { diff --git a/test/overlay/fakemods/mmgen/tw/common.py b/test/overlay/fakemods/mmgen/tw/common.py index 3326a828..463f5bf8 100644 --- a/test/overlay/fakemods/mmgen/tw/common.py +++ b/test/overlay/fakemods/mmgen/tw/common.py @@ -15,13 +15,5 @@ if overlay_fake_os.getenv('MMGEN_TEST_SUITE_DETERMINISTIC'): if overlay_fake_os.getenv('MMGEN_BOGUS_UNSPENT_DATA'): - class overlay_fake_data2: - - async def set_dates(foo,us): - for o in us: - o.date = 1831006505 - int(9.7 * 60 * (o.confs - 1)) - - TwCommon.set_dates = overlay_fake_data2.set_dates - # 1831006505 (09 Jan 2028) = projected time of block 1000000 TwCommon.date_formatter['days'] = lambda rpc,secs: (1831006505 - secs) // 86400 diff --git a/test/overlay/fakemods/mmgen/tw/unspent.py b/test/overlay/fakemods/mmgen/tw/unspent.py new file mode 100644 index 00000000..efcb3f7e --- /dev/null +++ b/test/overlay/fakemods/mmgen/tw/unspent.py @@ -0,0 +1,12 @@ +import os as overlay_fake_os +from .unspent_orig import * + +if overlay_fake_os.getenv('MMGEN_BOGUS_UNSPENT_DATA'): + + class overlay_fake_data: + + async def set_dates(foo,us): + for o in us: + o.date = 1831006505 - int(9.7 * 60 * (o.confs - 1)) + + TwUnspentOutputs.set_dates = overlay_fake_data.set_dates diff --git a/test/test_py_d/ts_ethdev.py b/test/test_py_d/ts_ethdev.py index 636b0695..814b9d0f 100755 --- a/test/test_py_d/ts_ethdev.py +++ b/test/test_py_d/ts_ethdev.py @@ -71,26 +71,26 @@ bals = { ('98831F3A:E:2','23.45495'), ('98831F3A:E:11','1.234'), ('98831F3A:E:21','2.345'), - (burn_addr + r'\s+Non-MMGen',amt1)], + (burn_addr + r'\s+non-MMGen',amt1)], '8': [ ('98831F3A:E:1','0'), ('98831F3A:E:2','23.45495'), ('98831F3A:E:11',vbal1,'a1'), ('98831F3A:E:12','99.99895'), ('98831F3A:E:21','2.345'), - (burn_addr + r'\s+Non-MMGen',amt1)], + (burn_addr + r'\s+non-MMGen',amt1)], '9': [ ('98831F3A:E:1','0'), ('98831F3A:E:2','23.45495'), ('98831F3A:E:11',vbal1,'a1'), ('98831F3A:E:12',vbal2), ('98831F3A:E:21','2.345'), - (burn_addr + r'\s+Non-MMGen',amt1)], + (burn_addr + r'\s+non-MMGen',amt1)], '10': [ ('98831F3A:E:1','0'), ('98831F3A:E:2','23.0218'), ('98831F3A:E:3','0.4321'), ('98831F3A:E:11',vbal1,'a1'), ('98831F3A:E:12',vbal2), ('98831F3A:E:21','2.345'), - (burn_addr + r'\s+Non-MMGen',amt1)] + (burn_addr + r'\s+non-MMGen',amt1)] } token_bals = { @@ -101,18 +101,18 @@ token_bals = { ('98831F3A:E:12','1.23456','0')], '4': [ ('98831F3A:E:11','110.654317776666555545',vbal1,'a1'), ('98831F3A:E:12','1.23456','0'), - (burn_addr + r'\s+Non-MMGen',amt2,amt1)], + (burn_addr + r'\s+non-MMGen',amt2,amt1)], '5': [ ('98831F3A:E:11','110.654317776666555545',vbal1,'a1'), ('98831F3A:E:12','1.23456','99.99895'), - (burn_addr + r'\s+Non-MMGen',amt2,amt1)], + (burn_addr + r'\s+non-MMGen',amt2,amt1)], '6': [ ('98831F3A:E:11','110.654317776666555545',vbal1,'a1'), ('98831F3A:E:12','0',vbal2), ('98831F3A:E:13','1.23456','0'), - (burn_addr + r'\s+Non-MMGen',amt2,amt1)], + (burn_addr + r'\s+non-MMGen',amt2,amt1)], '7': [ ('98831F3A:E:11','67.444317776666555545',vbal9,'a2'), ('98831F3A:E:12','43.21',vbal2), ('98831F3A:E:13','1.23456','0'), - (burn_addr + r'\s+Non-MMGen',amt2,amt1)] + (burn_addr + r'\s+non-MMGen',amt2,amt1)] } token_bals_getbalance = { '1': (vbal4,'999999.12345689012345678'), @@ -1260,20 +1260,32 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): def edit_comment(self,out_num,args=[],action='l',comment_text=None,changed=False,pexpect_spawn=None): t = self.spawn('mmgen-txcreate', self.eth_args + args + ['-B','-i'],pexpect_spawn=pexpect_spawn) - p1,p2 = ('efresh balance:\b','return to main menu): ') - p3,r3 = (p2,comment_text+'\n') if comment_text is not None else ('(y/N): ','y') - p4,r4 = (('(y/N): ',),('y',)) if comment_text == Ctrl_U else ((),()) - for p,r in zip((p1,p1,p2,p3)+p4,('M',action,out_num+'\n',r3)+r4): - t.expect(p,r) + + menu_prompt = 'efresh balance:\b' + + t.expect(menu_prompt,'M') + t.expect(menu_prompt,action) + t.expect(r'return to main menu): ',out_num+'\n') + + for p,r in ( + ('Enter label text.*: ',comment_text+'\n') if comment_text is not None else (r'\(y/N\): ','y'), + (r'\(y/N\): ','y') if comment_text == Ctrl_U else (None,None), + ): + if p: + t.expect(p,r,regex=True) + m = ( - 'Label to account #{} edited' if changed else + 'Label for account #{} edited' if changed else 'Account #{} removed' if action == 'D' else 'Label added to account #{}' if comment_text and comment_text != Ctrl_U else 'Label removed from account #{}' ) + t.expect(m.format(out_num)) - for p,r in zip((p1,p1),('M','q')): - t.expect(p,r) + t.expect(menu_prompt,'M') + t.expect(menu_prompt,'q') + t.expect('Total unspent:') + return t def edit_comment1(self): diff --git a/test/test_py_d/ts_regtest.py b/test/test_py_d/ts_regtest.py index a26df280..3edbce72 100755 --- a/test/test_py_d/ts_regtest.py +++ b/test/test_py_d/ts_regtest.py @@ -683,7 +683,7 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): t = self.spawn('mmgen-tool',['--'+user,'txhist'] + args) res = strip_ansi_escapes(t.read()).replace('\r','') m = re.search(expect,res,re.DOTALL) - assert m, m + assert m, f'Expected: {expect}' return t def bob_txhist1(self): @@ -699,7 +699,7 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): def bob_txhist3(self): return self.user_txhist('bob', args = ['sort=blockheight','sinceblock=-7','age_fmt=block'], - expect = fr'Displaying transactions since block 399.*\s6\)\s+405.*:C:3\s.*\s{rtBals[9]}\s.*:L:5.*\s7\)' + expect = fr'Displaying transactions since block 399.*\s6\)\s+405\s.*\s{rtBals[9]}\s.*:L:5.*\s7\)' ) def bob_txhist4(self): @@ -1147,28 +1147,29 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): mmid = self._user_sid('alice') + (':S:1',':L:1')[self.proto.coin=='BCH'] return self._user_chk_comment('alice',mmid,'Label added using coin address of MMGen address') - def alice_add_comment_badaddr(self,addr,reply): + def alice_add_comment_badaddr(self,addr,reply,exit_val): if os.getenv('PYTHONOPTIMIZE'): omsg(yellow(f'PYTHONOPTIMIZE set, skipping test {self.test_name!r}')) return 'skip' t = self.spawn('mmgen-tool',['--alice','add_label',addr,'(none)']) t.expect(reply,regex=True) + t.req_exit_val = exit_val return t def alice_add_comment_badaddr1(self): - return self.alice_add_comment_badaddr( rt_pw,'Invalid coin address for this chain: ') + return self.alice_add_comment_badaddr( rt_pw,'Invalid coin address for this chain: ', 2) def alice_add_comment_badaddr2(self): addr = init_proto(self.proto.coin,network='mainnet').pubhash2addr(bytes(20),False) # mainnet zero address - return self.alice_add_comment_badaddr( addr, f'Invalid coin address for this chain: {addr}' ) + return self.alice_add_comment_badaddr( addr, f'Invalid coin address for this chain: {addr}', 2 ) def alice_add_comment_badaddr3(self): addr = self._user_sid('alice') + ':C:123' - return self.alice_add_comment_badaddr( addr, f'MMGen address {addr!r} not found in tracking wallet' ) + return self.alice_add_comment_badaddr( addr, f'MMGen address {addr!r} not found in tracking wallet', 2 ) def alice_add_comment_badaddr4(self): addr = self.proto.pubhash2addr(bytes(20),False) # regtest (testnet) zero address - return self.alice_add_comment_badaddr( addr, f'Address {addr!r} not found in tracking wallet' ) + return self.alice_add_comment_badaddr( addr, f'Address {addr!r} not found in tracking wallet', 2 ) def alice_remove_comment1(self): sid = self._user_sid('alice') @@ -1201,7 +1202,7 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): t.expect(r'add \[l\]abel:.','M',regex=True) t.expect(r'add \[l\]abel:.','l',regex=True) t.expect(r"Enter unspent.*return to main menu\):.",output+'\n',regex=True) - t.expect(r"Enter label text.*return to main menu\):.",comment+'\n',regex=True) + t.expect(r"Enter label text.*:.",comment+'\n',regex=True) t.expect(r'\[q\]uit view, .*?:.','q',regex=True) return t