8 Commits 851335106f ... bdd7dd3393

Author SHA1 Message Date
  The MMGen Project bdd7dd3393 XMR compat: support tracking wallet views 1 week ago
  The MMGen Project 937f0af9c9 mmgen-xmrwallet label: prompt user to add timestamp 1 week ago
  The MMGen Project 040671bb06 minor cleanups 1 week ago
  The MMGen Project bfddd5b8ae TwCtl.set_comment(): return the comment 1 week ago
  The MMGen Project 8a750259b9 mmgen-xmrwallet: new `dump-json` operation 1 week ago
  The MMGen Project d5e142d475 xmrwallet.ops.wallet: support returning data from `main()` 1 week ago
  The MMGen Project c7a75b684f tw.view: support key mapping removal, multiline prompt str edits 1 week ago
  The MMGen Project fd65ae6660 whitespace, minor fixes 1 week ago

+ 13 - 2
mmgen/addr.py

@@ -145,14 +145,25 @@ class MMGenID(HiliteStr, InitErrors, MMGenObject):
 				assert id_str.count(':') == 2, 'mmtype letter required for extended MMGen IDs'
 				assert id_str.count(':') == 2, 'mmtype letter required for extended MMGen IDs'
 				me = str.__new__(cls, id_str)
 				me = str.__new__(cls, id_str)
 				idx, ext = idx.split('-', 1)
 				idx, ext = idx.split('-', 1)
-				me.acct_num, me.acct_addr_num = [MoneroIdx(e) for e in ext.split('/', 1)]
+				me.acct_idx, me.addr_idx = [MoneroIdx(e) for e in ext.split('/', 1)]
 			else:
 			else:
+				ext = None
 				me = str.__new__(cls, f'{sid}:{mmtype}:{idx}')
 				me = str.__new__(cls, f'{sid}:{mmtype}:{idx}')
 			me.sid = SeedID(sid=sid)
 			me.sid = SeedID(sid=sid)
 			me.mmtype = proto.addr_type(mmtype)
 			me.mmtype = proto.addr_type(mmtype)
 			me.idx = AddrIdx(idx)
 			me.idx = AddrIdx(idx)
 			me.al_id = str.__new__(AddrListID, me.sid + ':' + me.mmtype) # checks already done
 			me.al_id = str.__new__(AddrListID, me.sid + ':' + me.mmtype) # checks already done
-			me.sort_key = f'{me.sid}:{me.mmtype}:{me.idx:0{me.idx.max_digits}}'
+			if ext:
+				me.sort_key = '{}:{}:{:0{w1}}:{:0{w2}}:{:0{w2}}'.format(
+					me.sid,
+					me.mmtype,
+					me.idx,
+					me.acct_idx,
+					me.addr_idx,
+					w1 = me.idx.max_digits,
+					w2 = MoneroIdx.max_digits)
+			else:
+				me.sort_key = '{}:{}:{:0{w}}'.format(me.sid, me.mmtype, me.idx, w=me.idx.max_digits)
 			me.proto = proto
 			me.proto = proto
 			return me
 			return me
 		except Exception as e:
 		except Exception as e:

+ 1 - 0
mmgen/cfg.py

@@ -240,6 +240,7 @@ class Config(Lockable):
 	# Monero:
 	# Monero:
 	monero_wallet_rpc_user     = 'monero'
 	monero_wallet_rpc_user     = 'monero'
 	monero_wallet_rpc_password = ''
 	monero_wallet_rpc_password = ''
+	monero_daemon              = ''
 	xmrwallet_compat           = False
 	xmrwallet_compat           = False
 	priority                   = 0
 	priority                   = 0
 
 

+ 1 - 1
mmgen/data/version

@@ -1 +1 @@
-16.1.dev14
+16.1.dev15

+ 6 - 3
mmgen/help/xmrwallet.py

@@ -49,7 +49,7 @@ transfer  - transfer specified XMR amount from specified wallet:account to
             specified address
             specified address
 sweep     - sweep funds in specified wallet:account to new address in same
 sweep     - sweep funds in specified wallet:account to new address in same
             account, or new or specified account in another wallet
             account, or new or specified account in another wallet
-sweep_all - same as above, but sweep balances of all addresses in the account
+sweep-all - same as above, but sweep balances of all addresses in the account
 relay     - relay a transaction from a transaction file created using ‘sweep’
 relay     - relay a transaction from a transaction file created using ‘sweep’
             or ‘transfer’ with the --no-relay option
             or ‘transfer’ with the --no-relay option
 submit    - submit an autosigned transaction to a wallet and the network
 submit    - submit an autosigned transaction to a wallet and the network
@@ -59,8 +59,11 @@ abort     - abort the current transaction created with --autosign.  The
             transaction may be signed or unsigned
             transaction may be signed or unsigned
 txview    - display detailed information about a transaction file or files
 txview    - display detailed information about a transaction file or files
 txlist    - same as above, but display terse information in tabular format
 txlist    - same as above, but display terse information in tabular format
-dump      - produce JSON dumps of wallet metadata (accounts, addresses and
-            labels) for a list or range of wallets
+dump-json - dump wallet metadata (accounts, addresses, labels), plus address
+            balances, for a list or range of wallets, to standard output in
+            JSON format
+dump      - same as above, but dump metadata only and save the dumps to
+            separate files for each wallet
 restore   - same as ‘create’, but additionally restore wallet metadata from
 restore   - same as ‘create’, but additionally restore wallet metadata from
             the corresponding JSON dump files created with ‘dump’
             the corresponding JSON dump files created with ‘dump’
 export-outputs      - export outputs of watch-only wallets for import into
 export-outputs      - export outputs of watch-only wallets for import into

+ 1 - 0
mmgen/main_tool.py

@@ -34,6 +34,7 @@ opts_data = {
 			-- -d, --outdir=       d  Specify an alternate directory 'd' for output
 			-- -d, --outdir=       d  Specify an alternate directory 'd' for output
 			-- -h, --help             Print this help message
 			-- -h, --help             Print this help message
 			-- --, --longhelp         Print help message for long (global) options
 			-- --, --longhelp         Print help message for long (global) options
+			x- -a, --autosign         Operate on an autosigned transaction
 			-- -e, --echo-passphrase  Echo passphrase or mnemonic to screen upon entry
 			-- -e, --echo-passphrase  Echo passphrase or mnemonic to screen upon entry
 			-- -k, --use-internal-keccak-module Force use of the internal keccak module
 			-- -k, --use-internal-keccak-module Force use of the internal keccak module
 			-- -K, --keygen-backend=n Use backend 'n' for public key generation.  Options
 			-- -K, --keygen-backend=n Use backend 'n' for public key generation.  Options

+ 8 - 8
mmgen/main_xmrwallet.py

@@ -28,7 +28,7 @@ opts_data = {
 		'desc': """Perform various Monero wallet and transacting operations for
 		'desc': """Perform various Monero wallet and transacting operations for
                    addresses in an MMGen XMR key-address file""",
                    addresses in an MMGen XMR key-address file""",
 		'usage2': [
 		'usage2': [
-			'[opts] create | sync | list | view | listview | dump | restore [xmr_keyaddrfile] [wallets]',
+			'[opts] create | sync | list | view | listview | dump-json | dump | restore [xmr_keyaddrfile] [wallets]',
 			'[opts] label    [xmr_keyaddrfile] LABEL_SPEC',
 			'[opts] label    [xmr_keyaddrfile] LABEL_SPEC',
 			'[opts] new      [xmr_keyaddrfile] NEW_ADDRESS_SPEC',
 			'[opts] new      [xmr_keyaddrfile] NEW_ADDRESS_SPEC',
 			'[opts] transfer [xmr_keyaddrfile] TRANSFER_SPEC',
 			'[opts] transfer [xmr_keyaddrfile] TRANSFER_SPEC',
@@ -62,7 +62,7 @@ opts_data = {
 -b, --rescan-blockchain          Rescan the blockchain if wallet fails to sync
 -b, --rescan-blockchain          Rescan the blockchain if wallet fails to sync
 -d, --outdir=D                   Save transaction files to directory 'D'
 -d, --outdir=D                   Save transaction files to directory 'D'
                                  instead of the working directory
                                  instead of the working directory
--D, --daemon=H:P                 Connect to the monerod at {D}
+-D, --daemon=H:P                 Connect to the monerod at {dhp}
 -e, --skip-empty-accounts        Skip display of empty accounts in wallets
 -e, --skip-empty-accounts        Skip display of empty accounts in wallets
                                  where applicable
                                  where applicable
 -E, --skip-empty-addresses       Skip display of used empty addresses in
 -E, --skip-empty-addresses       Skip display of used empty addresses in
@@ -73,7 +73,7 @@ opts_data = {
 -P, --rescan-spent               Perform a rescan of spent outputs.  Used only
 -P, --rescan-spent               Perform a rescan of spent outputs.  Used only
                                  with the ‘export-outputs-sign’ operation
                                  with the ‘export-outputs-sign’ operation
 -R, --tx-relay-daemon=H:P[:H:P]  Relay transactions via a monerod specified by
 -R, --tx-relay-daemon=H:P[:H:P]  Relay transactions via a monerod specified by
-                                 {R}
+                                 {rdhp}
 -r, --restore-height=H           Scan from height 'H' when creating wallets.
 -r, --restore-height=H           Scan from height 'H' when creating wallets.
                                  Use special value ‘current’ to create empty
                                  Use special value ‘current’ to create empty
                                  wallet at current blockchain height.
                                  wallet at current blockchain height.
@@ -93,8 +93,8 @@ opts_data = {
 	},
 	},
 	'code': {
 	'code': {
 		'options': lambda cfg, help_notes, s: s.format(
 		'options': lambda cfg, help_notes, s: s.format(
-			D   = xmrwallet.uarg_info['daemon'].annot,
-			R   = xmrwallet.uarg_info['tx_relay_daemon'].annot,
+			dhp = xmrwallet.uarg_info['daemon'].annot,
+			rdhp = xmrwallet.uarg_info['tx_relay_daemon'].annot,
 			cfg = cfg,
 			cfg = cfg,
 			gc  = gc,
 			gc  = gc,
 			tw_dir = help_notes('tw_dir'),
 			tw_dir = help_notes('tw_dir'),
@@ -111,9 +111,9 @@ cfg = Config(opts_data=opts_data, init_opts={'coin':'xmr'})
 cmd_args = cfg._args
 cmd_args = cfg._args
 
 
 if cmd_args and cfg.autosign and (
 if cmd_args and cfg.autosign and (
-		cmd_args[0] in (
+		cmd_args[0].replace('-', '_') in (
 			xmrwallet.kafile_arg_ops
 			xmrwallet.kafile_arg_ops
-			+ ('export-outputs', 'export-outputs-sign', 'import-key-images', 'txview', 'txlist')
+			+ ('export_outputs', 'export_outputs_sign', 'import_key_images', 'txview', 'txlist')
 		)
 		)
 		or len(cmd_args) == 1 and cmd_args[0] in ('submit', 'resubmit', 'abort')
 		or len(cmd_args) == 1 and cmd_args[0] in ('submit', 'resubmit', 'abort')
 	):
 	):
@@ -133,7 +133,7 @@ match op:
 			cfg._usage()
 			cfg._usage()
 	case 'txview' | 'txlist':
 	case 'txview' | 'txlist':
 		infile = [infile] + cmd_args
 		infile = [infile] + cmd_args
-	case 'create' | 'sync' | 'list' | 'view' | 'listview' | 'dump' | 'restore': # kafile_arg_ops
+	case 'create' | 'sync' | 'list' | 'view' | 'listview' | 'dump_json' | 'dump' | 'restore':
 		if len(cmd_args) > 1:
 		if len(cmd_args) > 1:
 			cfg._usage()
 			cfg._usage()
 		wallets = cmd_args.pop(0) if cmd_args else None
 		wallets = cmd_args.pop(0) if cmd_args else None

+ 5 - 0
mmgen/opts.py

@@ -308,6 +308,9 @@ class UserOpts(Opts):
 			br --rpc-user=USER        Authenticate to coin daemon using username USER
 			br --rpc-user=USER        Authenticate to coin daemon using username USER
 			br --rpc-password=PASS    Authenticate to coin daemon using password PASS
 			br --rpc-password=PASS    Authenticate to coin daemon using password PASS
 			Rr --rpc-backend=backend  Use backend 'backend' for JSON-RPC communications
 			Rr --rpc-backend=backend  Use backend 'backend' for JSON-RPC communications
+			mr --monero-wallet-rpc-user=USER Monero wallet RPC username
+			mr --monero-wallet-rpc-password=USER Monero wallet RPC password
+			mr --monero-daemon=HOST:PORT Connect to the monerod at HOST:PORT
 			Rr --aiohttp-rpc-queue-len=N Use N simultaneous RPC connections with aiohttp
 			Rr --aiohttp-rpc-queue-len=N Use N simultaneous RPC connections with aiohttp
 			-p --regtest=0|1          Disable or enable regtest mode
 			-p --regtest=0|1          Disable or enable regtest mode
 			-- --testnet=0|1          Disable or enable testnet
 			-- --testnet=0|1          Disable or enable testnet
@@ -361,6 +364,7 @@ class UserOpts(Opts):
 		  'r' - local RPC coin
 		  'r' - local RPC coin
 		  'X' - remote RPC coin
 		  'X' - remote RPC coin
 		  'x' - local or remote RPC coin
 		  'x' - local or remote RPC coin
+		  'm' - Monero
 		  '-' - any coin
 		  '-' - any coin
 		Cmd codes:
 		Cmd codes:
 		  'p' - proto required
 		  'p' - proto required
@@ -378,6 +382,7 @@ class UserOpts(Opts):
 					['-', 'r', 'R', 'b', 'h', 'x'] if coin == 'bch' else
 					['-', 'r', 'R', 'b', 'h', 'x'] if coin == 'bch' else
 					['-', 'r', 'R', 'b', 'x'] if coin in gc.btc_fork_rpc_coins else
 					['-', 'r', 'R', 'b', 'x'] if coin in gc.btc_fork_rpc_coins else
 					['-', 'r', 'R', 'e', 'x'] if coin in gc.eth_fork_coins else
 					['-', 'r', 'R', 'e', 'x'] if coin in gc.eth_fork_coins else
+					['-', 'r', 'x', 'm'] if coin == 'xmr' else
 					['-', 'r', 'x'] if coin in gc.local_rpc_coins else
 					['-', 'r', 'x'] if coin in gc.local_rpc_coins else
 					['-', 'X', 'x'] if coin in gc.remote_rpc_coins else
 					['-', 'X', 'x'] if coin in gc.remote_rpc_coins else
 					['-']),
 					['-']),

+ 22 - 0
mmgen/proto/xmr/tw/addresses.py

@@ -0,0 +1,22 @@
+#!/usr/bin/env python3
+#
+# MMGen Wallet, a terminal-based cryptocurrency wallet
+# Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen-wallet
+#   https://gitlab.com/mmgen/mmgen-wallet
+
+"""
+proto.xmr.tw.addresses: Monero protocol tracking wallet address list class
+"""
+
+from ....tw.addresses import TwAddresses
+
+from .view import MoneroTwView
+
+class MoneroTwAddresses(MoneroTwView, TwAddresses):
+
+	include_empty = True
+	has_used = True

+ 26 - 0
mmgen/proto/xmr/tw/ctl.py

@@ -12,8 +12,34 @@
 proto.xmr.tw.ctl: Monero tracking wallet control class
 proto.xmr.tw.ctl: Monero tracking wallet control class
 """
 """
 
 
+from ....tw.ctl import write_mode
 from ....tw.store import TwCtlWithStore
 from ....tw.store import TwCtlWithStore
 
 
 class MoneroTwCtl(TwCtlWithStore):
 class MoneroTwCtl(TwCtlWithStore):
 
 
 	tw_subdir = 'tracking-wallets'
 	tw_subdir = 'tracking-wallets'
+	use_cached_balances = True
+
+	@write_mode
+	async def set_comment(
+			self,
+			addrspec,
+			comment      = '',
+			*,
+			trusted_pair = None,
+			silent       = False):
+
+		from ....ui import keypress_confirm
+		add_timestr = keypress_confirm(self.cfg, 'Add timestamp to label?')
+
+		m = trusted_pair[0].obj
+		from ....xmrwallet import op as xmrwallet_op
+		op = xmrwallet_op(
+			'label',
+			self.cfg,
+			None,
+			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)

+ 23 - 0
mmgen/proto/xmr/tw/unspent.py

@@ -0,0 +1,23 @@
+#!/usr/bin/env python3
+#
+# MMGen Wallet, a terminal-based cryptocurrency wallet
+# Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen-wallet
+#   https://gitlab.com/mmgen/mmgen-wallet
+
+"""
+proto.xmr.tw.unspent: Monero protocol tracking wallet unspent outputs class
+"""
+
+from ....tw.addresses import TwAddresses
+
+from .view import MoneroTwView
+
+class MoneroTwUnspentOutputs(MoneroTwView, TwAddresses):
+
+	hdr_lbl = 'spendable accounts'
+	desc    = 'address balances'
+	include_empty = False

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

@@ -0,0 +1,81 @@
+#!/usr/bin/env python3
+#
+# MMGen Wallet, a terminal-based cryptocurrency wallet
+# Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen-wallet
+#   https://gitlab.com/mmgen/mmgen-wallet
+
+"""
+proto.xmr.tw.view: Monero protocol base class for tracking wallet view classes
+"""
+
+from ....xmrwallet import op as xmrwallet_op
+from ....tw.view import TwView
+
+class MoneroTwView:
+
+	class rpc:
+		caps = ()
+		is_remote = False
+
+	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:'))}
+	extra_key_mappings = {
+		'u': 'd_showused',
+		'R': 'a_sync_wallets'}
+	removed_key_mappings = {
+		'D': 'i_addr_delete'}
+
+	async def get_rpc_data(self):
+		from mmgen.tw.shared import TwMMGenID, TwLabel
+
+		op = xmrwallet_op('dump_data', self.cfg, None, None, compat_call=True)
+		await op.restart_wallet_daemon()
+		wallets_data = await op.main()
+
+		self.total = self.proto.coin_amt('0')
+
+		def gen_addrs():
+			for wdata in wallets_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', []):
+					bals_data[d['account_index']].update({d['address_index']: d['unlocked_balance']})
+
+				for acct_idx, acct_data in enumerate(wdata['data'].addrs_data):
+					for addr_data in acct_data['addresses']:
+						addr_idx = addr_data['address_index']
+						self.total += (bal := self.proto.coin_amt(
+							bals_data[acct_idx].get(addr_idx, 0),
+							from_unit = 'atomic'))
+						if self.include_empty or bal:
+							mmid = '{}:M:{}-{}/{}'.format(
+								wdata['seed_id'],
+								wdata['wallet_num'],
+								acct_idx,
+								addr_idx)
+							yield (TwMMGenID(self.proto, mmid), {
+								'addr':    addr_data['address'],
+								'amt':     bal,
+								'recvd':   bal,
+								'is_used': addr_data['used'],
+								'confs':   1,
+								'lbl':     TwLabel(self.proto, mmid + ' ' + addr_data['label'])})
+
+		return dict(gen_addrs())
+
+	class action(TwView.action):
+
+		async def a_sync_wallets(self, parent):
+			from ....util import msg, msg_r
+			from ....tw.view import CUR_HOME, ERASE_ALL
+			msg('')
+			op = xmrwallet_op('sync', parent.cfg, None, None, compat_call=True)
+			await op.restart_wallet_daemon()
+			await op.main()
+			await parent.get_data()
+			msg_r(CUR_HOME + ERASE_ALL)

+ 4 - 3
mmgen/tool/rpc.py

@@ -157,13 +157,14 @@ class tool_cmd(tool_cmd_base):
 
 
 	async def add_label(self, mmgen_or_coin_addr: str, label: str):
 	async def add_label(self, mmgen_or_coin_addr: str, label: str):
 		"add descriptive label for address in tracking wallet"
 		"add descriptive label for address in tracking wallet"
+		from ..obj import TwComment
 		from ..tw.ctl import TwCtl
 		from ..tw.ctl import TwCtl
-		return await (await TwCtl(self.cfg, self.proto, mode='w')).set_comment(mmgen_or_coin_addr, label)
+		ret = await (await TwCtl(self.cfg, self.proto, mode='w')).set_comment(mmgen_or_coin_addr, label)
+		return True if isinstance(ret, TwComment) else False
 
 
 	async def remove_label(self, mmgen_or_coin_addr: str):
 	async def remove_label(self, mmgen_or_coin_addr: str):
 		"remove descriptive label for address in tracking wallet"
 		"remove descriptive label for address in tracking wallet"
-		await self.add_label(mmgen_or_coin_addr, '')
-		return True
+		return await self.add_label(mmgen_or_coin_addr, '')
 
 
 	async def remove_address(self, mmgen_or_coin_addr: str):
 	async def remove_address(self, mmgen_or_coin_addr: str):
 		"remove an address from tracking wallet"
 		"remove an address from tracking wallet"

+ 1 - 5
mmgen/tw/ctl.py

@@ -141,16 +141,12 @@ class TwCtl(MMGenObject, metaclass=AsyncInit):
 				msg(
 				msg(
 					'Added label {} to {}'.format(comment.hl2(encl='‘’'), desc) if comment else
 					'Added label {} to {}'.format(comment.hl2(encl='‘’'), desc) if comment else
 					'Removed label from {}'.format(desc))
 					'Removed label from {}'.format(desc))
-			return True
+			return comment
 		else:
 		else:
 			if not silent:
 			if not silent:
 				msg('Label could not be {}'.format('added' if comment else 'removed'))
 				msg('Label could not be {}'.format('added' if comment else 'removed'))
 			return False
 			return False
 
 
-	@write_mode
-	async def remove_comment(self, mmaddr):
-		await self.set_comment(mmaddr, '')
-
 	def check_import_mmid(self, addr, old_mmid, new_mmid):
 	def check_import_mmid(self, addr, old_mmid, new_mmid):
 		'returns True if mmid needs update, None otherwise'
 		'returns True if mmid needs update, None otherwise'
 		if new_mmid != old_mmid:
 		if new_mmid != old_mmid:

+ 26 - 17
mmgen/tw/view.py

@@ -96,6 +96,7 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 	display_hdr = ()
 	display_hdr = ()
 	display_body = ()
 	display_body = ()
 	prompt_fs_repl = {}
 	prompt_fs_repl = {}
+	removed_key_mappings = {}
 	nodata_msg = '[no data for requested parameters]'
 	nodata_msg = '[no data for requested parameters]'
 	cols = 0
 	cols = 0
 	term_height = 0
 	term_height = 0
@@ -191,14 +192,21 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 	async def __init__(self, cfg, proto):
 	async def __init__(self, cfg, proto):
 		self.cfg = cfg
 		self.cfg = cfg
 		self.proto = proto
 		self.proto = proto
-		self.rpc = await rpc_init(cfg, proto)
+		if have_rpc := 'rpc_init' in proto.mmcaps:
+			self.rpc = await rpc_init(cfg, proto)
 		if self.has_wallet:
 		if self.has_wallet:
 			from .ctl import TwCtl
 			from .ctl import TwCtl
-			self.twctl = await TwCtl(cfg, proto, mode='w')
+			self.twctl = await TwCtl(cfg, proto, mode='w', no_rpc=not have_rpc)
 		self.amt_keys = {'amt':'iwidth', 'amt2':'iwidth2'} if self.has_amt2 else {'amt':'iwidth'}
 		self.amt_keys = {'amt':'iwidth', 'amt2':'iwidth2'} if self.has_amt2 else {'amt':'iwidth'}
-		if repl := self.prompt_fs_repl.get(self.proto.coin):
-			self.prompt_fs_in[repl[0]] = repl[1]
+		if repl_data := self.prompt_fs_repl.get(self.proto.coin):
+			for repl in [repl_data] if isinstance(repl_data[0], int) else repl_data:
+				self.prompt_fs_in[repl[0]] = repl[1]
 		self.prompt_fs = '\n'.join(self.prompt_fs_in)
 		self.prompt_fs = '\n'.join(self.prompt_fs_in)
+		for k, v in self.removed_key_mappings.items():
+			if k in self.key_mappings and self.key_mappings[k] == v:
+				del self.key_mappings[k]
+			else:
+				raise ValueError(f'{k}: {v}: unrecognized or invalid key mapping')
 		self.key_mappings.update(self.extra_key_mappings)
 		self.key_mappings.update(self.extra_key_mappings)
 		if self.proto.coin == 'BCH':
 		if self.proto.coin == 'BCH':
 			self.key_mappings.update({'h': 'd_addr_view_pref'})
 			self.key_mappings.update({'h': 'd_addr_view_pref'})
@@ -434,7 +442,7 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 				yield 'Network: {}'.format(Green(
 				yield 'Network: {}'.format(Green(
 					self.proto.coin + ' ' + self.proto.chain_name.upper()))
 					self.proto.coin + ' ' + self.proto.chain_name.upper()))
 
 
-				if not self.rpc.is_remote:
+				if hasattr(self.rpc, 'blockcount'):
 					yield 'Block {} [{}]'.format(
 					yield 'Block {} [{}]'.format(
 						self.rpc.blockcount.hl(color=color),
 						self.rpc.blockcount.hl(color=color),
 						make_timestr(self.rpc.cur_date))
 						make_timestr(self.rpc.cur_date))
@@ -474,8 +482,7 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 				tuple(gen_hdr(spc='' if line_processing == 'print' else ' ')),
 				tuple(gen_hdr(spc='' if line_processing == 'print' else ' ')),
 				tuple(
 				tuple(
 					get_body(getattr(self, dt.fmt_method)) if data else
 					get_body(getattr(self, dt.fmt_method)) if data else
-					[(nocolor, yellow)[color](self.nodata_msg.ljust(self.term_width))])
-			)
+					[(nocolor, yellow)[color](self.nodata_msg.ljust(self.term_width))]))
 
 
 		if not gv.stdout.isatty():
 		if not gv.stdout.isatty():
 			line_processing = 'print'
 			line_processing = 'print'
@@ -536,8 +543,7 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 			'\n'.join(display_hdr) + '\n'
 			'\n'.join(display_hdr) + '\n'
 			+ dt.item_separator.join(display_body[top:bot])
 			+ dt.item_separator.join(display_body[top:bot])
 			+ fill
 			+ fill
-			+ footer
-		)
+			+ footer)
 
 
 	async def view_filter_and_sort(self):
 	async def view_filter_and_sort(self):
 
 
@@ -713,12 +719,13 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 					#  None:   action aborted by user or no action performed
 					#  None:   action aborted by user or no action performed
 					#  False:  an error occurred
 					#  False:  an error occurred
 					#  '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
 					ret = await action_method(parent, idx)
 					ret = await action_method(parent, idx)
 					if ret != 'redo':
 					if ret != 'redo':
 						break
 						break
 					await asyncio.sleep(0.5)
 					await asyncio.sleep(0.5)
 
 
-			if parent.scroll and ret is False:
+			if parent.scroll and ret is False or ret == 'redraw':
 				# 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(
 					CUR_HOME + ERASE_ALL +
 					CUR_HOME + ERASE_ALL +
@@ -739,7 +746,7 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 				parent.oneshot_msg = yellow(
 				parent.oneshot_msg = yellow(
 					f'{parent.proto.dcoin} balance for {parent.item_desc} #{idx} refreshed')
 					f'{parent.proto.dcoin} balance for {parent.item_desc} #{idx} refreshed')
 				if res == 0:
 				if res == 0:
-					return False # zeroing balance may mess up display
+					return 'redraw' # zeroing balance may mess up display
 
 
 		async def i_addr_delete(self, parent, idx):
 		async def i_addr_delete(self, parent, idx):
 			if not parent.keypress_confirm(
 			if not parent.keypress_confirm(
@@ -757,20 +764,22 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 
 
 		async def i_comment_add(self, parent, idx):
 		async def i_comment_add(self, parent, idx):
 
 
-			async def do_comment_add(comment):
-
-				if await parent.twctl.set_comment(
+			async def do_comment_add(comment_in):
+				from ..obj import TwComment
+				comment = await parent.twctl.set_comment(
 						addrspec     = None,
 						addrspec     = None,
-						comment      = comment,
+						comment      = comment_in,
 						trusted_pair = (entry.twmmid, entry.addr),
 						trusted_pair = (entry.twmmid, entry.addr),
-						silent       = parent.scroll):
+						silent       = parent.scroll)
+
+				if isinstance(comment, TwComment):
 					entry.comment = comment
 					entry.comment = comment
 					edited = cur_comment and comment
 					edited = cur_comment and comment
 					parent.oneshot_msg = (green if comment else yellow)('Label {a} {b}{c}'.format(
 					parent.oneshot_msg = (green if comment else yellow)('Label {a} {b}{c}'.format(
 						a = 'for' if edited else 'added to' if comment else 'removed from',
 						a = 'for' if edited else 'added to' if comment else 'removed from',
 						b = desc,
 						b = desc,
 						c = ' edited' if edited else ''))
 						c = ' edited' if edited else ''))
-					return True
+					return 'redraw' if parent.cfg.coin == 'XMR' else True
 				else:
 				else:
 					await asyncio.sleep(3)
 					await asyncio.sleep(3)
 					parent.oneshot_msg = red('Label for {desc} could not be {action}'.format(
 					parent.oneshot_msg = red('Label for {desc} could not be {action}'.format(

+ 9 - 2
mmgen/xmrwallet/__init__.py

@@ -64,6 +64,8 @@ op_names = {
 	'submit':              'submit',
 	'submit':              'submit',
 	'resubmit':            'submit',
 	'resubmit':            'submit',
 	'abort':               'submit',
 	'abort':               'submit',
+	'dump_data':           'dump',
+	'dump_json':           'dump',
 	'dump':                'dump',
 	'dump':                'dump',
 	'restore':             'restore',
 	'restore':             'restore',
 	'export_outputs':      'export',
 	'export_outputs':      'export',
@@ -84,6 +86,8 @@ kafile_arg_ops = (
 	'transfer',
 	'transfer',
 	'sweep',
 	'sweep',
 	'sweep_all',
 	'sweep_all',
+	'dump_data',
+	'dump_json',
 	'dump',
 	'dump',
 	'restore')
 	'restore')
 
 
@@ -112,8 +116,8 @@ def op_cls(op_name):
 	cls.name = op_name
 	cls.name = op_name
 	return cls
 	return cls
 
 
-def op(op, cfg, infile, wallets, *, spec=None):
-	if cfg.compat if cfg.compat is not None else cfg.xmrwallet_compat:
+def op(op, cfg, infile, wallets, *, spec=None, compat_call=False):
+	if compat_call or (cfg.compat if cfg.compat is not None else cfg.xmrwallet_compat):
 		if cfg.wallet_dir:
 		if cfg.wallet_dir:
 			die(1, '--wallet-dir can not be specified in xmrwallet compatibility mode')
 			die(1, '--wallet-dir can not be specified in xmrwallet compatibility mode')
 		from ..tw.ctl import TwCtl
 		from ..tw.ctl import TwCtl
@@ -122,5 +126,8 @@ def op(op, cfg, infile, wallets, *, spec=None):
 		cfg = Config({
 		cfg = Config({
 			'_clone': cfg,
 			'_clone': cfg,
 			'compat': True,
 			'compat': True,
+			'no_start_wallet_daemon': cfg.no_start_wallet_daemon or compat_call,
+			'daemon': cfg.daemon or cfg.monero_daemon,
+			'watch_only': cfg.watch_only or cfg.autosign or bool(cfg.autosign_mountpoint),
 			'wallet_dir': twctl_cls.get_tw_dir(cfg, cfg._proto)})
 			'wallet_dir': twctl_cls.get_tw_dir(cfg, cfg._proto)})
 	return op_cls(op)(cfg, uargs(infile, wallets, spec))
 	return op_cls(op)(cfg, uargs(infile, wallets, spec))

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

@@ -29,6 +29,7 @@ class OpBase:
 	trust_monerod = False
 	trust_monerod = False
 	do_umount = True
 	do_umount = True
 	name = None
 	name = None
+	return_data = False
 
 
 	def __init__(self, cfg, uarg_tuple):
 	def __init__(self, cfg, uarg_tuple):
 
 
@@ -110,12 +111,13 @@ 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):
+	def mount_removable_device(self, registered=[]):
 		if self.cfg.autosign:
 		if self.cfg.autosign:
 			if not self.asi.device_inserted:
 			if not self.asi.device_inserted:
 				die(1, 'Removable device not present!')
 				die(1, 'Removable device not present!')
-			if self.do_umount:
+			if self.do_umount and not registered:
 				atexit.register(lambda: self.asi.do_umount())
 				atexit.register(lambda: self.asi.do_umount())
+				registered.append(None)
 			self.asi.do_mount()
 			self.asi.do_mount()
 			self.post_mount_action()
 			self.post_mount_action()
 
 

+ 29 - 1
mmgen/xmrwallet/ops/dump.py

@@ -12,7 +12,7 @@
 xmrwallet.ops.dump: Monero wallet ops for the MMGen Suite
 xmrwallet.ops.dump: Monero wallet ops for the MMGen Suite
 """
 """
 
 
-from ...util import msg
+from ...util import msg, Msg
 
 
 from ..file.outputs import MoneroWalletDumpFile
 from ..file.outputs import MoneroWalletDumpFile
 from ..rpc import MoneroWalletRPC
 from ..rpc import MoneroWalletRPC
@@ -33,3 +33,31 @@ class OpDump(OpWallet):
 			data      = {'wallet_metadata': wallet_data.addrs_data}
 			data      = {'wallet_metadata': wallet_data.addrs_data}
 		).write()
 		).write()
 		return True
 		return True
+
+class OpDumpDataCommon(OpWallet):
+	wallet_offline = True
+	return_data = True
+
+	async def process_wallet(self, d, fn, last):
+		h = MoneroWalletRPC(self, d)
+		h.open_wallet('source', refresh=False)
+		return {
+			'seed_id': self.kal.al_id.sid,
+			'wallet_num': d.idx,
+			'data': h.get_wallet_data(print=False)}
+
+	def post_main_success(self):
+		pass
+
+class OpDumpData(OpDumpDataCommon):
+	start_daemon = False
+	stem = 'load'
+
+class OpDumpJson(OpDumpDataCommon):
+	stem = 'dump'
+
+	async def main(self):
+		import json
+		data = await super().main()
+		Msg(json.dumps(data))
+		return sum(map(bool, data))

+ 21 - 14
mmgen/xmrwallet/ops/label.py

@@ -15,6 +15,7 @@ xmrwallet.ops.label: Monero wallet ops for the MMGen Suite
 from ...color import pink, cyan, gray
 from ...color import pink, cyan, gray
 from ...util import msg, ymsg, gmsg, die, make_timestr
 from ...util import msg, ymsg, gmsg, die, make_timestr
 from ...ui import keypress_confirm
 from ...ui import keypress_confirm
+from ...obj import TwComment
 from ...addr import CoinAddr
 from ...addr import CoinAddr
 
 
 from ..rpc import MoneroWalletRPC
 from ..rpc import MoneroWalletRPC
@@ -28,7 +29,7 @@ class OpLabel(OpMixinSpec, OpWallet):
 	opts     = ()
 	opts     = ()
 	wallet_offline = True
 	wallet_offline = True
 
 
-	async def main(self):
+	async def main(self, add_timestr='ask', auto=False):
 
 
 		gmsg('\n{a} label for wallet {b}, account #{c}, address #{d}'.format(
 		gmsg('\n{a} label for wallet {b}, account #{c}, address #{d}'.format(
 			a = 'Setting' if self.label else 'Removing',
 			a = 'Setting' if self.label else 'Removing',
@@ -39,13 +40,13 @@ class OpLabel(OpMixinSpec, OpWallet):
 		h = MoneroWalletRPC(self, self.source)
 		h = MoneroWalletRPC(self, self.source)
 
 
 		h.open_wallet('source')
 		h.open_wallet('source')
-		wallet_data = h.get_wallet_data()
+		wallet_data = h.get_wallet_data(print=not auto)
 
 
 		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)
+		ret = h.print_acct_addrs(wallet_data, self.account, silent=auto)
 
 
 		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(
@@ -53,23 +54,28 @@ class OpLabel(OpMixinSpec, OpWallet):
 				len(ret) - 1))
 				len(ret) - 1))
 
 
 		addr = ret[self.address_idx]
 		addr = ret[self.address_idx]
-		new_label = f'{self.label} [{make_timestr()}]' if self.label else ''
+		if self.label and add_timestr == 'ask':
+			add_timestr = keypress_confirm(self.cfg, '\n  Add timestamp to label?')
+		new_label = TwComment(
+			(self.label + (f' [{make_timestr()}]' if add_timestr else '')) if self.label
+			else '')
 
 
-		ca = CoinAddr(self.proto, addr['address'])
-		from . import addr_width
-		msg('\n  {a} {b}\n  {c} {d}\n  {e} {f}'.format(
-				a = 'Address:       ',
-				b = ca.hl(0) if self.cfg.full_address else ca.fmt(0, addr_width, color=True),
-				c = 'Existing label:',
-				d = pink(addr['label']) if addr['label'] else gray('[none]'),
-				e = 'New label:     ',
-				f = pink(new_label) if new_label else gray('[none]')))
+		if not auto:
+			ca = CoinAddr(self.proto, addr['address'])
+			from . import addr_width
+			msg('\n  {a} {b}\n  {c} {d}\n  {e} {f}'.format(
+					a = 'Address:       ',
+					b = ca.hl(0) if self.cfg.full_address else ca.fmt(0, addr_width, color=True),
+					c = 'Existing label:',
+					d = pink(addr['label']) if addr['label'] else gray('[none]'),
+					e = 'New label:     ',
+					f = pink(new_label) if new_label else gray('[none]')))
 
 
 		op = 'remove' if not new_label else 'update' if addr['label'] else 'set'
 		op = 'remove' if not new_label else 'update' if addr['label'] else 'set'
 
 
 		if addr['label'] == new_label:
 		if addr['label'] == new_label:
 			ymsg('\nLabel is unchanged, operation cancelled')
 			ymsg('\nLabel is unchanged, operation cancelled')
-		elif keypress_confirm(self.cfg, f'  {op.capitalize()} label?'):
+		elif auto 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']
@@ -78,5 +84,6 @@ class OpLabel(OpMixinSpec, OpWallet):
 				return False
 				return False
 			else:
 			else:
 				msg(cyan('\nLabel successfully {}'.format('set' if op == 'set' else op+'d')))
 				msg(cyan('\nLabel successfully {}'.format('set' if op == 'set' else op+'d')))
+				return new_label
 		else:
 		else:
 			ymsg('\nOperation cancelled by user request')
 			ymsg('\nOperation cancelled by user request')

+ 2 - 2
mmgen/xmrwallet/ops/new.py

@@ -14,7 +14,7 @@ xmrwallet.ops.new: Monero wallet ops for the MMGen Suite
 
 
 from ...color import red, pink
 from ...color import red, pink
 from ...util import msg, ymsg, make_timestr
 from ...util import msg, ymsg, make_timestr
-
+from ...obj import TwComment
 from ...ui import keypress_confirm
 from ...ui import keypress_confirm
 
 
 from ..rpc import MoneroWalletRPC
 from ..rpc import MoneroWalletRPC
@@ -32,7 +32,7 @@ class OpNew(OpMixinSpec, OpWallet):
 		h.open_wallet('Monero')
 		h.open_wallet('Monero')
 
 
 		desc = 'account' if self.account is None else 'address'
 		desc = 'account' if self.account is None else 'address'
-		label = (
+		label = TwComment(
 			None if self.label == '' else
 			None if self.label == '' else
 			'{} [{}]'.format(self.label or f'xmrwallet new {desc}', make_timestr()))
 			'{} [{}]'.format(self.label or f'xmrwallet new {desc}', make_timestr()))
 
 

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

@@ -53,8 +53,7 @@ class OpTxview(OpBase):
 			(self.hdr if len(files) > 1 else '')
 			(self.hdr if len(files) > 1 else '')
 			+ self.col_hdr
 			+ self.col_hdr
 			+ '\n'.join(getattr(tx, self.view_method)(addr_w=addr_w) for tx in txs)
 			+ '\n'.join(getattr(tx, self.view_method)(addr_w=addr_w) for tx in txs)
-			+ self.footer
-		)
+			+ self.footer)
 
 
 class OpTxlist(OpTxview):
 class OpTxlist(OpTxview):
 	view_method = 'get_info_oneline'
 	view_method = 'get_info_oneline'
@@ -89,6 +88,6 @@ class OpTxlist(OpTxview):
 			from ...term import get_terminal_size
 			from ...term import get_terminal_size
 			cols = self.cfg.columns or get_terminal_size().width
 			cols = self.cfg.columns or get_terminal_size().width
 			if cols < self.fixed_cols_w + self.min_addr_w:
 			if cols < self.fixed_cols_w + self.min_addr_w:
-				die(1, f'A terminal at least {self.fixed_cols_w + self.min_addr_w} columns wide is required '
-						'to display this output (or use --columns or --pager)')
+				die(1, f'A terminal at least {self.fixed_cols_w + self.min_addr_w} columns wide is'
+						' required to display this output (or use --columns or --pager)')
 		await super().main(cols=cols)
 		await super().main(cols=cols)

+ 29 - 27
mmgen/xmrwallet/ops/wallet.py

@@ -32,8 +32,7 @@ class OpWallet(OpBase):
 		'no_start_wallet_daemon',
 		'no_start_wallet_daemon',
 		'no_stop_wallet_daemon',
 		'no_stop_wallet_daemon',
 		'autosign',
 		'autosign',
-		'watch_only',
-	)
+		'watch_only')
 	wallet_offline = False
 	wallet_offline = False
 	wallet_exists = True
 	wallet_exists = True
 	start_daemon = True
 	start_daemon = True
@@ -70,8 +69,7 @@ class OpWallet(OpBase):
 			test_suite  = self.cfg.test_suite,
 			test_suite  = self.cfg.test_suite,
 			monerod_addr = self.cfg.daemon or None,
 			monerod_addr = self.cfg.daemon or None,
 			trust_monerod = self.trust_monerod,
 			trust_monerod = self.trust_monerod,
-			test_monerod = not self.wallet_offline,
-		)
+			test_monerod = not self.wallet_offline)
 
 
 		if self.wallet_offline:
 		if self.wallet_offline:
 			self.wd.usr_daemon_args = ['--offline']
 			self.wd.usr_daemon_args = ['--offline']
@@ -79,8 +77,7 @@ class OpWallet(OpBase):
 		self.c = MoneroWalletRPCClient(
 		self.c = MoneroWalletRPCClient(
 			cfg             = self.cfg,
 			cfg             = self.cfg,
 			daemon          = self.wd,
 			daemon          = self.wd,
-			test_connection = False,
-		)
+			test_connection = False)
 
 
 		if self.cfg.offline:
 		if self.cfg.offline:
 			from ...wallet import Wallet
 			from ...wallet import Wallet
@@ -100,11 +97,14 @@ class OpWallet(OpBase):
 			self.mount_removable_device()
 			self.mount_removable_device()
 			# with watch_only, make a second attempt to open the file as KeyAddrList:
 			# with watch_only, make a second attempt to open the file as KeyAddrList:
 			for first_try in (True, False):
 			for first_try in (True, False):
+				addr_list = ViewKeyAddrList if (self.cfg.watch_only and first_try) else KeyAddrList
 				try:
 				try:
-					self.kal = (ViewKeyAddrList if (self.cfg.watch_only and first_try) else KeyAddrList)(
+					self.kal = addr_list(
 						cfg    = cfg,
 						cfg    = cfg,
 						proto  = self.proto,
 						proto  = self.proto,
-						infile = str(self.autosign_viewkey_addr_file) if self.cfg.autosign else self.uargs.infile,
+						infile =
+							str(self.autosign_viewkey_addr_file) if self.cfg.autosign else
+							self.uargs.infile,
 						key_address_validity_check = True,
 						key_address_validity_check = True,
 						skip_chksum_msg = True)
 						skip_chksum_msg = True)
 					break
 					break
@@ -134,7 +134,9 @@ class OpWallet(OpBase):
 
 
 	def get_coin_daemon_rpc(self):
 	def get_coin_daemon_rpc(self):
 
 
-		host, port = self.cfg.daemon.split(':') if self.cfg.daemon else ('localhost', self.wd.monerod_port)
+		host, port = (
+			self.cfg.daemon.split(':') if self.cfg.daemon else
+			('localhost', self.wd.monerod_port))
 
 
 		from ...daemon import CoinDaemon
 		from ...daemon import CoinDaemon
 		return MoneroRPCClient(
 		return MoneroRPCClient(
@@ -154,10 +156,9 @@ class OpWallet(OpBase):
 			die(2,
 			die(2,
 				'{a} viewkey-address files found in autosign mountpoint directory ‘{b}’!\n'.format(
 				'{a} viewkey-address files found in autosign mountpoint directory ‘{b}’!\n'.format(
 					a = 'Multiple' if flist else 'No',
 					a = 'Multiple' if flist else 'No',
-					b = self.asi.xmr_dir
-				)
-				+ 'Have you run ‘mmgen-autosign setup’ on your offline machine with the --xmrwallets option?'
-			)
+					b = self.asi.xmr_dir)
+				+ 'Have you run ‘mmgen-autosign setup’ on your offline machine'
+				' with the --xmrwallets option?')
 		else:
 		else:
 			return flist[0]
 			return flist[0]
 
 
@@ -166,12 +167,16 @@ class OpWallet(OpBase):
 			idxs = AddrIdxList(fmt_str=self.uargs.wallets)
 			idxs = AddrIdxList(fmt_str=self.uargs.wallets)
 			self.addr_data = [d for d in self.kal.data if d.idx in idxs]
 			self.addr_data = [d for d in self.kal.data if d.idx in idxs]
 			if len(self.addr_data) != len(idxs):
 			if len(self.addr_data) != len(idxs):
-				die(1, f'List {self.uargs.wallets!r} contains addresses not present in supplied key-address file')
+				die(1,
+					f'List {self.uargs.wallets!r} contains addresses not present'
+					' in supplied key-address file')
 		else:
 		else:
 			self.addr_data = self.kal.data
 			self.addr_data = self.kal.data
 
 
-	async def restart_wallet_daemon(self):
-		atexit.register(lambda: asyncio.run(self.stop_wallet_daemon()))
+	async def restart_wallet_daemon(self, registered=[]):
+		if not registered:
+			atexit.register(lambda: asyncio.run(self.stop_wallet_daemon()))
+			registered.append(None)
 		await self.c.restart_daemon()
 		await self.c.restart_daemon()
 
 
 	async def stop_wallet_daemon(self):
 	async def stop_wallet_daemon(self):
@@ -179,7 +184,7 @@ class OpWallet(OpBase):
 			try:
 			try:
 				await self.c.stop_daemon()
 				await self.c.stop_daemon()
 			except KeyboardInterrupt:
 			except KeyboardInterrupt:
-				ymsg('\nForce killing wallet daemon')
+				ymsg('\nForce-killing wallet daemon')
 				self.c.daemon.force_kill = True
 				self.c.daemon.force_kill = True
 				self.c.daemon.stop()
 				self.c.daemon.stop()
 
 
@@ -192,8 +197,7 @@ class OpWallet(OpBase):
 				a = self.kal.al_id.sid,
 				a = self.kal.al_id.sid,
 				b = data.idx,
 				b = data.idx,
 				c = 'WatchOnly' if watch_only else '',
 				c = 'WatchOnly' if watch_only else '',
-				d = f'.{self.cfg.network}' if self.cfg.network != 'mainnet' else '')
-		)
+				d = f'.{self.cfg.network}' if self.cfg.network != 'mainnet' else ''))
 
 
 	@property
 	@property
 	def add_wallet_desc(self):
 	def add_wallet_desc(self):
@@ -205,23 +209,21 @@ class OpWallet(OpBase):
 			b = len(self.addr_data),
 			b = len(self.addr_data),
 			c = self.add_wallet_desc,
 			c = self.add_wallet_desc,
 			d = suf(self.addr_data)))
 			d = suf(self.addr_data)))
-		processed = 0
+		data = []
 		for n, d in enumerate(self.addr_data): # [d.sec,d.addr,d.wallet_passwd,d.viewkey]
 		for n, d in enumerate(self.addr_data): # [d.sec,d.addr,d.wallet_passwd,d.viewkey]
 			fn = self.get_wallet_fn(d)
 			fn = self.get_wallet_fn(d)
 			gmsg('\n{a}ing wallet {b}/{c} ({d})'.format(
 			gmsg('\n{a}ing wallet {b}/{c} ({d})'.format(
 				a = self.stem.capitalize(),
 				a = self.stem.capitalize(),
 				b = n + 1,
 				b = n + 1,
 				c = len(self.addr_data),
 				c = len(self.addr_data),
-				d = fn.name,
-			))
-			processed += await self.process_wallet(d, fn, last=n==len(self.addr_data)-1)
-		gmsg(f'\n{processed} wallet{suf(processed)} {self.stem}ed\n')
-		return processed
+				d = fn.name))
+			data.append(await self.process_wallet(d, fn, last=n == len(self.addr_data) - 1))
+		gmsg(f'\n{len(data)} wallet{suf(len(data))} {self.stem}ed\n')
+		return data if self.return_data else sum(map(bool, data))
 
 
 	def head_msg(self, wallet_idx, fn):
 	def head_msg(self, wallet_idx, fn):
 		gmsg('\n{a} {b}wallet #{c} ({d})'.format(
 		gmsg('\n{a} {b}wallet #{c} ({d})'.format(
 			a = self.action.capitalize(),
 			a = self.action.capitalize(),
 			b = self.add_wallet_desc,
 			b = self.add_wallet_desc,
 			c = wallet_idx,
 			c = wallet_idx,
-			d = fn.name
-		))
+			d = fn.name))

+ 6 - 5
mmgen/xmrwallet/rpc.py

@@ -110,11 +110,12 @@ class MoneroWalletRPC:
 		msg('      Address: {}'.format(cyan(ret['base_address'])))
 		msg('      Address: {}'.format(cyan(ret['base_address'])))
 		return (ret['account_index'], ret['base_address'])
 		return (ret['account_index'], ret['base_address'])
 
 
-	def print_acct_addrs(self, wallet_data, account):
-		msg('\n      Addresses of account #{} ({}):'.format(
-			account,
-			wallet_data.accts_data['subaddress_accounts'][account]['label']))
-		msg('\n'.join(gen_acct_addr_info(self, wallet_data, account, indent='        ')))
+	def print_acct_addrs(self, wallet_data, account, silent=False):
+		if not silent:
+			msg('\n      Addresses of account #{} ({}):'.format(
+				account,
+				wallet_data.accts_data['subaddress_accounts'][account]['label']))
+			msg('\n'.join(gen_acct_addr_info(self, wallet_data, account, indent='        ')))
 		return wallet_data.addrs_data[account]['addresses']
 		return wallet_data.addrs_data[account]['addresses']
 
 
 	def create_new_addr(self, account, label):
 	def create_new_addr(self, account, label):

+ 1 - 0
test/cmdtest_d/include/cfg.py

@@ -49,6 +49,7 @@ cmd_groups_dfl = {
 	'runeswap':           gd('CmdTestRuneSwap',          {}),
 	'runeswap':           gd('CmdTestRuneSwap',          {}),
 	'xmrwallet':          gd('CmdTestXMRWallet',         {}),
 	'xmrwallet':          gd('CmdTestXMRWallet',         {}),
 	'xmr_autosign':       gd('CmdTestXMRAutosign',       {}),
 	'xmr_autosign':       gd('CmdTestXMRAutosign',       {}),
+	'xmr_compat':         gd('CmdTestXMRCompat',         {'modname': 'xmr_autosign'}),
 }
 }
 
 
 cmd_groups_extra = {
 cmd_groups_extra = {

+ 1 - 1
test/cmdtest_d/include/runner.py

@@ -168,7 +168,7 @@ class CmdTestRunner:
 
 
 		if self.logging:
 		if self.logging:
 			self.log_fd.write('[{}][{}:{}] {}\n'.format(
 			self.log_fd.write('[{}][{}:{}] {}\n'.format(
-				(self.proto.coin.lower() if 'coin' in self.tg.passthru_opts else 'NONE'),
+				self.proto.coin.lower(),
 				self.tg.group_name,
 				self.tg.group_name,
 				self.tg.test_name,
 				self.tg.test_name,
 				cmd_disp))
 				cmd_disp))

+ 78 - 11
test/cmdtest_d/xmr_autosign.py

@@ -17,8 +17,8 @@ import re, asyncio
 
 
 from mmgen.color import blue, cyan, brown
 from mmgen.color import blue, cyan, brown
 
 
-from ..include.common import imsg, silence, end_silence
-from .include.common import get_file_with_ext
+from ..include.common import imsg, silence, end_silence, strip_ansi_escapes
+from .include.common import get_file_with_ext, cleanup_env
 
 
 from .xmrwallet import CmdTestXMRWallet
 from .xmrwallet import CmdTestXMRWallet
 from .autosign import CmdTestAutosignThreaded
 from .autosign import CmdTestAutosignThreaded
@@ -50,18 +50,19 @@ class CmdTestXMRAutosign(CmdTestXMRWallet, CmdTestAutosignThreaded):
 
 
 	cmd_group = (
 	cmd_group = (
 		('daemon_version',           'checking daemon version'),
 		('daemon_version',           'checking daemon version'),
-		('gen_kafile_miner',         'generating key-address file for Miner'),
-		('create_wallet_miner',      'creating Monero wallet for Miner'),
-		('mine_initial_coins',       'mining initial coins'),
 		('create_tmp_wallets',       'creating temporary online wallets for Alice'),
 		('create_tmp_wallets',       'creating temporary online wallets for Alice'),
 		('new_account_alice',        'adding an account to Alice’s tmp wallet'),
 		('new_account_alice',        'adding an account to Alice’s tmp wallet'),
 		('new_address_alice',        'adding an address to Alice’s tmp wallet'),
 		('new_address_alice',        'adding an address to Alice’s tmp wallet'),
 		('new_address_alice_label',  'adding an address to Alice’s tmp wallet (with label)'),
 		('new_address_alice_label',  'adding an address to Alice’s tmp wallet (with label)'),
 		('dump_tmp_wallets',         'dumping Alice’s tmp wallets'),
 		('dump_tmp_wallets',         'dumping Alice’s tmp wallets'),
+		('dump_tmp_wallets_json',    'dumping Alice’s tmp wallets to JSON format'),
 		('delete_tmp_wallets',       'deleting Alice’s tmp wallets'),
 		('delete_tmp_wallets',       'deleting Alice’s tmp wallets'),
+		('gen_kafile_miner',         'generating key-address file for Miner'),
+		('create_wallet_miner',      'creating Monero wallet for Miner'),
+		('mine_initial_coins',       'mining initial coins'),
 		('autosign_setup',           'autosign setup with Alice’s seed'),
 		('autosign_setup',           'autosign setup with Alice’s seed'),
 		('autosign_xmr_setup',       'autosign setup (creation of Monero signing wallets)'),
 		('autosign_xmr_setup',       'autosign setup (creation of Monero signing wallets)'),
-		('create_watchonly_wallets', 'creating watch-only wallets from Alice’s wallet dumps'),
+		('restore_watchonly_wallets', 'creating watch-only wallets from Alice’s wallet dumps'),
 		('delete_tmp_dump_files',    'deleting Alice’s dump files'),
 		('delete_tmp_dump_files',    'deleting Alice’s dump files'),
 		('fund_alice1',              'sending funds to Alice (wallet #1)'),
 		('fund_alice1',              'sending funds to Alice (wallet #1)'),
 		('check_bal_alice1',         'mining, checking balance (wallet #1)'),
 		('check_bal_alice1',         'mining, checking balance (wallet #1)'),
@@ -174,10 +175,13 @@ class CmdTestXMRAutosign(CmdTestXMRWallet, CmdTestAutosignThreaded):
 	def dump_tmp_wallets(self):
 	def dump_tmp_wallets(self):
 		return self._dump_wallets(autosign=False)
 		return self._dump_wallets(autosign=False)
 
 
+	def dump_tmp_wallets_json(self):
+		return self._dump_wallets(autosign=False, op='dump_json')
+
 	def dump_wallets(self):
 	def dump_wallets(self):
 		return self._dump_wallets(autosign=True)
 		return self._dump_wallets(autosign=True)
 
 
-	def _dump_wallets(self, autosign):
+	def _dump_wallets(self, autosign, op='dump'):
 		data = self.users['alice']
 		data = self.users['alice']
 		self.insert_device_online()
 		self.insert_device_online()
 		t = self.spawn(
 		t = self.spawn(
@@ -186,10 +190,14 @@ class CmdTestXMRAutosign(CmdTestXMRWallet, CmdTestAutosignThreaded):
 			+ (['--alice', '--compat'] if self.compat else [f'--wallet-dir={data.udir}'])
 			+ (['--alice', '--compat'] if self.compat else [f'--wallet-dir={data.udir}'])
 			+ [f'--daemon=localhost:{data.md.rpc_port}']
 			+ [f'--daemon=localhost:{data.md.rpc_port}']
 			+ (self.autosign_opts if autosign else [])
 			+ (self.autosign_opts if autosign else [])
-			+ ['dump']
-			+ ([] if autosign else [get_file_with_ext(data.udir, 'akeys')]))
+			+ [op]
+			+ ([] if autosign else [get_file_with_ext(data.udir, 'akeys')]),
+			env = cleanup_env(self.cfg))
 		t.expect('2 wallets dumped')
 		t.expect('2 wallets dumped')
-		t.read()
+		res = t.read()
+		if op == 'dump_json':
+			import json
+			data = json.loads(re.sub('Stopping.*', '', strip_ansi_escapes(res)).strip())
 		self.remove_device_online()
 		self.remove_device_online()
 		return t
 		return t
 
 
@@ -222,6 +230,8 @@ class CmdTestXMRAutosign(CmdTestXMRWallet, CmdTestAutosignThreaded):
 	async def fund_alice1(self):
 	async def fund_alice1(self):
 		return await self.fund_alice(wallet=1)
 		return await self.fund_alice(wallet=1)
 
 
+	fund_alice1b = fund_alice1
+
 	async def check_bal_alice1(self):
 	async def check_bal_alice1(self):
 		return await self.check_bal_alice(wallet=1)
 		return await self.check_bal_alice(wallet=1)
 
 
@@ -253,7 +263,7 @@ class CmdTestXMRAutosign(CmdTestXMRWallet, CmdTestAutosignThreaded):
 		self.remove_device()
 		self.remove_device()
 		return t
 		return t
 
 
-	def create_watchonly_wallets(self):
+	def restore_watchonly_wallets(self):
 		return self._create_wallets('restore')
 		return self._create_wallets('restore')
 
 
 	def restore_wallets(self):
 	def restore_wallets(self):
@@ -484,3 +494,60 @@ class CmdTestXMRAutosignNoCompat(CmdTestXMRAutosign):
 	Monero autosigning operations (non-xmrwallet compat mode)
 	Monero autosigning operations (non-xmrwallet compat mode)
 	"""
 	"""
 	compat = False
 	compat = False
+
+class CmdTestXMRCompat(CmdTestXMRAutosign):
+	"""
+	Monero autosigning operations (compat mode)
+	"""
+	menu_prompt = 'efresh balances:\b'
+
+	cmd_group = (
+		('autosign_setup',           'autosign setup with Alice’s seed'),
+		('autosign_xmr_setup',       'autosign setup (creation of Monero signing wallets)'),
+		('create_watchonly_wallets', 'creating Alice’s watch-only wallets'),
+		('gen_kafile_miner',         'generating key-address file for Miner'),
+		('create_wallet_miner',      'creating Monero wallet for Miner'),
+		('mine_initial_coins',       'mining initial coins'),
+		('fund_alice1',              'sending funds to Alice (wallet #1)'),
+		('mine_blocks',              'mining some blocks'),
+		('alice_listaddresses',      'performing operations on Alice’s tracking wallets (listaddresses)'),
+		('fund_alice1b',             'sending funds to Alice (wallet #1)'),
+		('mine_blocks',              'mining some blocks'),
+		('alice_twview',             'performing operations on Alice’s tracking wallets (twview)'),
+	)
+
+	def __init__(self, cfg, trunner, cfgs, spawn):
+		super().__init__(cfg, trunner, cfgs, spawn)
+		if trunner is None:
+			return
+		self.alice_opts = [
+			'--alice',
+			'--coin=xmr',
+			'--monero-wallet-rpc-password=passwOrd',
+			f'--monero-daemon=localhost:{self.users["alice"].md.rpc_port}']
+
+	def create_watchonly_wallets(self):
+		return self._create_wallets()
+
+	async def mine_blocks(self):
+		self.spawn(msg_only=True)
+		return await self.mine(10)
+
+	def alice_listaddresses(self):
+		return self._alice_twops('listaddresses', 2, 'y', r'Primary account.*1\.234567891234')
+
+	def alice_twview(self):
+		return self._alice_twops('twview', 1, 'n', r'New Label.*2\.469135782468')
+
+	def _alice_twops(self, op, addr_num, add_timestr_resp, expect_str):
+		self.insert_device_online()
+		t = self.spawn('mmgen-tool', self.alice_opts + self.autosign_opts + [op, 'interactive=1'])
+		t.expect(self.menu_prompt, 'l')
+		t.expect('main menu): ', str(addr_num))
+		t.expect(': ', 'New Label\n')
+		t.expect('(y/N): ', add_timestr_resp)
+		t.expect(self.menu_prompt, 'R')
+		t.expect(expect_str, regex=True)
+		t.expect(self.menu_prompt, 'q')
+		self.remove_device_online()
+		return t

+ 19 - 5
test/cmdtest_d/xmrwallet.py

@@ -59,7 +59,6 @@ class CmdTestXMRWallet(CmdTestBase):
 	"""
 	"""
 
 
 	networks = ('xmr',)
 	networks = ('xmr',)
-	passthru_opts = ()
 	tmpdir_nums = [29]
 	tmpdir_nums = [29]
 	dfl_random_txs = 3
 	dfl_random_txs = 3
 	color = True
 	color = True
@@ -377,15 +376,28 @@ class CmdTestXMRWallet(CmdTestBase):
 			random_txs = self.dfl_random_txs)
 			random_txs = self.dfl_random_txs)
 
 
 	def set_label_miner(self):
 	def set_label_miner(self):
-		return self.set_label_user('miner', '1:0:0,"Miner’s new primary account label [1:0:0]"', 'updated')
+		return self.set_label_user(
+			'miner',
+			'1:0:0,"Miner’s new primary account label [1:0:0]"',
+			'y',
+			'updated')
 
 
 	def remove_label_alice(self):
 	def remove_label_alice(self):
-		return self.set_label_user('alice', '4:2:2,""', 'removed', add_opts=['--full-address'])
+		return self.set_label_user(
+			'alice',
+			'4:2:2,""',
+			None,
+			'removed',
+			add_opts = ['--full-address'])
 
 
 	def set_label_alice(self):
 	def set_label_alice(self):
-		return self.set_label_user('alice', '4:2:2,"Alice’s new subaddress label [4:2:2]"', 'set')
+		return self.set_label_user(
+			'alice',
+			'4:2:2,"Alice’s new subaddress label [4:2:2]"',
+			'n',
+			'set')
 
 
-	def set_label_user(self, user, label_spec, expect, add_opts=[]):
+	def set_label_user(self, user, label_spec, add_timestr_resp, expect, add_opts=[]):
 		data = self.users[user]
 		data = self.users[user]
 		cmd_opts = [f'--wallet-dir={data.udir}', f'--daemon=localhost:{data.md.rpc_port}']
 		cmd_opts = [f'--wallet-dir={data.udir}', f'--daemon=localhost:{data.md.rpc_port}']
 		t = self.spawn(
 		t = self.spawn(
@@ -394,6 +406,8 @@ class CmdTestXMRWallet(CmdTestBase):
 			+ add_opts
 			+ add_opts
 			+ cmd_opts
 			+ cmd_opts
 			+ ['label', data.kafile, label_spec])
 			+ ['label', data.kafile, label_spec])
+		if add_timestr_resp:
+			t.expect('(y/N): ', add_timestr_resp)
 		t.expect('(y/N): ', 'y')
 		t.expect('(y/N): ', 'y')
 		t.expect(f'Label successfully {expect}')
 		t.expect(f'Label successfully {expect}')
 		return t
 		return t