7 Commits 7b53f4337f ... 907bdc2bdf

Author SHA1 Message Date
  The MMGen Project 907bdc2bdf XMR compat: sweep transactions 2 months ago
  The MMGen Project c9e79dc630 help.txcreate_examples: cleanups 2 months ago
  The MMGen Project 71b3083678 cmdtest.py xmr_compat: cleanups 2 months ago
  The MMGen Project ff38069843 tw, tx: minor fixes and cleanups 2 months ago
  The MMGen Project 3bfad0c1d0 xmrwallet.ops: refactor `mount_removable_device()` 2 months ago
  The MMGen Project 0c0a8626ad proto.xmr.tw.view: new `get_label_from_user()`, `choose_wallet()` methods 2 months ago
  The MMGen Project 0e643aadf2 autosign.py: reorder signables for XMR compat (bugfix) 2 months ago

+ 4 - 2
mmgen/autosign.py

@@ -568,8 +568,10 @@ class Autosign:
 		if self.have_xmr:
 		if self.have_xmr:
 			self.dirs |= self.xmr_dirs | (
 			self.dirs |= self.xmr_dirs | (
 				{'txauto_dir': 'txauto'} if cfg.xmrwallet_compat and self.xmr_only else {})
 				{'txauto_dir': 'txauto'} if cfg.xmrwallet_compat and self.xmr_only else {})
-			self.signables += Signable.xmr_signables + (
-				('automount_transaction',) if cfg.xmrwallet_compat and self.xmr_only else ())
+			self.signables = (
+				Signable.xmr_signables # xmr_wallet_outputs_file must be signed before XMR TXs
+				+ (('automount_transaction',) if cfg.xmrwallet_compat and self.xmr_only else ())
+				+ self.signables)      # self.signables could contain compat XMR TXs
 
 
 		for name, path in self.dirs.items():
 		for name, path in self.dirs.items():
 			setattr(self, name, self.mountpoint / path)
 			setattr(self, name, self.mountpoint / path)

+ 1 - 1
mmgen/data/version

@@ -1 +1 @@
-16.1.dev25
+16.1.dev26

+ 1 - 1
mmgen/help/help_notes.py

@@ -23,7 +23,7 @@ class help_notes:
 			case 'Bitcoin':
 			case 'Bitcoin':
 				return '[ADDR,AMT ... | DATA_SPEC] ADDR [addr file ...]'
 				return '[ADDR,AMT ... | DATA_SPEC] ADDR [addr file ...]'
 			case 'Monero':
 			case 'Monero':
-				return 'ADDR,AMT'
+				return '[ADDR,AMT]'
 			case _:
 			case _:
 				return 'ADDR,AMT [addr file ...]'
 				return 'ADDR,AMT [addr file ...]'
 
 

+ 15 - 9
mmgen/help/txcreate_examples.py

@@ -21,6 +21,8 @@ def help(proto, cfg):
 	t = tool_cmd(cfg, mmtype=mmtype)
 	t = tool_cmd(cfg, mmtype=mmtype)
 	addr = t.privhex2addr('bead' * 16)
 	addr = t.privhex2addr('bead' * 16)
 	sample_addr = addr.views[addr.view_pref]
 	sample_addr = addr.views[addr.view_pref]
+	cmd_base = gc.prog_name + ('' if proto.coin == 'BTC' else f' --coin={proto.coin.lower()}')
+	action = 'Create' if gc.prog_name == 'mmgen-txcreate' else 'Execute'
 
 
 	match proto.base_proto:
 	match proto.base_proto:
 		case 'Bitcoin':
 		case 'Bitcoin':
@@ -30,30 +32,30 @@ EXAMPLES:
   Send 0.123 {proto.coin} to an external {proto.name} address, returning the change to a
   Send 0.123 {proto.coin} to an external {proto.name} address, returning the change to a
   specific MMGen address in the tracking wallet:
   specific MMGen address in the tracking wallet:
 
 
-    $ {gc.prog_name} {sample_addr},0.123 01ABCDEF:{mmtype}:7
+    $ {cmd_base} {sample_addr},0.123 01ABCDEF:{mmtype}:7
 
 
   Same as above, but select the change address automatically:
   Same as above, but select the change address automatically:
 
 
-    $ {gc.prog_name} {sample_addr},0.123 01ABCDEF:{mmtype}
+    $ {cmd_base} {sample_addr},0.123 01ABCDEF:{mmtype}
 
 
   Same as above, but select the change address automatically by address type:
   Same as above, but select the change address automatically by address type:
 
 
-    $ {gc.prog_name} {sample_addr},0.123 {mmtype}
+    $ {cmd_base} {sample_addr},0.123 {mmtype}
 
 
   Same as above, but reduce verbosity and specify fee of 20 satoshis
   Same as above, but reduce verbosity and specify fee of 20 satoshis
   per byte:
   per byte:
 
 
-    $ {gc.prog_name} -q -f 20s {sample_addr},0.123 {mmtype}
+    $ {cmd_base} -q -f 20s {sample_addr},0.123 {mmtype}
 
 
   Send entire balance of selected inputs minus fee to an external {proto.name}
   Send entire balance of selected inputs minus fee to an external {proto.name}
   address:
   address:
 
 
-    $ {gc.prog_name} {sample_addr}
+    $ {cmd_base} {sample_addr}
 
 
   Send entire balance of selected inputs minus fee to first unused wallet
   Send entire balance of selected inputs minus fee to first unused wallet
   address of specified type:
   address of specified type:
 
 
-    $ {gc.prog_name} {mmtype}
+    $ {cmd_base} {mmtype}
 """
 """
 
 
 		case 'Monero':
 		case 'Monero':
@@ -62,7 +64,11 @@ EXAMPLES:
 
 
   Send 0.123 {proto.coin} to an external {proto.name} address:
   Send 0.123 {proto.coin} to an external {proto.name} address:
 
 
-    $ {gc.prog_name} {sample_addr},0.123
+    $ {cmd_base} {sample_addr},0.123
+
+  {action} a sweep transaction:
+
+    $ {cmd_base}
 """
 """
 
 
 		case _:
 		case _:
@@ -71,9 +77,9 @@ EXAMPLES:
 
 
   Send 0.123 {proto.coin} to an external {proto.name} address:
   Send 0.123 {proto.coin} to an external {proto.name} address:
 
 
-    $ {gc.prog_name} {sample_addr},0.123
+    $ {cmd_base} {sample_addr},0.123
 
 
   Send 0.123 {proto.coin} to another account in wallet 01ABCDEF:
   Send 0.123 {proto.coin} to another account in wallet 01ABCDEF:
 
 
-    $ {gc.prog_name} 01ABCDEF:{mmtype}:7,0.123
+    $ {cmd_base} 01ABCDEF:{mmtype}:7,0.123
 """
 """

+ 3 - 2
mmgen/main_txcreate.py

@@ -130,8 +130,9 @@ if cfg.list_assets:
 	Msg('AVAILABLE SWAP ASSETS:\n' + sp.SwapAsset('BTC', 'send').fmt_assets_data(indent='  '))
 	Msg('AVAILABLE SWAP ASSETS:\n' + sp.SwapAsset('BTC', 'send').fmt_assets_data(indent='  '))
 	sys.exit(0)
 	sys.exit(0)
 
 
-if not (cfg.info or cfg.contract_data) and len(cfg._args) < {'tx': 1, 'swaptx': 2}[target]:
-	cfg._usage()
+if not (cfg.info or cfg.contract_data or cfg.coin == 'XMR'):
+	if len(cfg._args) < {'tx': 1, 'swaptx': 2}[target]:
+		cfg._usage()
 
 
 async def main():
 async def main():
 
 

+ 8 - 24
mmgen/proto/xmr/tw/addresses.py

@@ -17,13 +17,9 @@ from ....tw.addresses import TwAddresses
 from .view import MoneroTwView
 from .view import MoneroTwView
 
 
 async def add_new_address(parent, spec, ok_msg):
 async def add_new_address(parent, spec, ok_msg):
-	from ....ui import line_input, keypress_confirm
 	from ....color import green, yellow
 	from ....color import green, yellow
 	from ....xmrwallet import op as xmrwallet_op
 	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?')
+	lbl, add_timestr = parent.get_label_from_user()
 	op = xmrwallet_op(
 	op = xmrwallet_op(
 		'new',
 		'new',
 		parent.cfg,
 		parent.cfg,
@@ -70,25 +66,13 @@ class MoneroTwAddresses(MoneroTwView, TwAddresses):
 	class action(MoneroTwView.action):
 	class action(MoneroTwView.action):
 
 
 		async def a_acct_new(self, parent):
 		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()}')
+			if res := parent.choose_wallet('Choose a wallet to add a new account to'):
+				return await add_new_address(
+					parent,
+					str(res.item[0]),
+					f'New account added to wallet {parent.make_wallet_id(res.item[0]).hl()}')
+			else:
+				return 'erase'
 
 
 	class item_action(TwAddresses.item_action):
 	class item_action(TwAddresses.item_action):
 		acct_methods = ('i_addr_new')
 		acct_methods = ('i_addr_new')

+ 45 - 8
mmgen/proto/xmr/tw/unspent.py

@@ -22,11 +22,48 @@ class MoneroTwUnspentOutputs(MoneroTwView, TwUnspentOutputs):
 	desc = 'spendable accounts'
 	desc = 'spendable accounts'
 	include_empty = False
 	include_empty = False
 
 
-	prompt_fs_in = [
-		'Sort options: [a]mount, [A]ge, a[d]dr, [M]mgen addr, [r]everse',
-		'Display options: r[e]draw screen',
-		'View/Print: pager [v]iew, [w]ide pager view, [p]rint to file{s}',
-		'Actions: [q]uit menu, add [l]abel, [R]efresh balances:']
-	extra_key_mappings = {
-		'R': 'a_sync_wallets',
-		'A': 's_age'}
+	async def __init__(self, cfg, proto, *, minconf=1, addrs=[], tx=None):
+		self.prompt_fs_in = [
+			'Sort options: [a]mount, [A]ge, a[d]dr, [M]mgen addr, [r]everse',
+			'View/Print: pager [v]iew, [w]ide pager view, [p]rint to file{s}',
+			'Actions: [q]uit menu, add [l]abel, r[e]draw, [R]efresh balances:']
+		self.extra_key_mappings = {
+			'R': 'a_sync_wallets',
+			'A': 's_age'}
+		if tx and tx.is_sweep:
+			self.prompt_fs_in[-1] = 'Actions: [q]uit, add [l]abel, r[e]draw, [R]efresh balances:'
+			self.prompt_fs_in.insert(-1, 'Transaction ops: [s]weep to address, [S]weep to account')
+			self.extra_key_mappings.update({
+				's': 'i_addr_sweep',
+				'S': 'i_acct_sweep'})
+		await super().__init__(cfg, proto, minconf=minconf, addrs=addrs, tx=tx)
+
+	async def get_idx_from_user(self, method_name):
+		if method_name in ('i_acct_sweep', 'i_addr_sweep'):
+			from collections import namedtuple
+			ret = []
+			for acct_desc in {
+					'i_acct_sweep': ['source', 'destination'],
+					'i_addr_sweep': ['source']}[method_name]:
+				if res := await self.get_idx(f'{acct_desc} account number', self.accts_data):
+					ret.append(res)
+				else:
+					return None
+			return namedtuple('usr_idx_data', 'idx acct_addr_idx', defaults=[None])(*ret)
+		else:
+			return await super().get_idx_from_user(method_name)
+
+	class item_action(TwUnspentOutputs.item_action):
+		acct_methods = ('i_acct_sweep', 'i_addr_sweep')
+
+		async def i_acct_sweep(self, parent, idx, acct_addr_idx=None):
+			d = parent.accts_data
+			d1 = d[idx.idx - 1]
+			d2 = d[acct_addr_idx.idx - 1]
+			parent.tx.sweep_spec = f'{d1.idx}:{d1.acct_idx},{d2.idx}:{d2.acct_idx}'
+			return 'quit_view'
+
+		async def i_addr_sweep(self, parent, idx, acct_addr_idx=None):
+			d = parent.accts_data[idx.idx - 1]
+			parent.tx.sweep_spec = f'{d.idx}:{d.acct_idx}'
+			return 'quit_view'

+ 28 - 0
mmgen/proto/xmr/tw/view.py

@@ -219,6 +219,34 @@ class MoneroTwView:
 				self.accts_data[res.idx - 1].data,
 				self.accts_data[res.idx - 1].data,
 				is_addr_idx = True)
 				is_addr_idx = True)
 
 
+	def get_label_from_user(self):
+		from ....ui import line_input, keypress_confirm
+		lbl = line_input(
+			self.cfg,
+			'Enter label text for new address (or ENTER for default label): ')
+		add_timestr = keypress_confirm(self.cfg, 'Add timestamp to label?')
+		return lbl, add_timestr
+
+	def make_wallet_id(self, wnum):
+		from ....addr import MMGenID
+		return MMGenID(proto=self.proto, id_str='{}:M:{}'.format(self.sid, wnum))
+
+	def choose_wallet(self, prompt):
+		from ....obj import Int
+		from ....util import msg, suf
+		from ....ui import item_chooser
+		msg('\n')
+		return item_chooser(
+			self.cfg,
+			prompt,
+			[(d['wallet_num'], len(d['data'].accts_data['subaddress_accounts']))
+				for d in self.dump_data],
+			lambda d: '{a} [{b} account{c}]'.format(
+				a = self.make_wallet_id(d[0]).hl(),
+				b = Int(d[1]).hl(),
+				c = suf(d[1])),
+			empty_ok = True)
+
 	class action(TwView.action):
 	class action(TwView.action):
 
 
 		async def a_sync_wallets(self, parent):
 		async def a_sync_wallets(self, parent):

+ 28 - 4
mmgen/proto/xmr/tx/new.py

@@ -18,6 +18,11 @@ from .base import Base
 
 
 class New(Base, TxNew):
 class New(Base, TxNew):
 
 
+	async def create(self, cmd_args, **kwargs):
+		self.is_sweep = not cmd_args
+		self.sweep_spec = None
+		return await super().create(cmd_args, **kwargs)
+
 	async def get_input_addrs_from_inputs_opt(self):
 	async def get_input_addrs_from_inputs_opt(self):
 		return [] # TODO
 		return [] # TODO
 
 
@@ -41,15 +46,34 @@ class New(Base, TxNew):
 				msg(f'Account number must be an integer between 1 and {len(accts_data)} inclusive')
 				msg(f'Account number must be an integer between 1 and {len(accts_data)} inclusive')
 
 
 	async def compat_create(self):
 	async def compat_create(self):
+
+		if self.is_sweep:
+			if not self.sweep_spec:
+				from ....util import ymsg
+				ymsg('No transaction operation specified. Exiting')
+				return None
+			from ....ui import item_chooser
+			from ....color import pink
+			op = item_chooser(
+				self.cfg,
+				'Choose the sweep operation type',
+				('sweep', 'sweep_all'),
+				lambda s: pink(s.upper())).item
+			spec = self.sweep_spec
+		else:
+			op = 'transfer'
+			i = self.inputs[0]
+			o = self.outputs[0]
+			spec = f'{i.idx}:{i.acct_idx}:{o.addr},{o.amt}'
+
 		from ....xmrwallet import op as xmrwallet_op
 		from ....xmrwallet import op as xmrwallet_op
-		i = self.inputs[0]
-		o = self.outputs[0]
 		op = xmrwallet_op(
 		op = xmrwallet_op(
-			'transfer',
+			op,
 			self.cfg,
 			self.cfg,
 			None,
 			None,
 			None,
 			None,
-			spec = f'{i.idx}:{i.acct_idx}:{o.addr},{o.amt}',
+			spec = spec,
 			compat_call = True)
 			compat_call = True)
+
 		await op.restart_wallet_daemon()
 		await op.restart_wallet_daemon()
 		return await op.main()
 		return await op.main()

+ 2 - 1
mmgen/tw/unspent.py

@@ -95,7 +95,8 @@ class TwUnspentOutputs(TwView):
 			self.__dict__['proto'] = proto
 			self.__dict__['proto'] = proto
 			MMGenListItem.__init__(self, **kwargs)
 			MMGenListItem.__init__(self, **kwargs)
 
 
-	async def __init__(self, cfg, proto, *, minconf=1, addrs=[]):
+	async def __init__(self, cfg, proto, *, minconf=1, addrs=[], tx=None):
+		self.tx  = tx
 		await super().__init__(cfg, proto)
 		await super().__init__(cfg, proto)
 		self.minconf  = NonNegativeInt(minconf)
 		self.minconf  = NonNegativeInt(minconf)
 		self.addrs    = addrs
 		self.addrs    = addrs

+ 14 - 7
mmgen/tw/view.py

@@ -588,6 +588,12 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 				self.key_mappings.update(self.scroll_keys[sys.platform])
 				self.key_mappings.update(self.scroll_keys[sys.platform])
 			return self.key_mappings
 			return self.key_mappings
 
 
+		def cleanup(add_nl=False):
+			if add_nl:
+				msg('')
+			if self.scroll:
+				self.term.set('echo')
+
 		scroll = self.scroll = self.cfg.scroll
 		scroll = self.scroll = self.cfg.scroll
 
 
 		key_mappings = make_key_mappings(scroll)
 		key_mappings = make_key_mappings(scroll)
@@ -633,12 +639,11 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 				case ch if ch in key_mappings:
 				case ch if ch in key_mappings:
 					func = action_classes[ch].run
 					func = action_classes[ch].run
 					arg = action_methods[ch]
 					arg = action_methods[ch]
-					await func(self, arg) if isAsync(func) else func(self, arg)
+					ret = await func(self, arg) if isAsync(func) else func(self, arg)
+					if ret == 'quit_view':
+						return cleanup()
 				case 'q':
 				case 'q':
-					msg('')
-					if self.scroll:
-						self.term.set('echo')
-					return
+					return cleanup(add_nl=True)
 				case _:
 				case _:
 					if not scroll:
 					if not scroll:
 						msg_r('\ninvalid keypress ')
 						msg_r('\ninvalid keypress ')
@@ -693,6 +698,8 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 			await do_error_msg()
 			await do_error_msg()
 
 
 	async def post_action_cleanup(self, ret):
 	async def post_action_cleanup(self, ret):
+		if ret == 'quit_view':
+			return ret
 		if self.scroll and (ret is False or ret in ('redraw', 'erase')):
 		if self.scroll and (ret is False or ret in ('redraw', 'erase')):
 			# error messages could leave screen in messy state, so do complete redraw:
 			# error messages could leave screen in messy state, so do complete redraw:
 			msg_r(
 			msg_r(
@@ -704,7 +711,7 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 
 
 		@enable_echo
 		@enable_echo
 		async def run(self, parent, action_method):
 		async def run(self, parent, action_method):
-			await parent.post_action_cleanup(await action_method(parent))
+			return 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')
@@ -780,7 +787,7 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 					break
 					break
 				await asyncio.sleep(0.5)
 				await asyncio.sleep(0.5)
 
 
-			await parent.post_action_cleanup(ret)
+			return 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(

+ 11 - 2
mmgen/tx/new.py

@@ -79,6 +79,7 @@ def parse_fee_spec(proto, fee_arg):
 class New(Base):
 class New(Base):
 
 
 	fee_is_approximate = False
 	fee_is_approximate = False
+	is_sweep = False
 	msg_wallet_low_coin = 'Wallet has insufficient funds for this transaction ({} {} needed)'
 	msg_wallet_low_coin = 'Wallet has insufficient funds for this transaction ({} {} needed)'
 	msg_no_change_output = """
 	msg_no_change_output = """
 		ERROR: No change address specified.  If you wish to create a transaction with
 		ERROR: No change address specified.  If you wish to create a transaction with
@@ -455,7 +456,7 @@ class New(Base):
 		if self.cfg.comment_file:
 		if self.cfg.comment_file:
 			self.add_comment(infile=self.cfg.comment_file)
 			self.add_comment(infile=self.cfg.comment_file)
 
 
-		if not do_info:
+		if cmd_args and not do_info:
 			cmd_args, addrfile_args = self.get_addrfiles_from_cmdline(cmd_args)
 			cmd_args, addrfile_args = self.get_addrfiles_from_cmdline(cmd_args)
 			if self.is_swap:
 			if self.is_swap:
 				cmd_args = await self.process_swap_cmdline_args(cmd_args, addrfile_args)
 				cmd_args = await self.process_swap_cmdline_args(cmd_args, addrfile_args)
@@ -475,7 +476,8 @@ class New(Base):
 				self.cfg,
 				self.cfg,
 				self.proto,
 				self.proto,
 				minconf = self.cfg.minconf,
 				minconf = self.cfg.minconf,
-				addrs = await self.get_input_addrs_from_inputs_opt())
+				addrs = await self.get_input_addrs_from_inputs_opt(),
+				tx = self if self.is_sweep else None)
 			await self.twuo.get_data()
 			await self.twuo.get_data()
 			self.twctl = self.twuo.twctl
 			self.twctl = self.twuo.twctl
 
 
@@ -485,6 +487,11 @@ class New(Base):
 		if not (self.is_bump or self.cfg.inputs):
 		if not (self.is_bump or self.cfg.inputs):
 			await self.twuo.view_filter_and_sort()
 			await self.twuo.view_filter_and_sort()
 
 
+		if self.is_sweep:
+			del self.twctl
+			del self.twuo.twctl
+			return await self.compat_create()
+
 		if not self.is_bump:
 		if not self.is_bump:
 			self.twuo.display_total()
 			self.twuo.display_total()
 
 
@@ -530,6 +537,8 @@ class New(Base):
 				await self.update_vault_output(self.vault_output.amt)
 				await self.update_vault_output(self.vault_output.amt)
 
 
 		if self.is_compat:
 		if self.is_compat:
+			del self.twctl
+			del self.twuo.twctl
 			return await self.compat_create()
 			return await self.compat_create()
 
 
 		await self.create_serialized(locktime=locktime) # creates self.txid too
 		await self.create_serialized(locktime=locktime) # creates self.txid too

+ 6 - 4
mmgen/tx/util.py

@@ -23,12 +23,14 @@ def get_autosign_obj(cfg, add_cfg={}):
 			# used only in online environment (xmrwallet, txcreate, txsend, txbump):
 			# used only in online environment (xmrwallet, txcreate, txsend, txbump):
 			'online': not cfg.offline} | add_cfg))
 			'online': not cfg.offline} | add_cfg))
 
 
-def mount_removable_device(cfg, add_cfg={}):
-	asi = get_autosign_obj(cfg, add_cfg=add_cfg)
+def mount_removable_device(cfg, do_umount=True, asi=None, add_cfg={}, do_umount_registered=[]):
+	asi = asi or get_autosign_obj(cfg, add_cfg=add_cfg)
 	if not asi.device_inserted:
 	if not asi.device_inserted:
 		from ..util import die
 		from ..util import die
 		die(1, 'Removable device not present!')
 		die(1, 'Removable device not present!')
-	import atexit
-	atexit.register(lambda: asi.do_umount())
+	if do_umount and not do_umount_registered:
+		import atexit
+		atexit.register(lambda: asi.do_umount())
+		do_umount_registered.append(None)
 	asi.do_mount()
 	asi.do_mount()
 	return asi
 	return asi

+ 8 - 2
mmgen/ui.py

@@ -117,18 +117,24 @@ 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='  '):
+def item_chooser(cfg, hdr, items, item_formatter, indent='', empty_ok=False, add_nl=False):
 	from collections import namedtuple
 	from collections import namedtuple
 	col1_w = len(str(len(items)))
 	col1_w = len(str(len(items)))
 	prompt = '{i}{a}:\n{i}{b}\n{i}{c}'.format(
 	prompt = '{i}{a}:\n{i}{b}\n{i}{c}'.format(
 		a = hdr,
 		a = hdr,
 		b = ('\n' + indent).join(f'  {n:{col1_w}}) {item_formatter(d)}' for n, d in enumerate(items, 1)),
 		b = ('\n' + indent).join(f'  {n:{col1_w}}) {item_formatter(d)}' for n, d in enumerate(items, 1)),
-		c = 'Enter a number> ',
+		c = 'Enter a number, or ENTER to return to main menu> ' if empty_ok else 'Enter a number> ',
 		i = indent)
 		i = indent)
 	while True:
 	while True:
 		res = line_input(cfg, prompt)
 		res = line_input(cfg, prompt)
+		if not res and empty_ok:
+			if add_nl:
+				msg('')
+			return None
 		if is_int(res) and 0 < int(res) <= len(items):
 		if is_int(res) and 0 < int(res) <= len(items):
 			num = int(res)
 			num = int(res)
+			if add_nl:
+				msg('')
 			return namedtuple('user_choice', 'num idx item')(num, num - 1, items[num - 1])
 			return namedtuple('user_choice', 'num idx item')(num, num - 1, items[num - 1])
 		msg(f'{indent}{res}: invalid entry\n')
 		msg(f'{indent}{res}: invalid entry\n')
 
 

+ 4 - 8
mmgen/xmrwallet/ops/__init__.py

@@ -12,7 +12,7 @@
 xmrwallet.ops.__init__: Monero wallet ops for the MMGen Suite
 xmrwallet.ops.__init__: Monero wallet ops for the MMGen Suite
 """
 """
 
 
-import re, atexit
+import re
 
 
 from ...color import blue
 from ...color import blue
 from ...util import msg, die, fmt
 from ...util import msg, die, fmt
@@ -115,14 +115,10 @@ class OpBase:
 			  Proxy: {blue(m[2] or 'None')}
 			  Proxy: {blue(m[2] or 'None')}
 			""", strip_char='\t', indent=indent))
 			""", strip_char='\t', indent=indent))
 
 
-	def mount_removable_device(self, registered=[]):
+	def mount_removable_device(self):
 		if self.cfg.autosign:
 		if self.cfg.autosign:
-			if not self.asi.device_inserted:
-				die(1, 'Removable device not present!')
-			if self.do_umount and not registered:
-				atexit.register(lambda: self.asi.do_umount())
-				registered.append(None)
-			self.asi.do_mount()
+			from ...tx.util import mount_removable_device
+			mount_removable_device(self.cfg, do_umount=self.do_umount, asi=self.asi)
 			self.post_mount_action()
 			self.post_mount_action()
 
 
 	def pre_init_action(self):
 	def pre_init_action(self):

+ 3 - 4
mmgen/xmrwallet/ops/sweep.py

@@ -169,10 +169,9 @@ class OpSweep(OpMixinSpec, OpWallet):
 			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})')
 
 
 	async def main(self):
 	async def main(self):
-		if not self.compat_call:
-			gmsg(
-				f'\n{self.stem.capitalize()}ing account #{self.account}'
-				f' of wallet {self.source.idx}{self.add_desc}')
+		gmsg(
+			f'\n{self.stem.capitalize()}ing account #{self.account}'
+			f' of wallet {self.source.idx}{self.add_desc}')
 
 
 		h = MoneroWalletRPC(self, self.source)
 		h = MoneroWalletRPC(self, self.source)
 
 

+ 54 - 15
test/cmdtest_d/xmr_autosign.py

@@ -536,7 +536,7 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 		('mine_blocks_1',            'mining a block'),
 		('mine_blocks_1',            'mining a block'),
 		('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_twview_chk1',        'viewing Alice’s tracking wallets (check balances)'),
 		('alice_listaddresses_sort', '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'),
@@ -548,6 +548,16 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 		('alice_txcreate3',          'recreating the transaction'),
 		('alice_txcreate3',          'recreating the transaction'),
 		('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'),
+		('mine_blocks_10',           'mining some blocks'),
+		('alice_twview_chk2',        'viewing Alice’s tracking wallets (check balances)'),
+		('alice_txcreate_sweep1',    'creating a sweep transaction (account sweep)'),
+		('alice_txsend2',            'sending the transaction'),
+		('mine_blocks_10',           'mining some blocks'),
+		('alice_twview_chk3',        'viewing Alice’s tracking wallets (check balances)'),
+		('alice_txcreate_sweep2',    'creating a sweep transaction (address sweep)'),
+		('alice_txsend3',            'sending the transaction'),
+		('mine_blocks_10',           'mining some blocks'),
+		('alice_twview_chk4',        'viewing Alice’s tracking wallets (check balances)'),
 		('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_newacct1',           'adding account to Alice’s tracking wallet (dfl label)'),
 		('alice_newacct2',           'adding account to Alice’s tracking wallet (no timestr)'),
 		('alice_newacct2',           'adding account to Alice’s tracking wallet (no timestr)'),
@@ -680,14 +690,27 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 	def alice_twview2(self):
 	def alice_twview2(self):
 		return self._alice_twops('twview', menu='RaAdMraAdMe')
 		return self._alice_twops('twview', menu='RaAdMraAdMe')
 
 
-	def alice_twview3(self):
+	def alice_twview_chk1(self):
+		return self._alice_twview_chk([
+			'Total XMR: 3.722345649021 [3.729999970119]',
+			'1         0.026296296417',
+			'0.007654321098'])
+
+	def alice_twview_chk2(self):
+		return self._alice_twview_chk(['Total XMR: 3.715053370119'], sync=True)
+
+	def alice_twview_chk3(self):
+		return self._alice_twview_chk(['Total XMR: 3.713242570119', '1.232757091234'], sync=True)
+
+	def alice_twview_chk4(self):
+		return self._alice_twview_chk(['Total XMR: 3.709050970119', '1.254861787651'], sync=True)
+
+	def _alice_twview_chk(self, expect_arr, sync=False):
 		return self._alice_twops(
 		return self._alice_twops(
 			'twview',
 			'twview',
-			interactive = False,
-			expect_arr = [
-				'Total XMR: 3.722345649021 [3.729999970119]',
-				'1         0.026296296417',
-				'0.007654321098'])
+			interactive = sync,
+			menu = 'R' if sync else '',
+			expect_arr = expect_arr)
 
 
 	def _alice_twops(
 	def _alice_twops(
 			self,
 			self,
@@ -721,7 +744,7 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 				t.expect('main menu): ', str(lbl_addr_idx))
 				t.expect('main menu): ', str(lbl_addr_idx))
 			elif newacct_wallet_num:
 			elif newacct_wallet_num:
 				t.expect(menu_prompt, 'N')
 				t.expect(menu_prompt, 'N')
-				t.expect('number> ', f'{newacct_wallet_num}\n')
+				t.expect('number, or ENTER to return to main menu> ', f'{newacct_wallet_num}\n')
 			elif newaddr_acct_num:
 			elif newaddr_acct_num:
 				t.expect(menu_prompt, 'n')
 				t.expect(menu_prompt, 'n')
 				t.expect('main menu): ', str(newaddr_acct_num))
 				t.expect('main menu): ', str(newaddr_acct_num))
@@ -752,21 +775,28 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 	def alice_txcreate1(self):
 	def alice_txcreate1(self):
 		return self._alice_txops('txcreate', [f'{self.burn_addr},0.012345'], acct_num=1)
 		return self._alice_txops('txcreate', [f'{self.burn_addr},0.012345'], acct_num=1)
 
 
+	def alice_txcreate_sweep1(self):
+		return self._alice_txops('txcreate', menu='S', sweep_menu='23', sweep_type='sweep')
+
+	def alice_txcreate_sweep2(self):
+		return self._alice_txops('txcreate', menu='s', sweep_menu='2', sweep_type='sweep_all')
+
 	alice_txcreate3 = alice_txcreate2 = alice_txcreate1
 	alice_txcreate3 = alice_txcreate2 = alice_txcreate1
 
 
-	def alice_txabort1(self):
+	def _alice_txabort(self):
 		return self._alice_txops('txsend', opts=['--alice', '--abort'])
 		return self._alice_txops('txsend', opts=['--alice', '--abort'])
 
 
-	alice_txabort2 = alice_txabort1
+	alice_txabort1 = alice_txabort2 = _alice_txabort
 
 
-	def alice_txsend1(self):
+	def _alice_txsend(self):
 		return self._alice_txops(
 		return self._alice_txops(
 			'txsend',
 			'txsend',
 			opts        = ['--alice', '--quiet'],
 			opts        = ['--alice', '--quiet'],
 			add_opts    = self.alice_daemon_opts,
 			add_opts    = self.alice_daemon_opts,
-			acct_num    = 1,
 			wait_signed = True)
 			wait_signed = True)
 
 
+	alice_txsend1 = alice_txsend2 = alice_txsend3 = _alice_txsend
+
 	def wait_signed1(self):
 	def wait_signed1(self):
 		self.spawn(msg_only=True)
 		self.spawn(msg_only=True)
 		oqmsg('')
 		oqmsg('')
@@ -783,6 +813,8 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 			menu = '',
 			menu = '',
 			acct_num = None,
 			acct_num = None,
 			wait_signed = False,
 			wait_signed = False,
+			sweep_type = None,
+			sweep_menu = '',
 			signable_desc = 'transaction'):
 			signable_desc = 'transaction'):
 		if wait_signed:
 		if wait_signed:
 			self._wait_signed(signable_desc)
 			self._wait_signed(signable_desc)
@@ -791,9 +823,16 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 		if '--abort' in opts:
 		if '--abort' in opts:
 			t.expect('(y/N): ', 'y')
 			t.expect('(y/N): ', 'y')
 		elif op == 'txcreate':
 		elif op == 'txcreate':
-			for ch in menu + 'q':
-				t.expect(self.menu_prompt, ch)
-			t.expect('to spend from: ', f'{acct_num}\n')
+			if sweep_type:
+				t.expect(self.menu_prompt, menu)
+				for ch in sweep_menu:
+					t.expect('main menu): ', ch)
+				t.expect('number> ', {'sweep': '1', 'sweep_all': '2'}[sweep_type])
+				t.expect('(y/N): ', 'y') # create new address?
+			else:
+				for ch in menu + 'q':
+					t.expect(self.menu_prompt, ch)
+				t.expect('to spend from: ', f'{acct_num}\n')
 			t.expect('(y/N): ', 'y') # save?
 			t.expect('(y/N): ', 'y') # save?
 		elif op == 'txsend':
 		elif op == 'txsend':
 			t.expect('(y/N): ', 'y') # view?
 			t.expect('(y/N): ', 'y') # view?