5 Commits 4a4b814935 ... d8439ba691

Author SHA1 Message Date
  The MMGen Project d8439ba691 XMR compat: addrimport support 2 months ago
  The MMGen Project c4ec627153 mmgen-txbump: support transaction selection 2 months ago
  The MMGen Project a6ad36fd73 addrimport, txcreate: help screen fixes and cleanups 2 months ago
  The MMGen Project 38ae740035 xmrwallet OpCreate: ignore existing wallets instead of raising exception 2 months ago
  The MMGen Project cf5f7204c7 xmrwallet.ops.wallet, mmgen-addrimport: cleanups 2 months ago

+ 3 - 5
mmgen/autosign.py

@@ -334,13 +334,13 @@ class Signable:
 				shred_file(self.cfg, fn, iterations=15)
 				shred_file(self.cfg, fn, iterations=15)
 			sys.exit(0)
 			sys.exit(0)
 
 
-		async def get_last_sent(self, *, tx_range=None):
+		async def get_last_sent(self, *, tx_range):
 			return await self.get_last_created(
 			return await self.get_last_created(
 				# compat fallback - ‘sent_timestamp’ attr is missing in some old TX files:
 				# compat fallback - ‘sent_timestamp’ attr is missing in some old TX files:
 				sort_key = lambda x: x.sent_timestamp or x.timestamp,
 				sort_key = lambda x: x.sent_timestamp or x.timestamp,
 				tx_range = tx_range)
 				tx_range = tx_range)
 
 
-		async def get_last_created(self, *, sort_key=lambda x: x.timestamp, tx_range=None):
+		async def get_last_created(self, *, tx_range, sort_key=lambda x: x.timestamp):
 			from .tx import CompletedTX
 			from .tx import CompletedTX
 			fns = [f for f in self.dir.iterdir() if f.name.endswith(self.subext)]
 			fns = [f for f in self.dir.iterdir() if f.name.endswith(self.subext)]
 			files = sorted(
 			files = sorted(
@@ -348,9 +348,7 @@ class Signable:
 					for txfile in fns],
 					for txfile in fns],
 				key = sort_key)
 				key = sort_key)
 			if files:
 			if files:
-				return (
-					files[-1] if tx_range is None else
-					files[len(files) - 1 - tx_range.last:len(files) - tx_range.first])
+				return files[len(files) - 1 - tx_range.last:len(files) - tx_range.first]
 			else:
 			else:
 				die(1, 'No sent automount transactions!')
 				die(1, 'No sent automount transactions!')
 
 

+ 1 - 1
mmgen/data/version

@@ -1 +1 @@
-16.1.dev30
+16.1.dev31

+ 4 - 1
mmgen/help/help_notes.py

@@ -21,7 +21,10 @@ class help_notes:
 	def txcreate_args(self):
 	def txcreate_args(self):
 		match self.proto.base_proto:
 		match self.proto.base_proto:
 			case 'Bitcoin':
 			case 'Bitcoin':
-				return '[ADDR,AMT ... | DATA_SPEC] ADDR [addr file ...]'
+				if self.cfg.autosign:
+					return '[ADDR,AMT ... | DATA_SPEC] ADDR'
+				else:
+					return '[ADDR,AMT ... | DATA_SPEC] ADDR [addr file ...]'
 			case 'Monero':
 			case 'Monero':
 				return '[ADDR,AMT]'
 				return '[ADDR,AMT]'
 			case _:
 			case _:

+ 52 - 6
mmgen/main_addrimport.py

@@ -33,7 +33,11 @@ opts_data = {
 		'options': """
 		'options': """
 -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
--a, --address=a    Import the single coin address 'a'
+-a, --autosign     Import addresses from pre-created key-address file on the
+                   removable device.  The removable device is mounted and
+                   unmounted automatically.  This option is available for XMR
+                   only (see XMR NOTES below).
+-A, --address=ADDR Import the single coin address ADDR
 -b, --batch        Import all addresses in one RPC call
 -b, --batch        Import all addresses in one RPC call
 -l, --addrlist     Address source is a flat list of non-MMGen coin addresses
 -l, --addrlist     Address source is a flat list of non-MMGen coin addresses
 -k, --keyaddr-file Address source is a key-address file
 -k, --keyaddr-file Address source is a key-address file
@@ -42,24 +46,46 @@ opts_data = {
                    blockchain for unspent outputs that include the imported
                    blockchain for unspent outputs that include the imported
                    address(es).  Required if any of the imported addresses
                    address(es).  Required if any of the imported addresses
                    are already in the blockchain and have a balance.
                    are already in the blockchain and have a balance.
--t, --token-addr=A Import addresses for ERC20 token with address 'A'
+-t, --token-addr=ADDR Import addresses for ERC20 token with address ADDR
 """,
 """,
 	'notes': """
 	'notes': """
+                                   XMR NOTES
 
 
-This command can also be used to update the comment fields or balances of
-addresses already in the tracking wallet.
+For Monero, --autosign is required, and a key-address file on the removable
+device is used instead of an address file.  Specifying the file explicitly
+on the command line is not supported.
+
+When ‘mmgen-autosign setup’ (or ‘xmr_setup’) is run with the --xmrwallets
+option, an ephemeral Monero wallet is created for each wallet number listed,
+to be used for transaction signing. In addition, a key-address file is created
+on the removable device, with an address and viewkey matching the base address
+of each signing wallet.
+
+This script uses that file to create an online view-only tracking wallet to
+match each offline signing wallet.  If a tracking wallet for a given address
+already exists, it is left untouched and no action is performed.  To create
+additional tracking wallets, just specify new wallet numbers via --xmrwallets
+during the offline setup process.
+
+
+                           NOTES FOR BTC, LTC AND BCH
 
 
 Rescanning now uses the ‘scantxoutset’ RPC call and a selective scan of
 Rescanning now uses the ‘scantxoutset’ RPC call and a selective scan of
 blocks containing the relevant UTXOs for much faster performance than the
 blocks containing the relevant UTXOs for much faster performance than the
 previous implementation.  The rescan operation typically takes around two
 previous implementation.  The rescan operation typically takes around two
 minutes total, independent of the number of addresses imported.
 minutes total, independent of the number of addresses imported.
 
 
+It’s recommended to use ‘--rpc-backend=aio’ with ‘--rescan’.
+
 Bear in mind that the UTXO scan will not find historical transactions: to add
 Bear in mind that the UTXO scan will not find historical transactions: to add
 them to the tracking wallet, you must perform a full or partial rescan of the
 them to the tracking wallet, you must perform a full or partial rescan of the
 blockchain with the ‘mmgen-tool rescan_blockchain’ utility.  A full rescan of
 blockchain with the ‘mmgen-tool rescan_blockchain’ utility.  A full rescan of
 the blockchain may take up to several hours.
 the blockchain may take up to several hours.
 
 
-It’s recommended to use ‘--rpc-backend=aio’ with ‘--rescan’.
+A full rescan is required if you plan to use ‘mmgen-tool txhist’ or the
+automatic change address functionality of ‘mmgen-txcreate’, or wish to see
+which addresses in your tracking wallet are used.  Without it, all addresses
+without balances will be displayed as new.
 """
 """
 	}
 	}
 }
 }
@@ -130,9 +156,29 @@ def check_opts(twctl):
 
 
 	return batch, rescan
 	return batch, rescan
 
 
+def check_xmr_args():
+	for k in ('address', 'batch', 'addrlist', 'keyaddr_file', 'rescan', 'token_addr'):
+		if getattr(cfg, k):
+			die(1, 'Option --{} not supported for XMR'.format(k.replace('_', '-')))
+	if not cfg.autosign:
+		die(1, 'For XMR address import, --autosign is required')
+	if cfg._args:
+		die(1, 'Address file arg not supported with --autosign')
+
 async def main():
 async def main():
-	from .tw.ctl import TwCtl
 
 
+	if cfg._proto.base_coin == 'XMR':
+		from .tx.util import mount_removable_device
+		from .xmrwallet import op as xmrwallet_op
+		check_xmr_args()
+		mount_removable_device(cfg)
+		op = xmrwallet_op('create', cfg, None, None, compat_call=True)
+		if op.to_process:
+			await op.restart_wallet_daemon()
+			await op.main()
+		return
+
+	from .tw.ctl import TwCtl
 	twctl = await TwCtl(
 	twctl = await TwCtl(
 		cfg        = cfg,
 		cfg        = cfg,
 		proto      = proto,
 		proto      = proto,

+ 13 - 5
mmgen/main_txbump.py

@@ -23,7 +23,7 @@ mmgen-txbump: Create, and optionally send and sign, a replacement transaction on
 import sys
 import sys
 
 
 from .cfg import gc, Config
 from .cfg import gc, Config
-from .util import msg, msg_r, die, async_run
+from .util import msg, msg_r, die, is_int, async_run
 from .color import green
 from .color import green
 
 
 opts_data = {
 opts_data = {
@@ -32,11 +32,13 @@ opts_data = {
 	'text': {
 	'text': {
 		'desc': """
 		'desc': """
                 Create, and optionally send and sign, a replacement transaction
                 Create, and optionally send and sign, a replacement transaction
-                on supporting networks
+                on supported networks
 		 """,
 		 """,
 		'usage2':   (
 		'usage2':   (
 			f'[opts] [{gc.proj_name} TX file] [seed source] ...',
 			f'[opts] [{gc.proj_name} TX file] [seed source] ...',
 			f'[opts] {{u_args}} [{gc.proj_name} TX file] [seed source] ...',
 			f'[opts] {{u_args}} [{gc.proj_name} TX file] [seed source] ...',
+			'--autosign [opts] [index]',
+			'--autosign [opts] [index] {u_args}',
 		),
 		),
 		'options': """
 		'options': """
 			-- -h, --help             Print this help message
 			-- -h, --help             Print this help message
@@ -94,8 +96,12 @@ opts_data = {
 """,
 """,
 	'notes': """
 	'notes': """
 
 
-With --autosign, the TX file argument is omitted, and the last submitted TX
-file on the removable device will be used.
+With --autosign, the TX file argument is omitted, and the last submitted
+transaction on the removable device will be used.  Or, if the first non-option
+argument is a non-negative integer, it specifies an index into the list of
+submitted transactions, in reverse chronological order, and that transaction
+will be bumped.  ‘0’ (the default) signifies the last sent transaction, ‘1’
+the next-to-last, and so on.
 
 
 If no outputs are specified, the original outputs will be used for the
 If no outputs are specified, the original outputs will be used for the
 replacement transaction, otherwise a new transaction will be created with the
 replacement transaction, otherwise a new transaction will be created with the
@@ -149,6 +155,8 @@ seedfiles = pop_seedfiles(cfg, ignore_dfl_wallet=not cfg.send, empty_ok=not cfg.
 if cfg.autosign:
 if cfg.autosign:
 	if cfg.send:
 	if cfg.send:
 		die(1, '--send cannot be used together with --autosign')
 		die(1, '--send cannot be used together with --autosign')
+	from .tx.online import CreatedTXRange
+	tx_range = CreatedTXRange(cfg._args.pop(0) if cfg._args and is_int(cfg._args[0]) else '0')
 else:
 else:
 	tx_file = cfg._args.pop()
 	tx_file = cfg._args.pop()
 	from .fileutil import check_infile
 	from .fileutil import check_infile
@@ -172,7 +180,7 @@ async def main():
 				'Only sent transactions can be bumped with --autosign.  Instead of bumping\n'
 				'Only sent transactions can be bumped with --autosign.  Instead of bumping\n'
 				f'your {state} transaction, abort it with ‘mmgen-txsend --abort’ and create\n'
 				f'your {state} transaction, abort it with ‘mmgen-txsend --abort’ and create\n'
 				'a new one.')
 				'a new one.')
-		orig_tx = await si.get_last_created()
+		orig_tx = (await si.get_last_created(tx_range=tx_range))[0]
 		sign_and_send = False
 		sign_and_send = False
 	else:
 	else:
 		orig_tx = await CompletedTX(cfg=cfg, filename=tx_file)
 		orig_tx = await CompletedTX(cfg=cfg, filename=tx_file)

+ 3 - 0
mmgen/tx/online.py

@@ -24,6 +24,9 @@ class SentTXRange(MMGenRange):
 	min_idx = 0
 	min_idx = 0
 	max_idx = 1_000_000
 	max_idx = 1_000_000
 
 
+class CreatedTXRange(SentTXRange):
+	pass
+
 class OnlineSigned(Signed):
 class OnlineSigned(Signed):
 
 
 	@property
 	@property

+ 0 - 1
mmgen/xmrwallet/ops/create.py

@@ -21,7 +21,6 @@ from .wallet import OpWallet
 
 
 class OpCreate(OpWallet):
 class OpCreate(OpWallet):
 	stem    = 'creat'
 	stem    = 'creat'
-	wallet_exists = False
 	opts    = ('restore_height',)
 	opts    = ('restore_height',)
 
 
 	def check_uopts(self):
 	def check_uopts(self):

+ 31 - 20
mmgen/xmrwallet/ops/wallet.py

@@ -15,8 +15,9 @@ xmrwallet.ops.wallet: xmrwallet wallet op for the MMGen Suite
 import asyncio, re, atexit
 import asyncio, re, atexit
 from pathlib import Path
 from pathlib import Path
 
 
-from ...color import orange
+from ...color import orange, cyan
 from ...util import msg, gmsg, ymsg, die, suf
 from ...util import msg, gmsg, ymsg, die, suf
+from ...addr import MMGenID
 from ...addrlist import KeyAddrList, ViewKeyAddrList, AddrIdxList
 from ...addrlist import KeyAddrList, ViewKeyAddrList, AddrIdxList
 from ...proto.xmr.rpc import MoneroRPCClient, MoneroWalletRPCClient
 from ...proto.xmr.rpc import MoneroRPCClient, MoneroWalletRPCClient
 from ...proto.xmr.daemon import MoneroWalletDaemon
 from ...proto.xmr.daemon import MoneroWalletDaemon
@@ -34,32 +35,31 @@ class OpWallet(OpBase):
 		'autosign',
 		'autosign',
 		'watch_only')
 		'watch_only')
 	wallet_offline = False
 	wallet_offline = False
-	wallet_exists = True
 	start_daemon = True
 	start_daemon = True
 	skip_wallet_check = False # for debugging
 	skip_wallet_check = False # for debugging
 
 
 	def __init__(self, cfg, uarg_tuple):
 	def __init__(self, cfg, uarg_tuple):
 
 
-		def wallet_exists(fn):
-			try:
-				fn.stat()
-			except:
-				return False
-			else:
-				return True
-
 		def check_wallets():
 		def check_wallets():
+			count = 0
 			for d in self.addr_data:
 			for d in self.addr_data:
 				fn = self.get_wallet_fn(d)
 				fn = self.get_wallet_fn(d)
-				match wallet_exists(fn):
-					case True if not self.wallet_exists:
-						die(1, f'Wallet ‘{fn}’ already exists!')
-					case False if self.wallet_exists:
+				match self.stat_wallet(fn):
+					case True if self.is_create:
+						msg('Skipping wallet create for {} - {} exists'.format(
+							MMGenID(cfg._proto, id_str=f'{self.kal.al_id}:{d.idx}').hl(),
+							cyan(f'‘{fn.name}’')))
+						continue
+					case False if not self.is_create:
 						die(1, f'Wallet ‘{fn}’ not found!')
 						die(1, f'Wallet ‘{fn}’ not found!')
+				count += 1
+			return count
 
 
 		super().__init__(cfg, uarg_tuple)
 		super().__init__(cfg, uarg_tuple)
 
 
-		if self.cfg.offline or (self.name == 'create' and self.cfg.restore_height is None):
+		self.is_create = self.name in ('create', 'create_offline', 'restore')
+
+		if self.cfg.offline or (self.is_create and self.cfg.restore_height is None):
 			self.wallet_offline = True
 			self.wallet_offline = True
 
 
 		self.wd = MoneroWalletDaemon(
 		self.wd = MoneroWalletDaemon(
@@ -117,11 +117,20 @@ class OpWallet(OpBase):
 		self.create_addr_data()
 		self.create_addr_data()
 
 
 		if not self.skip_wallet_check:
 		if not self.skip_wallet_check:
-			check_wallets()
+			self.to_process = check_wallets()
 
 
-		if self.start_daemon and not self.cfg.no_start_wallet_daemon:
+		if self.to_process and self.start_daemon and not self.cfg.no_start_wallet_daemon:
 			asyncio.run(self.restart_wallet_daemon())
 			asyncio.run(self.restart_wallet_daemon())
 
 
+	@staticmethod
+	def stat_wallet(fn):
+		try:
+			fn.stat()
+		except:
+			return False
+		else:
+			return True
+
 	@classmethod
 	@classmethod
 	def get_idx_from_fn(cls, fn):
 	def get_idx_from_fn(cls, fn):
 		return int(re.match(r'[0-9a-fA-F]{8}-(\d+)-Monero(WatchOnly)?Wallet.*', fn.name)[1])
 		return int(re.match(r'[0-9a-fA-F]{8}-(\d+)-Monero(WatchOnly)?Wallet.*', fn.name)[1])
@@ -204,15 +213,17 @@ class OpWallet(OpBase):
 		return 'offline signing ' if self.cfg.offline else 'watch-only ' if self.cfg.watch_only else ''
 		return 'offline signing ' if self.cfg.offline else 'watch-only ' if self.cfg.watch_only else ''
 
 
 	async def main(self):
 	async def main(self):
-		if not self.compat_call:
+		if self.to_process and not self.compat_call:
 			gmsg('\n{a}ing {b} {c}wallet{d}'.format(
 			gmsg('\n{a}ing {b} {c}wallet{d}'.format(
 				a = self.stem.capitalize(),
 				a = self.stem.capitalize(),
-				b = len(self.addr_data),
+				b = self.to_process,
 				c = self.add_wallet_desc,
 				c = self.add_wallet_desc,
-				d = suf(self.addr_data)))
+				d = suf(self.to_process)))
 		data = []
 		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)
+			if self.is_create and self.stat_wallet(fn):
+				continue
 			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,

+ 20 - 5
test/cmdtest_d/automount.py

@@ -70,14 +70,14 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
 		('alice_txsend_abort5',              'aborting the transaction again (error)'),
 		('alice_txsend_abort5',              'aborting the transaction again (error)'),
 		('generate',                         'mining a block'),
 		('generate',                         'mining a block'),
 		('alice_txcreate4',                  'creating a transaction'),
 		('alice_txcreate4',                  'creating a transaction'),
-		('alice_txbump1',                    'bumping the unsigned transaction (error)'),
+		('alice_txbump1',                    'bumping the unsigned transaction (error, idx=0)'),
 		('alice_txbump2',                    'bumping the unsent transaction (error)'),
 		('alice_txbump2',                    'bumping the unsent transaction (error)'),
 		('alice_txsend2_dump_hex',           'dumping the transaction to hex'),
 		('alice_txsend2_dump_hex',           'dumping the transaction to hex'),
 		('alice_txsend2_cli',                'sending the transaction via cli'),
 		('alice_txsend2_cli',                'sending the transaction via cli'),
 		('alice_txsend2_mark_sent',          'marking the transaction sent'),
 		('alice_txsend2_mark_sent',          'marking the transaction sent'),
 		('alice_txbump3',                    'bumping the transaction'),
 		('alice_txbump3',                    'bumping the transaction'),
 		('alice_txsend3',                    'sending the bumped transaction'),
 		('alice_txsend3',                    'sending the bumped transaction'),
-		('alice_txbump4',                    'bumping the transaction (new outputs, fee too low)'),
+		('alice_txbump4',                    'bumping the transaction (new outputs, fee too low, idx=0)'),
 		('alice_txbump_abort1',              'aborting the transaction'),
 		('alice_txbump_abort1',              'aborting the transaction'),
 		('alice_txbump5',                    'bumping the transaction (new outputs)'),
 		('alice_txbump5',                    'bumping the transaction (new outputs)'),
 		('alice_txsend5',                    'sending the bumped transaction'),
 		('alice_txsend5',                    'sending the bumped transaction'),
@@ -86,6 +86,7 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
 		('alice_txstatus7',                  'getting transaction status (tx_range=1, replaced)'),
 		('alice_txstatus7',                  'getting transaction status (tx_range=1, replaced)'),
 		('alice_txstatus8',                  'getting transaction status (tx_range=3, 2 confirmations)'),
 		('alice_txstatus8',                  'getting transaction status (tx_range=3, 2 confirmations)'),
 		('alice_txstatus9',                  'getting transaction status (tx_range=0-3)'),
 		('alice_txstatus9',                  'getting transaction status (tx_range=0-3)'),
+		('alice_txbump6',                    'bumping the next-to-last sent transaction (idx=1)'),
 		('generate',                         'mining a block'),
 		('generate',                         'mining a block'),
 		('alice_bal2',                       'checking Alice’s balance'),
 		('alice_bal2',                       'checking Alice’s balance'),
 		('wait_loop_kill',                   'stopping autosign wait loop'),
 		('wait_loop_kill',                   'stopping autosign wait loop'),
@@ -293,7 +294,14 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
 		self.remove_device_online()
 		self.remove_device_online()
 		return t
 		return t
 
 
-	def _alice_txbump(self, fee_opt=None, output_args=[], bad_tx_expect=None, low_fee_fix=None):
+	def _alice_txbump(
+			self,
+			fee_opt        = None,
+			output_args    = [],
+			bad_tx_expect  = None,
+			low_fee_fix    = None,
+			orig_tx_expect = None,
+			idx            = None):
 		if not self.proto.cap('rbf'):
 		if not self.proto.cap('rbf'):
 			return 'skip'
 			return 'skip'
 		self.insert_device_online()
 		self.insert_device_online()
@@ -301,6 +309,7 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
 				'mmgen-txbump',
 				'mmgen-txbump',
 				['--alice', '--autosign']
 				['--alice', '--autosign']
 				+ ([fee_opt] if fee_opt else [])
 				+ ([fee_opt] if fee_opt else [])
+				+ ([] if idx is None else [str(idx)])
 				+ output_args,
 				+ output_args,
 				exit_val = 1 if bad_tx_expect else None)
 				exit_val = 1 if bad_tx_expect else None)
 		if bad_tx_expect:
 		if bad_tx_expect:
@@ -308,6 +317,8 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
 			t.expect('Only sent transactions')
 			t.expect('Only sent transactions')
 			t.expect(bad_tx_expect)
 			t.expect(bad_tx_expect)
 		else:
 		else:
+			if orig_tx_expect:
+				t.expect(orig_tx_expect)
 			if not output_args:
 			if not output_args:
 				t.expect(r'to deduct the fee from .* change output\): ', '\n', regex=True)
 				t.expect(r'to deduct the fee from .* change output\): ', '\n', regex=True)
 				t.expect(r'(Y/n): ', 'y')  # output OK?
 				t.expect(r'(Y/n): ', 'y')  # output OK?
@@ -325,7 +336,7 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
 		return t
 		return t
 
 
 	def alice_txbump1(self):
 	def alice_txbump1(self):
-		return self._alice_txbump(bad_tx_expect='unsigned transaction')
+		return self._alice_txbump(bad_tx_expect='unsigned transaction', idx=0)
 
 
 	def alice_txbump2(self):
 	def alice_txbump2(self):
 		self._wait_signed('transaction')
 		self._wait_signed('transaction')
@@ -339,7 +350,8 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
 		return self._alice_txbump(
 		return self._alice_txbump(
 			fee_opt = '--fee=3s',
 			fee_opt = '--fee=3s',
 			output_args = [f'{self.burn_addr},7.654321', f'{sid}:C:1'],
 			output_args = [f'{self.burn_addr},7.654321', f'{sid}:C:1'],
-			low_fee_fix = '300s')
+			low_fee_fix = '300s',
+			idx = 0)
 
 
 	def alice_txbump_abort1(self):
 	def alice_txbump_abort1(self):
 		if not self.proto.cap('rbf'):
 		if not self.proto.cap('rbf'):
@@ -352,5 +364,8 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
 			fee_opt = '--fee=400s',
 			fee_opt = '--fee=400s',
 			output_args = ['data:message for posterity', f'{self.burn_addr},7.654321', f'{sid}:C:1'])
 			output_args = ['data:message for posterity', f'{self.burn_addr},7.654321', f'{sid}:C:1'])
 
 
+	def alice_txbump6(self):
+		return self._alice_txbump(idx=1, fee_opt='--fee=250s', orig_tx_expect='1.23456')
+
 	def alice_bal2(self):
 	def alice_bal2(self):
 		return self.user_bal('alice', self.bal2_chk[self.coin])
 		return self.user_bal('alice', self.bal2_chk[self.coin])

+ 18 - 3
test/cmdtest_d/xmr_autosign.py

@@ -520,7 +520,8 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 	cmd_group = (
 	cmd_group = (
 		('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 Alice’s watch-only wallets'),
+		('addrimport_alice',         'creating (importing) Alice’s watch-only wallets from key-address file'),
+		('addrimport_alice2',        'reimporting Alice’s watch-only wallets from key-address file'),
 		('gen_kafile_miner',         'generating key-address file for Miner'),
 		('gen_kafile_miner',         'generating key-address file for Miner'),
 		('create_wallet_miner',      'creating Monero wallet for Miner'),
 		('create_wallet_miner',      'creating Monero wallet for Miner'),
 		('mine_initial_coins',       'mining initial coins'),
 		('mine_initial_coins',       'mining initial coins'),
@@ -601,8 +602,22 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 			'--monero-wallet-rpc-password=passwOrd']
 			'--monero-wallet-rpc-password=passwOrd']
 		self.alice_opts = ['--alice', '--coin=xmr'] + self.alice_daemon_opts
 		self.alice_opts = ['--alice', '--coin=xmr'] + self.alice_daemon_opts
 
 
-	def create_watchonly_wallets(self):
-		return self._create_wallets()
+	def addrimport_alice(self):
+		return self._addrimport_alice(create_address_files=True)
+
+	def addrimport_alice2(self):
+		return self._addrimport_alice(expect_str='Skipping.*Skipping')
+
+	def _addrimport_alice(self, *, expect_str=None, create_address_files=False):
+		self.insert_device_online()
+		t = self.spawn('mmgen-addrimport', self.alice_opts + self.autosign_opts)
+		if expect_str:
+			t.expect(expect_str, regex=True)
+		if create_address_files:
+			self._create_address_files(t, 'alice')
+		t.read() # required!
+		self.remove_device_online()
+		return t
 
 
 	async def mine_blocks_1(self):
 	async def mine_blocks_1(self):
 		return await self._mine_blocks(1)
 		return await self._mine_blocks(1)

+ 11 - 6
test/cmdtest_d/xmrwallet.py

@@ -293,6 +293,16 @@ class CmdTestXMRWallet(CmdTestBase):
 	def create_wallets_alice(self):
 	def create_wallets_alice(self):
 		return self.create_wallets('alice')
 		return self.create_wallets('alice')
 
 
+	def _create_address_files(self, t, user, wallet=None):
+		data = self.users[user]
+		for i in MMGenRange(wallet or data.kal_range).items:
+			write_data_to_file(
+				self.cfg,
+				data.addrfile_fs.format(i),
+				t.expect_getend('Address: '),
+				ask_overwrite = False,
+				quiet = True)
+
 	def create_wallets(self, user, wallet=None, add_opts=[], op='create'):
 	def create_wallets(self, user, wallet=None, add_opts=[], op='create'):
 		assert wallet is None or is_int(wallet), 'wallet arg'
 		assert wallet is None or is_int(wallet), 'wallet arg'
 		data = self.users[user]
 		data = self.users[user]
@@ -311,12 +321,7 @@ class CmdTestXMRWallet(CmdTestBase):
 			+ [op]
 			+ [op]
 			+ ([] if data.autosign else [data.kafile])
 			+ ([] if data.autosign else [data.kafile])
 			+ [wallet or data.kal_range])
 			+ [wallet or data.kal_range])
-		for i in MMGenRange(wallet or data.kal_range).items:
-			write_data_to_file(
-				self.cfg,
-				self.users[user].addrfile_fs.format(i),
-				t.expect_getend('Address: '),
-				quiet = True)
+		self._create_address_files(t, user, wallet)
 		return t
 		return t
 
 
 	def new_addr_alice(self, spec, cfg, expect, kafile=None, do_autosign=False):
 	def new_addr_alice(self, spec, cfg, expect, kafile=None, do_autosign=False):