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
 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):
 class MoneroTwAddresses(MoneroTwView, TwAddresses):
 
 
 	include_empty = True
 	include_empty = True
@@ -23,8 +47,10 @@ class MoneroTwAddresses(MoneroTwView, TwAddresses):
 
 
 	prompt_fs_repl = {'XMR': (
 	prompt_fs_repl = {'XMR': (
 		(1, 'Filters: show [E]mpty addrs, [u]sed addrs, all [L]abels'),
 		(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 = {
 	extra_key_mappings = {
+		'N': 'a_acct_new',
+		'n': 'i_addr_new',
 		'u': 'd_showused',
 		'u': 'd_showused',
 		'R': 'a_sync_wallets'}
 		'R': 'a_sync_wallets'}
 	removed_key_mappings = {
 	removed_key_mappings = {
@@ -40,3 +66,36 @@ class MoneroTwAddresses(MoneroTwView, TwAddresses):
 
 
 	def get_disp_data(self):
 	def get_disp_data(self):
 		return MoneroTwView.get_disp_data(self, input_data=tuple(TwAddresses.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,
 			None,
 			spec = f'{m.idx}:{m.acct_idx}:{m.addr_idx},{comment}',
 			spec = f'{m.idx}:{m.acct_idx}:{m.addr_idx},{comment}',
 			compat_call = True)
 			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)
 		op = xmrwallet_op('dump_data', self.cfg, None, None, compat_call=True)
 		await op.restart_wallet_daemon()
 		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')
 		self.total = self.unlocked_total = self.proto.coin_amt('0')
 
 
 		def gen_addrs():
 		def gen_addrs():
 			bd = namedtuple('address_balance_data', ['bal', 'unlocked_bal', 'blocks_to_unlock'])
 			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']))}
 				bals_data = {i: {} for i in range(len(wdata['data'].accts_data['subaddress_accounts']))}
 
 
 				for d in wdata['data'].bals_data.get('per_subaddress', []):
 				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',
 				color_override = None if d.amt == d.unlocked_amt else 'orange',
 				prec = self.disp_prec))
 				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 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(
 			return await self.get_idx(
 				'address index',
 				'address index',
 				self.accts_data[res.idx - 1].data,
 				self.accts_data[res.idx - 1].data,
@@ -220,8 +222,7 @@ class MoneroTwView:
 	class action(TwView.action):
 	class action(TwView.action):
 
 
 		async def a_sync_wallets(self, parent):
 		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('')
 			msg('')
 			try:
 			try:
 				op = xmrwallet_op('sync', parent.cfg, None, None, compat_call=True)
 				op = xmrwallet_op('sync', parent.cfg, None, None, compat_call=True)
@@ -230,10 +231,9 @@ class MoneroTwView:
 					import asyncio
 					import asyncio
 					ymsg(str(e))
 					ymsg(str(e))
 					await asyncio.sleep(2)
 					await asyncio.sleep(2)
-					msg_r(CUR_HOME + ERASE_ALL)
 					return False
 					return False
 				raise
 				raise
 			await op.restart_wallet_daemon()
 			await op.restart_wallet_daemon()
 			await op.main()
 			await op.main()
 			await parent.get_data()
 			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
 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 ..obj import MMGenListItem, ImmutableAttr, ListItemAttr, TwComment, NonNegativeInt
 from ..addr import CoinAddr, MMGenID, MMGenAddrType
 from ..addr import CoinAddr, MMGenID, MMGenAddrType
 from ..amt import CoinAmtChk
 from ..amt import CoinAmtChk
@@ -403,24 +403,12 @@ class TwAddresses(TwView):
 		"""
 		"""
 
 
 		def choose_address(addrs):
 		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):
 		def get_addr(mmtype):
 			return [self.get_change_address(
 			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
 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 ..color import red, green, gray, yellow
 from ..obj import ListItemAttr
 from ..obj import ListItemAttr
 from .addresses import TwAddresses
 from .addresses import TwAddresses
-from .view import CUR_HOME, ERASE_ALL
 
 
 class TwAddressesPrune(TwAddresses):
 class TwAddressesPrune(TwAddresses):
 
 
@@ -155,15 +154,12 @@ class TwAddressesPrune(TwAddresses):
 				else:
 				else:
 					e.tag = True
 					e.tag = True
 
 
-			if parent.scroll:
-				msg_r(CUR_HOME + ERASE_ALL)
+			return 'erase'
 
 
 		async def a_unprune(self, parent):
 		async def a_unprune(self, parent):
 			for addrnum in self.get_addrnums(parent, 'unprune'):
 			for addrnum in self.get_addrnums(parent, 'unprune'):
 				parent.disp_data[addrnum-1].tag = False
 				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):
 		async def a_clear_prune_list(self, parent):
 			for d in parent.data:
 			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! '))
 				msg_r('\r'+''.ljust(self.term_width)+'\r'+yellow('Canceling! '))
 			return False
 			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)
 		return await self.get_idx(f'{self.item_desc} number', self.disp_data)
 
 
 	async def get_idx(self, desc, data, *, is_addr_idx=False):
 	async def get_idx(self, desc, data, *, is_addr_idx=False):
@@ -692,11 +692,19 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 					return ur(idx, None)
 					return ur(idx, None)
 			await do_error_msg()
 			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:
 	class action:
 
 
 		@enable_echo
 		@enable_echo
 		async def run(self, parent, action_method):
 		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):
 		async def a_print_detail(self, parent):
 			return await self._print(parent, output_type='detail')
 			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))
 			do_pager(await parent.format('detail', color=True))
 
 
 	class item_action:
 	class item_action:
+		acct_methods = ()
 
 
 		@enable_echo
 		@enable_echo
 		async def run(self, parent, action_method):
 		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
 				#  None:   action aborted by user or no action performed
 				#  'redo': user will be re-prompted for item number
 				#  'redo': user will be re-prompted for item number
 				#  'redraw': action successfully performed, screen will be redrawn
 				#  '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)
 					ret = await action_method(parent, usr_ret.idx, usr_ret.acct_addr_idx)
 				else:
 				else:
 					ret = None
 					ret = None
@@ -770,11 +780,7 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 					break
 					break
 				await asyncio.sleep(0.5)
 				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):
 		async def i_balance_refresh(self, parent, idx, acct_addr_idx=None):
 			if not parent.keypress_confirm(
 			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 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:
 			if acct_addr_idx is None:
 				desc       = f'{parent.item_desc} #{idx}'
 				desc       = f'{parent.item_desc} #{idx}'
 				color_desc = f'{parent.item_desc} {red("#" + str(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)'))
 			msg('Current label: {}'.format(old_comment.hl() if old_comment else '(none)'))
 
 
 			from ..ui import line_input
 			from ..ui import line_input
-			match res:= line_input(
+			match comment_in := line_input(
 					parent.cfg,
 					parent.cfg,
 					f'Enter label text for {color_desc}: ',
 					f'Enter label text for {color_desc}: ',
 					insert_txt = old_comment):
 					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?'):
 					if not parent.keypress_confirm(f'Removing label for {color_desc}. OK?'):
 						return 'redo'
 						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:
 	class scroll_action:
 
 

+ 16 - 1
mmgen/ui.py

@@ -14,7 +14,7 @@ ui: Interactive user interface functions for the MMGen suite
 
 
 import sys, os
 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'):
 def confirm_or_raise(cfg, message, action, *, expect='YES', exit_msg='Exiting at user request'):
 	if message:
 	if message:
@@ -117,6 +117,21 @@ def keypress_confirm(
 			case _:
 			case _:
 				msg_r('\nInvalid reply\n' if verbose else '\r')
 				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):
 def do_pager(text):
 
 
 	pagers = ['less', 'more']
 	pagers = ['less', 'more']

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

@@ -29,7 +29,7 @@ class OpLabel(OpMixinSpec, OpWallet):
 	opts     = ()
 	opts     = ()
 	wallet_offline = True
 	wallet_offline = True
 
 
-	async def main(self, add_timestr='ask', auto=False):
+	async def main(self, add_timestr='ask'):
 
 
 		if not self.compat_call:
 		if not self.compat_call:
 			gmsg('\n{a} label for wallet {b}, account #{c}, address #{d}'.format(
 			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 = 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
 		max_acct = len(wallet_data.accts_data['subaddress_accounts']) - 1
 		if self.account > max_acct:
 		if self.account > max_acct:
 			die(2, f'{self.account}: requested account index out of bounds (>{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:
 		if self.address_idx > len(ret) - 1:
 			die(2, '{}: requested address index out of bounds (>{})'.format(
 			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
 			(self.label + (f' [{make_timestr()}]' if add_timestr else '')) if self.label
 			else '')
 			else '')
 
 
-		if not auto:
+		if not self.compat_call:
 			ca = CoinAddr(self.proto, addr['address'])
 			ca = CoinAddr(self.proto, addr['address'])
 			from . import addr_width
 			from . import addr_width
 			msg('\n  {a} {b}\n  {c} {d}\n  {e} {f}'.format(
 			msg('\n  {a} {b}\n  {c} {d}\n  {e} {f}'.format(
@@ -76,7 +76,7 @@ class OpLabel(OpMixinSpec, OpWallet):
 
 
 		if addr['label'] == new_label:
 		if addr['label'] == new_label:
 			ymsg('\nLabel is unchanged, operation cancelled')
 			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)
 			h.set_label(self.account, self.address_idx, new_label)
 			ret = h.print_acct_addrs(h.get_wallet_data(print=False), self.account)
 			ret = h.print_acct_addrs(h.get_wallet_data(print=False), self.account)
 			label_chk = ret[self.address_idx]['label']
 			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'),)
 	spec_key = ((1, 'source'),)
 	wallet_offline = True
 	wallet_offline = True
 
 
-	async def main(self):
+	async def main(self, add_timestr=True):
 		h = MoneroWalletRPC(self, self.source)
 		h = MoneroWalletRPC(self, self.source)
-		h.open_wallet('Monero')
+		h.open_wallet('Monero', refresh=False)
 
 
 		desc = 'account' if self.account is None else 'address'
 		desc = 'account' if self.account is None else 'address'
 		label = TwComment(
 		label = TwComment(
 			None if self.label == '' else
 			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()
 		wallet_data = h.get_wallet_data()
 
 
@@ -58,11 +60,14 @@ class OpNew(OpMixinSpec, OpWallet):
 
 
 			if desc == 'address':
 			if desc == 'address':
 				h.print_acct_addrs(wallet_data, self.account)
 				h.print_acct_addrs(wallet_data, self.account)
+			ret = True
 		else:
 		else:
 			ymsg('\nOperation cancelled by user request')
 			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
 		# 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:
 		if self.cfg.no_stop_wallet_daemon:
 			h.close_wallet('Monero')
 			h.close_wallet('Monero')
 
 
 		msg('')
 		msg('')
+		return ret

+ 95 - 25
test/cmdtest_d/xmr_autosign.py

@@ -508,6 +508,7 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 	Monero autosigning operations (compat mode)
 	Monero autosigning operations (compat mode)
 	"""
 	"""
 	menu_prompt = 'efresh balances:\b'
 	menu_prompt = 'efresh balances:\b'
+	listaddresses_menu_prompt = 'efresh bals:\b'
 	extra_daemons = ['ltc']
 	extra_daemons = ['ltc']
 
 
 	cmd_group = (
 	cmd_group = (
@@ -521,7 +522,7 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 		('check_bal_alice2',         'mining, checking balance (wallet #2)'),
 		('check_bal_alice2',         'mining, checking balance (wallet #2)'),
 		('fund_alice1',              'sending funds to Alice (wallet #1)'),
 		('fund_alice1',              'sending funds to Alice (wallet #1)'),
 		('mine_blocks_10',           'mining some blocks'),
 		('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)'),
 		('fund_alice1b',             'sending funds to Alice (wallet #1)'),
 		('mine_blocks_10',           'mining some blocks'),
 		('mine_blocks_10',           'mining some blocks'),
 		('alice_twview1',            'adding label to Alice’s tracking wallets (twview)'),
 		('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)'),
 		('fund_alice_sub3',          'sending funds to Alice’s subaddress #3 (wallet #2)'),
 		('alice_twview2',            'viewing Alice’s tracking wallets (reload, sort options)'),
 		('alice_twview2',            'viewing Alice’s tracking wallets (reload, sort options)'),
 		('alice_twview3',            'viewing Alice’s tracking wallets (check balances)'),
 		('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]'),
 		('wait_loop_start_compat',   'starting autosign wait loop in XMR compat mode [--coins=xmr]'),
 		('alice_txcreate1',          'creating a transaction'),
 		('alice_txcreate1',          'creating a transaction'),
 		('alice_txabort1',           'aborting the 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]'),
 		('wait_loop_start_ltc',      'starting autosign wait loop in XMR compat mode [--coins=ltc,xmr]'),
 		('alice_txsend1',            'sending the transaction'),
 		('alice_txsend1',            'sending the transaction'),
 		('wait_loop_kill',           'stopping autosign wait loop'),
 		('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'),
 		('stop_daemons',             'stopping all wallet and coin daemons'),
 	)
 	)
 
 
@@ -600,12 +609,59 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 		addr_data = data['MoneroMMGenWalletDumpFile']['data']['wallet_metadata'][1]['addresses']
 		addr_data = data['MoneroMMGenWalletDumpFile']['data']['wallet_metadata'][1]['addresses']
 		return await self.fund_alice(addr=addr_data[addr_num-1]['address'], amt=amt)
 		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(
 		return self._alice_twops(
 			'listaddresses',
 			'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',
 			menu = 'R',
 			expect_str = r'Primary account.*1\.234567891234')
 			expect_str = r'Primary account.*1\.234567891234')
 
 
@@ -615,10 +671,11 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 	def alice_twview1(self):
 	def alice_twview1(self):
 		return self._alice_twops(
 		return self._alice_twops(
 			'twview',
 			'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',
 			menu = 'R',
-			expect_str = r'New Label.*2\.469135782468')
+			expect_str = r'New Test Label.*2\.469135782468')
 
 
 	def alice_twview2(self):
 	def alice_twview2(self):
 		return self._alice_twops('twview', menu='RaAdMraAdMe')
 		return self._alice_twops('twview', menu='RaAdMraAdMe')
@@ -626,26 +683,27 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 	def alice_twview3(self):
 	def alice_twview3(self):
 		return self._alice_twops(
 		return self._alice_twops(
 			'twview',
 			'twview',
+			interactive = False,
 			expect_arr = [
 			expect_arr = [
 				'Total XMR: 3.722345649021 [3.729999970119]',
 				'Total XMR: 3.722345649021 [3.729999970119]',
 				'1         0.026296296417',
 				'1         0.026296296417',
 				'0.007654321098'])
 				'0.007654321098'])
 
 
-	def alice_listaddresses2(self):
-		return self._alice_twops('listaddresses', menu='aAdMELLuuuraAdMeEuu')
-
 	def _alice_twops(
 	def _alice_twops(
 			self,
 			self,
 			op,
 			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 = '',
 			menu = '',
+			interactive = True,
 			expect_str = '',
 			expect_str = '',
 			expect_arr = []):
 			expect_arr = []):
 
 
-		interactive = not expect_arr
 		self.insert_device_online()
 		self.insert_device_online()
 		t = self.spawn(
 		t = self.spawn(
 			'mmgen-tool',
 			'mmgen-tool',
@@ -653,23 +711,35 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 			+ self.autosign_opts
 			+ self.autosign_opts
 			+ [op]
 			+ [op]
 			+ (['interactive=1'] if interactive else []))
 			+ (['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 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:
 			for ch in menu:
-				t.expect(self.menu_prompt, ch)
+				t.expect(menu_prompt, ch)
 			if expect_str:
 			if expect_str:
 				t.expect(expect_str, regex=True)
 				t.expect(expect_str, regex=True)
-			t.expect(self.menu_prompt, 'q')
+			t.expect(menu_prompt, 'q')
 		elif expect_arr:
 		elif expect_arr:
 			text = strip_ansi_escapes(t.read())
 			text = strip_ansi_escapes(t.read())
 			for s in expect_arr:
 			for s in expect_arr:
 				assert s in text
 				assert s in text
+		t.read()
 		self.remove_device_online()
 		self.remove_device_online()
 		return t
 		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):
 	async def fund_alice(self, wallet=1, amt=1234567891234, addr=None):
 		self.spawn(msg_only=True, extra_desc='(transferring funds from Miner wallet)')
 		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'
 		return 'ok'
 
 
 	async def check_bal_alice(self, wallet=1, bal='1.234567891234'):
 	async def check_bal_alice(self, wallet=1, bal='1.234567891234'):