9 Commits b89b6270a7 ... 7b53f4337f

Author SHA1 Message Date
  The MMGen Project 7b53f4337f XMR compat: address/account creation 2 months ago
  The MMGen Project a75518e78f ui: new `item_chooser()` function 2 months ago
  The MMGen Project b62bebc7b8 tw.view: new `post_action_cleanup()` method 2 months ago
  The MMGen Project e8ca36af44 test/cmdtest.py xmr_autosign: cleanups 2 months ago
  The MMGen Project 72565eb646 xmrwallet OpLabel, OpNew: skip wallet restart and refresh 2 months ago
  The MMGen Project c7f240bb5b xmrwallet.ops.label:main(): remove `auto` param 2 months ago
  The MMGen Project 3d8e1223a1 TwView.item_action.i_comment_add(): inline nested function 2 months ago
  The MMGen Project caade42c16 TwView.item_action: add `acct_methods` attribute 2 months ago
  The MMGen Project 496c817b9c minor cleanups 2 months ago

+ 1 - 1
mmgen/data/version

@@ -1 +1 @@
-16.1.dev24
+16.1.dev25

+ 60 - 1
mmgen/proto/xmr/tw/addresses.py

@@ -16,6 +16,30 @@ from ....tw.addresses import TwAddresses
 
 from .view import MoneroTwView
 
+async def add_new_address(parent, spec, ok_msg):
+	from ....ui import line_input, keypress_confirm
+	from ....color import green, yellow
+	from ....xmrwallet import op as xmrwallet_op
+	lbl = line_input(
+		parent.cfg,
+		'Enter label text for new address (or ENTER for default label): ')
+	add_timestr = keypress_confirm(parent.cfg, 'Add timestamp to label?')
+	op = xmrwallet_op(
+		'new',
+		parent.cfg,
+		None,
+		None,
+		spec = spec + (',' + lbl if lbl else ''),
+		compat_call = True)
+	op.c.call('close_wallet')
+	if await op.main(add_timestr=add_timestr):
+		await parent.get_data()
+		parent.oneshot_msg = green(ok_msg)
+		return 'redraw'
+	else:
+		parent.oneshot_msg = yellow('Operation cancelled')
+		return False
+
 class MoneroTwAddresses(MoneroTwView, TwAddresses):
 
 	include_empty = True
@@ -23,8 +47,10 @@ class MoneroTwAddresses(MoneroTwView, TwAddresses):
 
 	prompt_fs_repl = {'XMR': (
 		(1, 'Filters: show [E]mpty addrs, [u]sed addrs, all [L]abels'),
-		(3, 'Actions: [q]uit menu, add [l]abel, [R]efresh balances:'))}
+		(3, 'Actions: [q]uit menu, add [l]abel, [N]ew acct, [n]ew addr, [R]efresh bals:'))}
 	extra_key_mappings = {
+		'N': 'a_acct_new',
+		'n': 'i_addr_new',
 		'u': 'd_showused',
 		'R': 'a_sync_wallets'}
 	removed_key_mappings = {
@@ -40,3 +66,36 @@ class MoneroTwAddresses(MoneroTwView, TwAddresses):
 
 	def get_disp_data(self):
 		return MoneroTwView.get_disp_data(self, input_data=tuple(TwAddresses.get_disp_data(self)))
+
+	class action(MoneroTwView.action):
+
+		async def a_acct_new(self, parent):
+			from ....obj import Int
+			from ....util import suf
+			from ....addr import MMGenID
+			from ....ui import item_chooser
+			def wallet_id(wnum):
+				return MMGenID(proto=parent.proto, id_str='{}:M:{}'.format(parent.sid, wnum))
+			res = item_chooser(
+				parent.cfg,
+				'Choose a wallet to add a new account to',
+				[(d['wallet_num'], len(d['data'].accts_data['subaddress_accounts']))
+					for d in parent.dump_data],
+				lambda d: '{a} [{b} account{c}]'.format(
+					a = wallet_id(d[0]).hl(),
+					b = Int(d[1]).hl(),
+					c = suf(d[1])))
+			return await add_new_address(
+				parent,
+				str(res.item[0]),
+				f'New account added to wallet {wallet_id(res.item[0]).hl()}')
+
+	class item_action(TwAddresses.item_action):
+		acct_methods = ('i_addr_new')
+
+		async def i_addr_new(self, parent, idx, acct_addr_idx=None):
+			e = parent.accts_data[idx-1]
+			return await add_new_address(
+				parent,
+				f'{e.idx}:{e.acct_idx}',
+				f'New address added to wallet {e.idx}, account #{e.acct_idx}')

+ 2 - 2
mmgen/proto/xmr/tw/ctl.py

@@ -41,5 +41,5 @@ class MoneroTwCtl(TwCtlWithStore):
 			None,
 			spec = f'{m.idx}:{m.acct_idx}:{m.addr_idx},{comment}',
 			compat_call = True)
-		await op.restart_wallet_daemon()
-		return await op.main(add_timestr=add_timestr, auto=True)
+		op.c.call('close_wallet')
+		return await op.main(add_timestr=add_timestr)

+ 9 - 9
mmgen/proto/xmr/tw/view.py

@@ -57,16 +57,16 @@ class MoneroTwView:
 
 		op = xmrwallet_op('dump_data', self.cfg, None, None, compat_call=True)
 		await op.restart_wallet_daemon()
-		wallets_data = await op.main()
+		self.dump_data = await op.main()
 
-		if wallets_data:
-			self.sid = SeedID(sid=wallets_data[0]['seed_id'])
+		if self.dump_data:
+			self.sid = SeedID(sid=self.dump_data[0]['seed_id'])
 
 		self.total = self.unlocked_total = self.proto.coin_amt('0')
 
 		def gen_addrs():
 			bd = namedtuple('address_balance_data', ['bal', 'unlocked_bal', 'blocks_to_unlock'])
-			for wdata in wallets_data:
+			for wdata in self.dump_data:
 				bals_data = {i: {} for i in range(len(wdata['data'].accts_data['subaddress_accounts']))}
 
 				for d in wdata['data'].bals_data.get('per_subaddress', []):
@@ -210,8 +210,10 @@ class MoneroTwView:
 				color_override = None if d.amt == d.unlocked_amt else 'orange',
 				prec = self.disp_prec))
 
-	async def get_idx_from_user(self):
+	async def get_idx_from_user(self, method_name):
 		if res := await self.get_idx(f'{self.item_desc} number', self.accts_data):
+			if method_name in self.item_action.acct_methods:
+				return res
 			return await self.get_idx(
 				'address index',
 				self.accts_data[res.idx - 1].data,
@@ -220,8 +222,7 @@ class MoneroTwView:
 	class action(TwView.action):
 
 		async def a_sync_wallets(self, parent):
-			from ....util import msg, msg_r, ymsg
-			from ....tw.view import CUR_HOME, ERASE_ALL
+			from ....util import msg, ymsg
 			msg('')
 			try:
 				op = xmrwallet_op('sync', parent.cfg, None, None, compat_call=True)
@@ -230,10 +231,9 @@ class MoneroTwView:
 					import asyncio
 					ymsg(str(e))
 					await asyncio.sleep(2)
-					msg_r(CUR_HOME + ERASE_ALL)
 					return False
 				raise
 			await op.restart_wallet_daemon()
 			await op.main()
 			await parent.get_data()
-			msg_r(CUR_HOME + ERASE_ALL)
+			return 'erase'

+ 7 - 19
mmgen/tw/addresses.py

@@ -12,7 +12,7 @@
 tw.addresses: Tracking wallet listaddresses class for the MMGen suite
 """
 
-from ..util import msg, is_int, die
+from ..util import msg, die
 from ..obj import MMGenListItem, ImmutableAttr, ListItemAttr, TwComment, NonNegativeInt
 from ..addr import CoinAddr, MMGenID, MMGenAddrType
 from ..amt import CoinAmtChk
@@ -403,24 +403,12 @@ class TwAddresses(TwView):
 		"""
 
 		def choose_address(addrs):
-
-			def format_line(n, d):
-				return '{a:3}) {b}{c}'.format(
-					a = n,
-					b = d.twmmid.hl(),
-					c = yellow(' <== has a label!') if d.comment else ''
-				)
-
-			prompt = '\nChoose a {desc}:\n\n{items}\n\nEnter a number> '.format(
-				desc = desc,
-				items = '\n'.join(format_line(n, d) for n, d in enumerate(addrs, 1)))
-
-			from ..ui import line_input
-			while True:
-				res = line_input(self.cfg, prompt)
-				if is_int(res) and 0 < int(res) <= len(addrs):
-					return addrs[int(res)-1]
-				msg(f'{res}: invalid entry')
+			def format_addr(d):
+				return '{a}{b}'.format(
+					a = d.twmmid.hl(),
+					b = yellow(' <== has a label!') if d.comment else '')
+			from ..ui import item_chooser
+			return item_chooser(self.cfg, f'Choose a {desc}', addrs, format_addr).item
 
 		def get_addr(mmtype):
 			return [self.get_change_address(

+ 3 - 7
mmgen/tw/prune.py

@@ -12,11 +12,10 @@
 tw.prune: Tracking wallet pruned listaddresses class for the MMGen suite
 """
 
-from ..util import msg, msg_r, rmsg, ymsg
+from ..util import msg, rmsg, ymsg
 from ..color import red, green, gray, yellow
 from ..obj import ListItemAttr
 from .addresses import TwAddresses
-from .view import CUR_HOME, ERASE_ALL
 
 class TwAddressesPrune(TwAddresses):
 
@@ -155,15 +154,12 @@ class TwAddressesPrune(TwAddresses):
 				else:
 					e.tag = True
 
-			if parent.scroll:
-				msg_r(CUR_HOME + ERASE_ALL)
+			return 'erase'
 
 		async def a_unprune(self, parent):
 			for addrnum in self.get_addrnums(parent, 'unprune'):
 				parent.disp_data[addrnum-1].tag = False
-
-			if parent.scroll:
-				msg_r(CUR_HOME + ERASE_ALL)
+			return 'erase'
 
 		async def a_clear_prune_list(self, parent):
 			for d in parent.data:

+ 36 - 33
mmgen/tw/view.py

@@ -657,7 +657,7 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 				msg_r('\r'+''.ljust(self.term_width)+'\r'+yellow('Canceling! '))
 			return False
 
-	async def get_idx_from_user(self):
+	async def get_idx_from_user(self, method_name):
 		return await self.get_idx(f'{self.item_desc} number', self.disp_data)
 
 	async def get_idx(self, desc, data, *, is_addr_idx=False):
@@ -692,11 +692,19 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 					return ur(idx, None)
 			await do_error_msg()
 
+	async def post_action_cleanup(self, ret):
+		if self.scroll and (ret is False or ret in ('redraw', 'erase')):
+			# error messages could leave screen in messy state, so do complete redraw:
+			msg_r(
+				CUR_HOME + ERASE_ALL + (
+					'' if ret == 'erase' else
+					await self.format(display_type='squeezed', interactive=True, scroll=True)))
+
 	class action:
 
 		@enable_echo
 		async def run(self, parent, action_method):
-			return await action_method(parent)
+			await parent.post_action_cleanup(await action_method(parent))
 
 		async def a_print_detail(self, parent):
 			return await self._print(parent, output_type='detail')
@@ -748,6 +756,7 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 			do_pager(await parent.format('detail', color=True))
 
 	class item_action:
+		acct_methods = ()
 
 		@enable_echo
 		async def run(self, parent, action_method):
@@ -762,7 +771,8 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 				#  None:   action aborted by user or no action performed
 				#  'redo': user will be re-prompted for item number
 				#  'redraw': action successfully performed, screen will be redrawn
-				if usr_ret := await parent.get_idx_from_user():
+				#  'erase': action successfully performed, screen will be erased
+				if usr_ret := await parent.get_idx_from_user(action_method.__name__):
 					ret = await action_method(parent, usr_ret.idx, usr_ret.acct_addr_idx)
 				else:
 					ret = None
@@ -770,11 +780,7 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 					break
 				await asyncio.sleep(0.5)
 
-			if parent.scroll and (ret is False or ret == 'redraw'):
-				# error messages could leave screen in messy state, so do complete redraw:
-				msg_r(
-					CUR_HOME + ERASE_ALL +
-					await parent.format(display_type='squeezed', interactive=True, scroll=True))
+			await parent.post_action_cleanup(ret)
 
 		async def i_balance_refresh(self, parent, idx, acct_addr_idx=None):
 			if not parent.keypress_confirm(
@@ -809,29 +815,6 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 
 		async def i_comment_add(self, parent, idx, acct_addr_idx=None):
 
-			async def do_comment_add(comment_in):
-				from ..obj import TwComment
-				new_comment = await parent.twctl.set_comment(
-					addrspec     = None,
-					comment      = comment_in,
-					trusted_pair = (entry.twmmid, entry.addr),
-					silent       = parent.scroll)
-
-				edited = old_comment and new_comment
-				if isinstance(new_comment, TwComment):
-					entry.comment = new_comment
-					parent.oneshot_msg = (green if new_comment else yellow)('Label {a} {b}{c}'.format(
-						a = 'for' if edited else 'added to' if new_comment else 'removed from',
-						b = desc,
-						c = ' edited' if edited else ''))
-					return 'redraw' if parent.cfg.coin == 'XMR' else True
-				else:
-					await asyncio.sleep(3)
-					parent.oneshot_msg = red('Label for {desc} could not be {action}'.format(
-						desc = desc,
-						action = 'edited' if edited else 'added' if new_comment else 'removed'))
-					return False
-
 			if acct_addr_idx is None:
 				desc       = f'{parent.item_desc} #{idx}'
 				color_desc = f'{parent.item_desc} {red("#" + str(idx))}'
@@ -844,7 +827,7 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 			msg('Current label: {}'.format(old_comment.hl() if old_comment else '(none)'))
 
 			from ..ui import line_input
-			match res:= line_input(
+			match comment_in := line_input(
 					parent.cfg,
 					f'Enter label text for {color_desc}: ',
 					insert_txt = old_comment):
@@ -855,7 +838,27 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 					if not parent.keypress_confirm(f'Removing label for {color_desc}. OK?'):
 						return 'redo'
 
-			return await do_comment_add(res)
+			from ..obj import TwComment
+			new_comment = await parent.twctl.set_comment(
+				addrspec     = None,
+				comment      = comment_in,
+				trusted_pair = (entry.twmmid, entry.addr),
+				silent       = parent.scroll)
+
+			edited = old_comment and new_comment
+			if isinstance(new_comment, TwComment):
+				entry.comment = new_comment
+				parent.oneshot_msg = (green if new_comment else yellow)('Label {a} {b}{c}'.format(
+					a = 'for' if edited else 'added to' if new_comment else 'removed from',
+					b = desc,
+					c = ' edited' if edited else ''))
+				return 'redraw' if parent.cfg.coin == 'XMR' else True
+			else:
+				await asyncio.sleep(3)
+				parent.oneshot_msg = red('Label for {desc} could not be {action}'.format(
+					desc = desc,
+					action = 'edited' if edited else 'added' if new_comment else 'removed'))
+				return False
 
 	class scroll_action:
 

+ 16 - 1
mmgen/ui.py

@@ -14,7 +14,7 @@ ui: Interactive user interface functions for the MMGen suite
 
 import sys, os
 
-from .util import msg, msg_r, Msg, die
+from .util import msg, msg_r, Msg, die, is_int
 
 def confirm_or_raise(cfg, message, action, *, expect='YES', exit_msg='Exiting at user request'):
 	if message:
@@ -117,6 +117,21 @@ def keypress_confirm(
 			case _:
 				msg_r('\nInvalid reply\n' if verbose else '\r')
 
+def item_chooser(cfg, hdr, items, item_formatter, indent='  '):
+	from collections import namedtuple
+	col1_w = len(str(len(items)))
+	prompt = '{i}{a}:\n{i}{b}\n{i}{c}'.format(
+		a = hdr,
+		b = ('\n' + indent).join(f'  {n:{col1_w}}) {item_formatter(d)}' for n, d in enumerate(items, 1)),
+		c = 'Enter a number> ',
+		i = indent)
+	while True:
+		res = line_input(cfg, prompt)
+		if is_int(res) and 0 < int(res) <= len(items):
+			num = int(res)
+			return namedtuple('user_choice', 'num idx item')(num, num - 1, items[num - 1])
+		msg(f'{indent}{res}: invalid entry\n')
+
 def do_pager(text):
 
 	pagers = ['less', 'more']

+ 6 - 6
mmgen/xmrwallet/ops/label.py

@@ -29,7 +29,7 @@ class OpLabel(OpMixinSpec, OpWallet):
 	opts     = ()
 	wallet_offline = True
 
-	async def main(self, add_timestr='ask', auto=False):
+	async def main(self, add_timestr='ask'):
 
 		if not self.compat_call:
 			gmsg('\n{a} label for wallet {b}, account #{c}, address #{d}'.format(
@@ -40,14 +40,14 @@ class OpLabel(OpMixinSpec, OpWallet):
 
 		h = MoneroWalletRPC(self, self.source)
 
-		h.open_wallet('source')
-		wallet_data = h.get_wallet_data(print=not auto)
+		h.open_wallet('source', refresh=False)
+		wallet_data = h.get_wallet_data()
 
 		max_acct = len(wallet_data.accts_data['subaddress_accounts']) - 1
 		if self.account > max_acct:
 			die(2, f'{self.account}: requested account index out of bounds (>{max_acct})')
 
-		ret = h.print_acct_addrs(wallet_data, self.account, silent=auto)
+		ret = h.print_acct_addrs(wallet_data, self.account)
 
 		if self.address_idx > len(ret) - 1:
 			die(2, '{}: requested address index out of bounds (>{})'.format(
@@ -61,7 +61,7 @@ class OpLabel(OpMixinSpec, OpWallet):
 			(self.label + (f' [{make_timestr()}]' if add_timestr else '')) if self.label
 			else '')
 
-		if not auto:
+		if not self.compat_call:
 			ca = CoinAddr(self.proto, addr['address'])
 			from . import addr_width
 			msg('\n  {a} {b}\n  {c} {d}\n  {e} {f}'.format(
@@ -76,7 +76,7 @@ class OpLabel(OpMixinSpec, OpWallet):
 
 		if addr['label'] == new_label:
 			ymsg('\nLabel is unchanged, operation cancelled')
-		elif auto or keypress_confirm(self.cfg, f'  {op.capitalize()} label?'):
+		elif self.compat_call or keypress_confirm(self.cfg, f'  {op.capitalize()} label?'):
 			h.set_label(self.account, self.address_idx, new_label)
 			ret = h.print_acct_addrs(h.get_wallet_data(print=False), self.account)
 			label_chk = ret[self.address_idx]['label']

+ 8 - 3
mmgen/xmrwallet/ops/new.py

@@ -27,14 +27,16 @@ class OpNew(OpMixinSpec, OpWallet):
 	spec_key = ((1, 'source'),)
 	wallet_offline = True
 
-	async def main(self):
+	async def main(self, add_timestr=True):
 		h = MoneroWalletRPC(self, self.source)
-		h.open_wallet('Monero')
+		h.open_wallet('Monero', refresh=False)
 
 		desc = 'account' if self.account is None else 'address'
 		label = TwComment(
 			None if self.label == '' else
-			'{} [{}]'.format(self.label or f'xmrwallet new {desc}', make_timestr()))
+			'{a}{b}'.format(
+				a = self.label or (f'new {desc}' if self.compat_call else f'xmrwallet new {desc}'),
+				b = ' [{}]'.format(make_timestr()) if add_timestr else ''))
 
 		wallet_data = h.get_wallet_data()
 
@@ -58,11 +60,14 @@ class OpNew(OpMixinSpec, OpWallet):
 
 			if desc == 'address':
 				h.print_acct_addrs(wallet_data, self.account)
+			ret = True
 		else:
 			ymsg('\nOperation cancelled by user request')
+			ret = False
 
 		# wallet must be left open: otherwise the 'stop_wallet' RPC call used to stop the daemon will fail
 		if self.cfg.no_stop_wallet_daemon:
 			h.close_wallet('Monero')
 
 		msg('')
+		return ret

+ 95 - 25
test/cmdtest_d/xmr_autosign.py

@@ -508,6 +508,7 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 	Monero autosigning operations (compat mode)
 	"""
 	menu_prompt = 'efresh balances:\b'
+	listaddresses_menu_prompt = 'efresh bals:\b'
 	extra_daemons = ['ltc']
 
 	cmd_group = (
@@ -521,7 +522,7 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 		('check_bal_alice2',         'mining, checking balance (wallet #2)'),
 		('fund_alice1',              'sending funds to Alice (wallet #1)'),
 		('mine_blocks_10',           'mining some blocks'),
-		('alice_listaddresses1',     'adding label to Alice’s tracking wallets (listaddresses)'),
+		('alice_listaddresses_lbl',  'adding label to Alice’s tracking wallets (listaddresses)'),
 		('fund_alice1b',             'sending funds to Alice (wallet #1)'),
 		('mine_blocks_10',           'mining some blocks'),
 		('alice_twview1',            'adding label to Alice’s tracking wallets (twview)'),
@@ -536,7 +537,7 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 		('fund_alice_sub3',          'sending funds to Alice’s subaddress #3 (wallet #2)'),
 		('alice_twview2',            'viewing Alice’s tracking wallets (reload, sort options)'),
 		('alice_twview3',            'viewing Alice’s tracking wallets (check balances)'),
-		('alice_listaddresses2',     'listing Alice’s addresses (sort options)'),
+		('alice_listaddresses_sort', 'listing Alice’s addresses (sort options)'),
 		('wait_loop_start_compat',   'starting autosign wait loop in XMR compat mode [--coins=xmr]'),
 		('alice_txcreate1',          'creating a transaction'),
 		('alice_txabort1',           'aborting the transaction'),
@@ -548,6 +549,14 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 		('wait_loop_start_ltc',      'starting autosign wait loop in XMR compat mode [--coins=ltc,xmr]'),
 		('alice_txsend1',            'sending the transaction'),
 		('wait_loop_kill',           'stopping autosign wait loop'),
+		('alice_newacct1',           'adding account to Alice’s tracking wallet (dfl label)'),
+		('alice_newacct2',           'adding account to Alice’s tracking wallet (no timestr)'),
+		('alice_newacct3',           'adding account to Alice’s tracking wallet'),
+		('alice_newacct4',           'adding account to Alice’s tracking wallet (dfl label, no timestr)'),
+		('alice_newaddr1',           'adding address to Alice’s tracking wallet'),
+		('alice_newaddr2',           'adding address to Alice’s tracking wallet (no timestr)'),
+		('alice_newaddr3',           'adding address to Alice’s tracking wallet (dfl label)'),
+		('alice_newaddr4',           'adding address to Alice’s tracking wallet (dfl label, no timestr)'),
 		('stop_daemons',             'stopping all wallet and coin daemons'),
 	)
 
@@ -600,12 +609,59 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 		addr_data = data['MoneroMMGenWalletDumpFile']['data']['wallet_metadata'][1]['addresses']
 		return await self.fund_alice(addr=addr_data[addr_num-1]['address'], amt=amt)
 
-	def alice_listaddresses1(self):
+	def alice_newacct1(self):
+		return self._alice_newacct(2, lbl_text='New Test Account', add_timestr=True)
+
+	def alice_newacct2(self):
+		return self._alice_newacct(1, lbl_text='New Test Account')
+
+	def alice_newacct3(self):
+		return self._alice_newacct(2, add_timestr=True)
+
+	def alice_newacct4(self):
+		return self._alice_newacct(2)
+
+	def _alice_newacct(self, wallet_num, lbl_text='', add_timestr=False):
+		return self._alice_twops(
+			'listaddresses',
+			newacct_wallet_num = wallet_num,
+			lbl_text = lbl_text,
+			add_timestr = add_timestr,
+			expect_str = (lbl_text or 'new account ') + (r'.*\[20.*\]' if add_timestr else ''))
+
+	def alice_newaddr1(self):
+		return self._alice_newaddr(1, lbl_text='New Test Address', add_timestr=True)
+
+	def alice_newaddr2(self):
+		return self._alice_newaddr(1, lbl_text='New Test Address')
+
+	def alice_newaddr3(self):
+		return self._alice_newaddr(2, add_timestr=True)
+
+	def alice_newaddr4(self):
+		return self._alice_newaddr(2)
+
+	def _alice_newaddr(self, acct_num, lbl_text='', add_timestr=False):
 		return self._alice_twops(
 			'listaddresses',
-			lbl_addr_num = 2,
-			lbl_addr_idx_num = 0,
-			lbl_add_timestr = True,
+			newaddr_acct_num = acct_num,
+			lbl_text = lbl_text,
+			add_timestr = add_timestr,
+			expect_str = (lbl_text or 'new address ') + (r'.*\[20.*\]' if add_timestr else ''))
+
+	def alice_listaddresses(self):
+		return self._alice_twops('listaddresses', menu='R')
+
+	def alice_listaddresses_sort(self):
+		return self._alice_twops('listaddresses', menu='aAdMELLuuuraAdMeEuu')
+
+	def alice_listaddresses_lbl(self):
+		return self._alice_twops(
+			'listaddresses',
+			lbl_acct_num = 2,
+			lbl_addr_idx = 0,
+			lbl_text = 'New Test Label',
+			add_timestr = True,
 			menu = 'R',
 			expect_str = r'Primary account.*1\.234567891234')
 
@@ -615,10 +671,11 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 	def alice_twview1(self):
 		return self._alice_twops(
 			'twview',
-			lbl_addr_num = 1,
-			lbl_addr_idx_num = 0,
+			lbl_acct_num = 1,
+			lbl_addr_idx = 0,
+			lbl_text = 'New Test Label',
 			menu = 'R',
-			expect_str = r'New Label.*2\.469135782468')
+			expect_str = r'New Test Label.*2\.469135782468')
 
 	def alice_twview2(self):
 		return self._alice_twops('twview', menu='RaAdMraAdMe')
@@ -626,26 +683,27 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 	def alice_twview3(self):
 		return self._alice_twops(
 			'twview',
+			interactive = False,
 			expect_arr = [
 				'Total XMR: 3.722345649021 [3.729999970119]',
 				'1         0.026296296417',
 				'0.007654321098'])
 
-	def alice_listaddresses2(self):
-		return self._alice_twops('listaddresses', menu='aAdMELLuuuraAdMeEuu')
-
 	def _alice_twops(
 			self,
 			op,
 			*,
-			lbl_addr_num = None,
-			lbl_addr_idx_num = None,
-			lbl_add_timestr = False,
+			lbl_acct_num = None,
+			lbl_addr_idx = None,
+			newacct_wallet_num = None,
+			newaddr_acct_num = None,
+			lbl_text = '',
+			add_timestr = False,
 			menu = '',
+			interactive = True,
 			expect_str = '',
 			expect_arr = []):
 
-		interactive = not expect_arr
 		self.insert_device_online()
 		t = self.spawn(
 			'mmgen-tool',
@@ -653,23 +711,35 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 			+ self.autosign_opts
 			+ [op]
 			+ (['interactive=1'] if interactive else []))
+		menu_prompt = self.listaddresses_menu_prompt if op == 'listaddresses' else self.menu_prompt
+		have_lbl = lbl_acct_num or newacct_wallet_num or newaddr_acct_num
+		have_new_addr = newacct_wallet_num or newaddr_acct_num
 		if interactive:
-			if lbl_addr_num:
-				t.expect(self.menu_prompt, 'l')
-				t.expect('main menu): ', str(lbl_addr_num))
-				if lbl_addr_idx_num is not None:
-					t.expect('main menu): ', str(lbl_addr_idx_num))
-				t.expect(': ', 'New Label\n')
-				t.expect('(y/N): ', 'y' if lbl_add_timestr else 'n')
+			if lbl_acct_num:
+				t.expect(menu_prompt, 'l')
+				t.expect('main menu): ', str(lbl_acct_num))
+				t.expect('main menu): ', str(lbl_addr_idx))
+			elif newacct_wallet_num:
+				t.expect(menu_prompt, 'N')
+				t.expect('number> ', f'{newacct_wallet_num}\n')
+			elif newaddr_acct_num:
+				t.expect(menu_prompt, 'n')
+				t.expect('main menu): ', str(newaddr_acct_num))
+			if have_lbl:
+				t.expect(': ', lbl_text + '\n')                    # add label
+				t.expect('(y/N): ', ('y' if add_timestr else 'n')) # add timestr
+			if have_new_addr:
+				t.expect('(y/N): ', 'y')
 			for ch in menu:
-				t.expect(self.menu_prompt, ch)
+				t.expect(menu_prompt, ch)
 			if expect_str:
 				t.expect(expect_str, regex=True)
-			t.expect(self.menu_prompt, 'q')
+			t.expect(menu_prompt, 'q')
 		elif expect_arr:
 			text = strip_ansi_escapes(t.read())
 			for s in expect_arr:
 				assert s in text
+		t.read()
 		self.remove_device_online()
 		return t
 

+ 3 - 4
test/cmdtest_d/xmrwallet.py

@@ -375,10 +375,9 @@ class CmdTestXMRWallet(CmdTestBase):
 
 	async def fund_alice(self, wallet=1, amt=1234567891234, addr=None):
 		self.spawn(msg_only=True, extra_desc='(transferring funds from Miner wallet)')
-		await self.transfer(
-			'miner',
-			amt,
-			addr or read_from_file(self.users['alice'].addrfile_fs.format(wallet)))
+		addr = addr or read_from_file(self.users['alice'].addrfile_fs.format(wallet))
+		imsg(f'Dest: {addr}')
+		await self.transfer('miner', amt, addr)
 		return 'ok'
 
 	async def check_bal_alice(self, wallet=1, bal='1.234567891234'):