Browse Source

tw view: cleanups, interactive address removal for ETH

MMGen 6 years ago
parent
commit
084b493b7f
4 changed files with 119 additions and 54 deletions
  1. 3 0
      doc/release-notes/release-notes-v0.9.9.md
  2. 9 3
      mmgen/altcoins/eth/tw.py
  3. 62 44
      mmgen/tw.py
  4. 45 7
      test/test.py

+ 3 - 0
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

+ 9 - 3
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)

+ 62 - 44
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):
 

+ 45 - 7
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