Browse Source

XMR compat: support tracking wallet views

- all operations, including label editing and balance refresh, supported
- requires --autosign
- enables --xmrwallet-compat, meaning Monero watch-only wallets must be
  located in ~/.mmgen/altcoins/xmr/tracking-wallets

This is a work in progress, UI will undergo changes.

Examples:

    $ mmgen-tool --coin=xmr listaddresses interactive=1
    $ mmgen-tool --coin=xmr twview interactive=1

Testing:

    $ test/cmdtest.py -ne xmr_compat
The MMGen Project 1 week ago
parent
commit
bdd7dd3393

+ 12 - 1
mmgen/addr.py

@@ -147,12 +147,23 @@ class MMGenID(HiliteStr, InitErrors, MMGenObject):
 				idx, ext = idx.split('-', 1)
 				idx, ext = idx.split('-', 1)
 				me.acct_idx, me.addr_idx = [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

+ 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

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

+ 5 - 4
mmgen/tw/view.py

@@ -192,10 +192,11 @@ 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_data := self.prompt_fs_repl.get(self.proto.coin):
 		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:
 			for repl in [repl_data] if isinstance(repl_data[0], int) else repl_data:
@@ -441,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))
@@ -778,7 +779,7 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 						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(

+ 5 - 2
mmgen/xmrwallet/__init__.py

@@ -116,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
@@ -126,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))

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

@@ -111,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()
 
 

+ 15 - 13
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'):
+	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',
@@ -40,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(
@@ -60,21 +60,22 @@ 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 '')
 
 
-		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']
@@ -83,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()))
 
 

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

@@ -173,8 +173,10 @@ class OpWallet(OpBase):
 		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):

+ 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 = {

+ 59 - 0
test/cmdtest_d/xmr_autosign.py

@@ -230,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)
 
 
@@ -492,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