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)
 			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(
 				# compat fallback - ‘sent_timestamp’ attr is missing in some old TX files:
 				sort_key = lambda x: x.sent_timestamp or x.timestamp,
 				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
 			fns = [f for f in self.dir.iterdir() if f.name.endswith(self.subext)]
 			files = sorted(
@@ -348,9 +348,7 @@ class Signable:
 					for txfile in fns],
 				key = sort_key)
 			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:
 				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):
 		match self.proto.base_proto:
 			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':
 				return '[ADDR,AMT]'
 			case _:

+ 52 - 6
mmgen/main_addrimport.py

@@ -33,7 +33,11 @@ opts_data = {
 		'options': """
 -h, --help         Print this help message
 --, --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
 -l, --addrlist     Address source is a flat list of non-MMGen coin addresses
 -k, --keyaddr-file Address source is a key-address file
@@ -42,24 +46,46 @@ opts_data = {
                    blockchain for unspent outputs that include the imported
                    address(es).  Required if any of the imported addresses
                    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': """
+                                   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
 blocks containing the relevant UTXOs for much faster performance than the
 previous implementation.  The rescan operation typically takes around two
 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
 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
 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
 
+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():
-	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(
 		cfg        = cfg,
 		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
 
 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
 
 opts_data = {
@@ -32,11 +32,13 @@ opts_data = {
 	'text': {
 		'desc': """
                 Create, and optionally send and sign, a replacement transaction
-                on supporting networks
+                on supported networks
 		 """,
 		'usage2':   (
 			f'[opts] [{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': """
 			-- -h, --help             Print this help message
@@ -94,8 +96,12 @@ opts_data = {
 """,
 	'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
 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.send:
 		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:
 	tx_file = cfg._args.pop()
 	from .fileutil import check_infile
@@ -172,7 +180,7 @@ async def main():
 				'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'
 				'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
 	else:
 		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
 	max_idx = 1_000_000
 
+class CreatedTXRange(SentTXRange):
+	pass
+
 class OnlineSigned(Signed):
 
 	@property

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

@@ -21,7 +21,6 @@ from .wallet import OpWallet
 
 class OpCreate(OpWallet):
 	stem    = 'creat'
-	wallet_exists = False
 	opts    = ('restore_height',)
 
 	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
 from pathlib import Path
 
-from ...color import orange
+from ...color import orange, cyan
 from ...util import msg, gmsg, ymsg, die, suf
+from ...addr import MMGenID
 from ...addrlist import KeyAddrList, ViewKeyAddrList, AddrIdxList
 from ...proto.xmr.rpc import MoneroRPCClient, MoneroWalletRPCClient
 from ...proto.xmr.daemon import MoneroWalletDaemon
@@ -34,32 +35,31 @@ class OpWallet(OpBase):
 		'autosign',
 		'watch_only')
 	wallet_offline = False
-	wallet_exists = True
 	start_daemon = True
 	skip_wallet_check = False # for debugging
 
 	def __init__(self, cfg, uarg_tuple):
 
-		def wallet_exists(fn):
-			try:
-				fn.stat()
-			except:
-				return False
-			else:
-				return True
-
 		def check_wallets():
+			count = 0
 			for d in self.addr_data:
 				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!')
+				count += 1
+			return count
 
 		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.wd = MoneroWalletDaemon(
@@ -117,11 +117,20 @@ class OpWallet(OpBase):
 		self.create_addr_data()
 
 		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())
 
+	@staticmethod
+	def stat_wallet(fn):
+		try:
+			fn.stat()
+		except:
+			return False
+		else:
+			return True
+
 	@classmethod
 	def get_idx_from_fn(cls, fn):
 		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 ''
 
 	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(
 				a = self.stem.capitalize(),
-				b = len(self.addr_data),
+				b = self.to_process,
 				c = self.add_wallet_desc,
-				d = suf(self.addr_data)))
+				d = suf(self.to_process)))
 		data = []
 		for n, d in enumerate(self.addr_data): # [d.sec,d.addr,d.wallet_passwd,d.viewkey]
 			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(
 				a = self.stem.capitalize(),
 				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)'),
 		('generate',                         'mining a block'),
 		('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_txsend2_dump_hex',           'dumping the transaction to hex'),
 		('alice_txsend2_cli',                'sending the transaction via cli'),
 		('alice_txsend2_mark_sent',          'marking the transaction sent'),
 		('alice_txbump3',                    'bumping the 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_txbump5',                    'bumping the transaction (new outputs)'),
 		('alice_txsend5',                    'sending the bumped transaction'),
@@ -86,6 +86,7 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
 		('alice_txstatus7',                  'getting transaction status (tx_range=1, replaced)'),
 		('alice_txstatus8',                  'getting transaction status (tx_range=3, 2 confirmations)'),
 		('alice_txstatus9',                  'getting transaction status (tx_range=0-3)'),
+		('alice_txbump6',                    'bumping the next-to-last sent transaction (idx=1)'),
 		('generate',                         'mining a block'),
 		('alice_bal2',                       'checking Alice’s balance'),
 		('wait_loop_kill',                   'stopping autosign wait loop'),
@@ -293,7 +294,14 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
 		self.remove_device_online()
 		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'):
 			return 'skip'
 		self.insert_device_online()
@@ -301,6 +309,7 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
 				'mmgen-txbump',
 				['--alice', '--autosign']
 				+ ([fee_opt] if fee_opt else [])
+				+ ([] if idx is None else [str(idx)])
 				+ output_args,
 				exit_val = 1 if bad_tx_expect else None)
 		if bad_tx_expect:
@@ -308,6 +317,8 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
 			t.expect('Only sent transactions')
 			t.expect(bad_tx_expect)
 		else:
+			if orig_tx_expect:
+				t.expect(orig_tx_expect)
 			if not output_args:
 				t.expect(r'to deduct the fee from .* change output\): ', '\n', regex=True)
 				t.expect(r'(Y/n): ', 'y')  # output OK?
@@ -325,7 +336,7 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
 		return t
 
 	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):
 		self._wait_signed('transaction')
@@ -339,7 +350,8 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
 		return self._alice_txbump(
 			fee_opt = '--fee=3s',
 			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):
 		if not self.proto.cap('rbf'):
@@ -352,5 +364,8 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
 			fee_opt = '--fee=400s',
 			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):
 		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 = (
 		('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'),
+		('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'),
 		('create_wallet_miner',      'creating Monero wallet for Miner'),
 		('mine_initial_coins',       'mining initial coins'),
@@ -601,8 +602,22 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 			'--monero-wallet-rpc-password=passwOrd']
 		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):
 		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):
 		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'):
 		assert wallet is None or is_int(wallet), 'wallet arg'
 		data = self.users[user]
@@ -311,12 +321,7 @@ class CmdTestXMRWallet(CmdTestBase):
 			+ [op]
 			+ ([] if data.autosign else [data.kafile])
 			+ [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
 
 	def new_addr_alice(self, spec, cfg, expect, kafile=None, do_autosign=False):