From 084b493b7f33e7faca91c3db717fc7f58ee7f6cc Mon Sep 17 00:00:00 2001 From: MMGen Date: Mon, 15 Oct 2018 15:58:57 +0000 Subject: [PATCH] tw view: cleanups, interactive address removal for ETH --- doc/release-notes/release-notes-v0.9.9.md | 3 + mmgen/altcoins/eth/tw.py | 12 ++- mmgen/tw.py | 106 +++++++++++++--------- test/test.py | 52 +++++++++-- 4 files changed, 119 insertions(+), 54 deletions(-) diff --git a/doc/release-notes/release-notes-v0.9.9.md b/doc/release-notes/release-notes-v0.9.9.md index b90bd585..38b25d17 100644 --- a/doc/release-notes/release-notes-v0.9.9.md +++ b/doc/release-notes/release-notes-v0.9.9.md @@ -3,4 +3,7 @@ #### New features: - Full Ethereum (`adef0b3`), Ethereum Classic (`d4eb8f6`) and ERC20 token (`881d559`) support + For usage details, see https://github.com/mmgen/mmgen/wiki/Altcoin-and-Forkcoin-Support + + This is a Linux-only release diff --git a/mmgen/altcoins/eth/tw.py b/mmgen/altcoins/eth/tw.py index 8a7d02c9..6e0afe61 100755 --- a/mmgen/altcoins/eth/tw.py +++ b/mmgen/altcoins/eth/tw.py @@ -178,13 +178,18 @@ class EthereumTwUnspentOutputs(TwUnspentOutputs): disp_type = 'eth' can_group = False + col_adj = 29 hdr_fmt = 'TRACKED ACCOUNTS (sort order: {})\nTotal {}: {}' desc = 'account balances' + item_desc = 'account' dump_fn_pfx = 'balances' prompt = """ -Sort options: [a]mount, a[d]dress, [A]ge, [r]everse, [M]mgen addr -Display options: show [D]ays, show [m]mgen addr, r[e]draw screen +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, [R]emove address: """ + valid_keypresses = 'adrMmeqpvwlR' def do_sort(self,key=None,reverse=False): if key == 'txid': return @@ -206,8 +211,9 @@ class EthereumTokenTwUnspentOutputs(EthereumTwUnspentOutputs): disp_type = 'token' prompt_fs = 'Total to spend: {} {}\n\n' + col_adj = 37 - def get_display_precision(self): return 10 + def get_display_precision(self): return 10 # truncate precision for narrow display def get_addr_bal(self,addr): return Token(g.token).balance(addr) diff --git a/mmgen/tw.py b/mmgen/tw.py index 700dc675..864afa64 100755 --- a/mmgen/tw.py +++ b/mmgen/tw.py @@ -25,6 +25,7 @@ from mmgen.obj import * from mmgen.tx import is_mmgen_id CUR_HOME,ERASE_ALL = '\033[H','\033[0J' +def CUR_RIGHT(n): return '\033[{}C'.format(n) class TwUnspentOutputs(MMGenObject): @@ -36,12 +37,16 @@ class TwUnspentOutputs(MMGenObject): can_group = True hdr_fmt = 'UNSPENT OUTPUTS (sort order: {}) Total {}: {}' desc = 'unspent outputs' + item_desc = 'unspent output' dump_fn_pfx = 'listunspent' 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 +Actions: [q]uit view, [p]rint to file, pager [v]iew, [w]ide view, add [l]abel: """ + valid_keypresses = 'tadArMDgmeqpvwl' + col_adj = 38 class MMGenTwOutputList(list,MMGenObject): pass @@ -122,7 +127,7 @@ watch-only wallet using '{}-addrimport' and then re-run this program. for u in self.unspent: if u.label == None: u.label = '' if not self.unspent: - die(1,'No tracked unspent outputs in tracking wallet!') + die(1,'No tracked {}s in tracking wallet!'.format(self.item_desc)) def do_sort(self,key=None,reverse=False): sort_funcs = { @@ -168,7 +173,7 @@ watch-only wallet using '{}-addrimport' and then re-run this program. mmid_w = max(len(('',i.twmmid)[i.twmmid.type=='mmgen']) for i in unsp) or 12 # DEADBEEF:S:1 max_acct_w = max(len(i.label) for i in unsp) + mmid_w + 1 max_btcaddr_w = max(len(i.addr) for i in unsp) - min_addr_w = self.cols - 38 + min_addr_w = self.cols - self.col_adj addr_w = min(max_btcaddr_w + (0,1+max_acct_w)[self.show_mmid],min_addr_w) acct_w = min(max_acct_w, max(24,addr_w-10)) btaddr_w = addr_w - acct_w - 1 @@ -275,83 +280,96 @@ watch-only wallet using '{}-addrimport' and then re-run this program. fs = '\nTotal unspent: {} {} ({} outputs)' msg(fs.format(self.total.hl(),g.dcoin,len(self.unspent))) - def get_idx_and_label_from_user(self): + def get_idx_from_user(self,get_label=False): msg('') while True: - ret = my_raw_input("Enter unspent output number (or 'q' to return to main menu): ") - if ret == 'q': return None,None + ret = my_raw_input('Enter {} number (or RETURN to return to main menu): '.format(self.item_desc)) + if ret == '': return (None,None) if get_label else None n = AddrIdx(ret,on_fail='silent') # hacky way to test and convert to integer if not n or n < 1 or n > len(self.unspent): msg('Choice must be a single number between 1 and {}'.format(len(self.unspent))) # elif not self.unspent[n-1].mmid: # msg('Address #{} is not an {} address. No label can be added to it'.format(n,g.proj_name)) else: - while True: - s = my_raw_input("Enter label text (or 'q' to return to main menu): ") - if s == 'q': - return None,None - elif s == '': - fs = "Removing label for address #{}. Is this what you want?" - if keypress_confirm(fs.format(n)): - return n,s - elif s: - if TwComment(s,on_fail='return'): - return n,s + if get_label: + while True: + s = my_raw_input("Enter label text (or 'q' to return to main menu): ") + if s == 'q': + return None,None + elif s == '': + fs = "Removing label for {} #{}. Is this what you want?" + if keypress_confirm(fs.format(self.item_desc,n)): + return n,s + elif s: + if TwComment(s,on_fail='return'): + return n,s + else: + fs = "Removing {} #{} from tracking wallet. Is this what you want?" + if keypress_confirm(fs.format(self.item_desc,n)): + return n def view_and_sort(self,tx): - txos = self.prompt_fs.format(tx.sum_outputs().hl(),g.dcoin) if tx.outputs else '' - prompt = txos + self.prompt.strip() self.display() - msg(prompt) - from mmgen.term import get_char - p = "'q'=quit view, 'p'=print to file, 'v'=pager view, 'w'=wide view, 'l'=add label:\b" + prompt = self.prompt.strip() + '\b' + skip_prompt,oneshot_msg = False,None while True: - reply = get_char(p,immed_chars='atDdAMrgmeqpvw') - if reply == 'a': self.do_sort('amt') + reply = get_char('' if skip_prompt else (oneshot_msg or '')+prompt,immed_chars=self.valid_keypresses) + skip_prompt = False + oneshot_msg = '' if oneshot_msg else None # tristate, saves previous state + if reply not in self.valid_keypresses: + msg_r('\ninvalid keypress ') + time.sleep(0.5) + elif reply == 'a': self.do_sort('amt') elif reply == 'A': self.do_sort('age') elif reply == 'd': self.do_sort('addr') elif reply == 'D': self.show_days = not self.show_days - elif reply == 'e': msg('\n{}\n{}\n{}'.format(self.fmt_display,prompt,p)) + elif reply == 'e': pass elif reply == 'g': if self.can_group: self.group = not self.group elif reply == 'l': - idx,lbl = self.get_idx_and_label_from_user() + idx,lbl = self.get_idx_from_user(get_label=True) if idx: e = self.unspent[idx-1] if TrackingWallet(mode='w').add_label(e.twmmid,lbl,addr=e.addr): self.get_unspent_data() self.do_sort() - msg(u'{}\n{}\n{}'.format(self.fmt_display,prompt,p)) + action = 'added to' if lbl else 'removed from' + oneshot_msg = yellow("Label {} {} #{}\n\n".format(action,self.item_desc,idx)) else: - msg('Label could not be added\n{}\n{}'.format(prompt,p)) + msg('Label could not be added\n{}'.format(prompt)) elif reply == 'M': self.do_sort('twmmid'); self.show_mmid = True elif reply == 'm': self.show_mmid = not self.show_mmid elif reply == 'p': - msg('') of = '{}-{}[{}].out'.format(self.dump_fn_pfx,g.dcoin, ','.join(self.sort_info(include_group=False)).lower()) - write_data_to_file(of,self.format_for_printing(),'{} listing'.format(self.desc)) - m = yellow("Data written to '{}'".format(of)) - msg('\n{}\n{}\n\n{}'.format(self.fmt_display,m,prompt)) - continue - elif reply == 'q': return self.unspent + msg('') + write_data_to_file(of,self.format_for_printing(),desc='{} listing'.format(self.desc)) + oneshot_msg = yellow("Data written to '{}'\n\n".format(of)) + elif reply == 'q': msg(''); return self.unspent elif reply == 'r': self.unspent.reverse(); self.reverse = not self.reverse + elif reply == 'R': + idx = self.get_idx_from_user() + if idx: + e = self.unspent[idx-1] + if TrackingWallet(mode='w').remove_address(e.addr): + self.get_unspent_data() + self.do_sort() + self.total = self.get_total_coin() + oneshot_msg = yellow("{} #{} removed\n\n".format(capfirst(self.item_desc),idx)) + else: + msg('Address could not be removed\n{}'.format(prompt)) elif reply == 't': self.do_sort('txid') - elif reply == 'v': - do_pager(self.fmt_display) - continue - elif reply == 'w': - do_pager(self.format_for_printing(color=True)) - continue - else: - msg('\nInvalid input') - continue + elif reply in ('v','w'): + do_pager(self.fmt_display if reply == 'v' else self.format_for_printing(color=True)) + if g.platform == 'linux' and oneshot_msg == None: + msg_r(CUR_RIGHT(len(prompt.split('\n')[-1])-2)) + skip_prompt = True + continue msg('\n') - self.display() - msg(prompt) + self.display() # creates self.fmt_display class TwAddrList(MMGenDict): diff --git a/test/test.py b/test/test.py index 6722a8eb..64c96166 100755 --- a/test/test.py +++ b/test/test.py @@ -629,11 +629,13 @@ eth_key = '4d5db4107d237df6a3d58ee5f70ae63d73d7658d4026f2eefd2f204c81682cb7' eth_burn_addr = 'deadbeef'*5 eth_amt1 = '999999.12345689012345678' eth_amt2 = '888.111122223333444455' +eth_rem_addrs = ('4','1') def eth_args(): assert g.coin in ('ETH','ETC'),'for ethdev tests, --coin must be set to either ETH or ETC' return [u'--outdir={}'.format(cfgs['22']['tmpdir']),'--rpc-port=8549','--quiet'] + from copy import deepcopy for a,b in (('6','11'),('7','12'),('8','13')): cfgs[b] = deepcopy(cfgs[a]) @@ -991,6 +993,15 @@ cmd_group['ethdev'] = ( ('ethdev_token_twview2','twview --token=mm1 wide=1'), ('ethdev_token_twview3','twview --token=mm1 wide=1 sort=age (ignored)'), + ('ethdev_edit_label1','adding label to addr #{} in {} tracking wallet'.format(eth_rem_addrs[0],g.coin)), + ('ethdev_edit_label2','adding label to addr #{} in {} tracking wallet'.format(eth_rem_addrs[1],g.coin)), + ('ethdev_edit_label3','removing label from addr #{} in {} tracking wallet'.format(eth_rem_addrs[0],g.coin)), + + ('ethdev_remove_addr1','removing addr #{} from {} tracking wallet'.format(eth_rem_addrs[0],g.coin)), + ('ethdev_remove_addr2','removing addr #{} from {} tracking wallet'.format(eth_rem_addrs[1],g.coin)), + ('ethdev_remove_token_addr1','removing addr #{} from {} token tracking wallet'.format(eth_rem_addrs[0],g.coin)), + ('ethdev_remove_token_addr2','removing addr #{} from {} token tracking wallet'.format(eth_rem_addrs[1],g.coin)), + ('ethdev_stop', 'stopping parity'), ) @@ -1807,7 +1818,7 @@ class MMGenTestSuite(object): fee_desc='transaction fee',fee_res=None, add_comment='',view='t',save=True,no_ok=False): for choice in menu + ['q']: - t.expect(r"'q'=quit view, .*?:.",choice,regex=True) + t.expect(r'\[q\]uit view, .*?:.',choice,regex=True) if bad_input_sels: for r in ('x','3-1','9999'): t.expect(input_sels_prompt+': ',r+'\n') @@ -3126,11 +3137,11 @@ class MMGenTestSuite(object): def regtest_user_edit_label(self,name,user,output,label): t = MMGenExpect(name,'mmgen-txcreate',['-B','--'+user,'-i']) - t.expect(r"'q'=quit view, .*?:.",'M',regex=True) - t.expect(r"'q'=quit view, .*?:.",'l',regex=True) + 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\):.",label+'\n',regex=True) - t.expect(r"'q'=quit view, .*?:.",'q',regex=True) + t.expect(r'\[q\]uit view, .*?:.','q',regex=True) t.ok() def regtest_stop(self,name): @@ -3175,7 +3186,7 @@ class MMGenTestSuite(object): '--outdir='+cfg['tmpdir'], '--tx-fees=0.0001,0.0003', sid+':S:1',sid+':S:2']) - t.expect(r"'q'=quit view, .*?:.",'q', regex=True) + t.expect(r'\[q\]uit view, .*?:.','q', regex=True) t.expect('outputs to spend: ','1\n') for tx in ('timelocked','split'): @@ -3279,7 +3290,7 @@ class MMGenTestSuite(object): fee_res='0.00105 {} (50 gas price in Gwei)'.format(g.coin), fee_desc = 'gas price'): t = MMGenExpect(name,'mmgen-txcreate', eth_args() + ['-B'] + args) - t.expect(r"'q'=quit view, .*?:.",'p', regex=True) + t.expect(r'add \[l\]abel, .*?:.','p', regex=True) t.written_to_file('Account balances listing') self.txcreate_ui_common(t,name, menu=menu, @@ -3304,7 +3315,8 @@ class MMGenTestSuite(object): self.txsend_ui_common(t,name,quiet=True,bogus_send=bogus_send,has_label=True) def ethdev_txcreate1(self,name): - menu = ['a','d','A','r','M','D','e','m','m'] + # valid_keypresses = 'adrMmeqpvwl' + menu = ['a','d','r','M','D','e','m','m'] # include one invalid keypress, 'D' args = ['98831F3A:E:1,123.456'] return self.ethdev_txcreate(name,args=args,menu=menu,acct='1',non_mmgen_inputs=1) @@ -3390,6 +3402,7 @@ class MMGenTestSuite(object): t.expect('Removed label.*in tracking wallet',regex=True) t.ok() + def init_ethdev_common(self): g.testnet = True init_coin(g.coin) @@ -3622,6 +3635,31 @@ class MMGenTestSuite(object): def ethdev_token_twview3(self,name): return self.ethdev_twview(name,args=['--token=mm1'],tool_args=['wide=1','sort=age']) + def ethdev_edit_label(self,name,out_num,args=[],action='l',label_text=None): + t = MMGenExpect(name,'mmgen-txcreate', eth_args() + args + ['-B','-i']) + p1,p2 = ('emove address:\b','return to main menu): ') + p3,r3 = (p2,label_text+'\n') if label_text is not None else ('(y/N): ','y') + p4,r4 = (('(y/N): ',),('y',)) if label_text == '' else ((),()) + for p,r in zip((p1,p1,p2,p3)+p4+(p1,p1),('M',action,out_num+'\n',r3)+r4+('M','q')): + t.expect(p,r) + t.ok() + + def ethdev_edit_label1(self,name): + self.ethdev_edit_label(name,out_num=eth_rem_addrs[0],label_text='First added label-α') + def ethdev_edit_label2(self,name): + self.ethdev_edit_label(name,out_num=eth_rem_addrs[1],label_text='Second added label') + def ethdev_edit_label3(self,name): + self.ethdev_edit_label(name,out_num=eth_rem_addrs[0],label_text='') + + def ethdev_remove_addr1(self,name): + self.ethdev_edit_label(name,out_num=eth_rem_addrs[0],action='R') + def ethdev_remove_addr2(self,name): + self.ethdev_edit_label(name,out_num=eth_rem_addrs[1],action='R') + def ethdev_remove_token_addr1(self,name): + self.ethdev_edit_label(name,out_num=eth_rem_addrs[0],args=['--token=mm1'],action='R') + def ethdev_remove_token_addr2(self,name): + self.ethdev_edit_label(name,out_num=eth_rem_addrs[1],args=['--token=mm1'],action='R') + def ethdev_stop(self,name): MMGenExpect(name,'',msg_only=True) pid = read_from_tmpfile(cfg,cfg['parity_pidfile']) # exits if file not found