tw view: cleanups, interactive address removal for ETH

This commit is contained in:
The MMGen Project 2018-10-15 15:58:57 +00:00
commit 084b493b7f
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
4 changed files with 119 additions and 54 deletions

View file

@ -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

View file

@ -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)

View file

@ -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):

View file

@ -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