4 Commits e69dfbe8f3 ... 4f1b16f489

Author SHA1 Message Date
  The MMGen Project 4f1b16f489 mmgen-txsend: support transaction selection with `--status` 2 months ago
  The MMGen Project 7aadef4de3 mmgen-txsend: cleanups; Signable: new `get_last_sent()` method 2 months ago
  The MMGen Project 712fb43721 tw.view: support HOME and END keys for tmux 2 months ago
  The MMGen Project 870b24cd1d tw.view: whitespace, minor cleanups 2 months ago

+ 14 - 5
mmgen/autosign.py

@@ -334,13 +334,22 @@ 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_created(self):
+		async def get_last_sent(self, *, idx=0):
+			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,
+				idx = idx)
+
+		async def get_last_created(self, *, sort_key=lambda x: x.timestamp, idx=0):
 			from .tx import CompletedTX
 			from .tx import CompletedTX
-			files = [f for f in self.dir.iterdir() if f.name.endswith(self.subext)]
-			return sorted(
+			fns = [f for f in self.dir.iterdir() if f.name.endswith(self.subext)]
+			files = sorted(
 				[await CompletedTX(cfg=self.cfg, filename=str(txfile), quiet_open=True)
 				[await CompletedTX(cfg=self.cfg, filename=str(txfile), quiet_open=True)
-					for txfile in files],
-				key = lambda x: x.timestamp)[-1]
+					for txfile in fns],
+				key = sort_key)
+			if not (0 <= idx < len(files)):
+				die(2, f'{idx}: invalid transaction index (must be less than {len(files)})')
+			return files[-1 - idx]
 
 
 	class xmr_signable: # mixin class
 	class xmr_signable: # mixin class
 		automount = True
 		automount = True

+ 1 - 1
mmgen/data/version

@@ -1 +1 @@
-16.1.dev27
+16.1.dev28

+ 41 - 20
mmgen/main_txsend.py

@@ -23,7 +23,7 @@ mmgen-txsend: Broadcast a transaction signed by 'mmgen-txsign' to the network
 import sys
 import sys
 
 
 from .cfg import gc, Config
 from .cfg import gc, Config
-from .util import async_run, die
+from .util import async_run, die, is_int
 
 
 opts_data = {
 opts_data = {
 	'sets': [
 	'sets': [
@@ -32,7 +32,11 @@ opts_data = {
 	],
 	],
 	'text': {
 	'text': {
 		'desc':    f'Send a signed {gc.proj_name} cryptocoin transaction',
 		'desc':    f'Send a signed {gc.proj_name} cryptocoin transaction',
-		'usage':   '[opts] [signed transaction file]',
+		'usage2': [
+			'[opts] <signed transaction file>',
+			'[opts] --autosign',
+			'[opts] --autosign (--status | --receipt) [IDX]',
+		],
 		'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
@@ -65,6 +69,12 @@ opts_data = {
                  Use special value ‘env’ to honor *_PROXY environment vars
                  Use special value ‘env’ to honor *_PROXY environment vars
                  instead.
                  instead.
 -y, --yes        Answer 'yes' to prompts, suppress non-essential output
 -y, --yes        Answer 'yes' to prompts, suppress non-essential output
+""",
+		'notes': """
+With --autosign, combined with --status or --receipt, the optional IDX arg
+represents an index into the list of sent transaction files on the removable
+device, in reverse chronological order.  ‘0’ (the default) specifies the
+last sent transaction, ‘1’ the next-to-last, and so on.
 """
 """
 	},
 	},
 	'code': {
 	'code': {
@@ -88,27 +98,38 @@ if cfg.dump_hex and cfg.dump_hex != '-':
 	from .fileutil import check_outfile_dir
 	from .fileutil import check_outfile_dir
 	check_outfile_dir(cfg.dump_hex)
 	check_outfile_dir(cfg.dump_hex)
 
 
+post_send_op = cfg.status or cfg.receipt
+
 asi = None
 asi = None
 
 
+def init_autosign(arg):
+	global asi, si, infile, tx_idx
+	from .tx.util import mount_removable_device
+	from .autosign import Signable
+	asi = mount_removable_device(cfg)
+	si = Signable.automount_transaction(asi)
+	if cfg.abort:
+		si.shred_abortable() # prompts user, then raises exception or exits
+	elif post_send_op:
+		if si.unsent:
+			die(1, 'Transaction is unsent')
+		if si.unsigned:
+			die(1, 'Transaction is unsigned')
+		if not is_int(arg):
+			die(2, f'{arg}: invalid transaction index (must be a non-negative integer)')
+		tx_idx = int(arg)
+	else:
+		infile = si.get_unsent()
+		cfg._util.qmsg(f'Got signed transaction file ‘{infile}’')
+
 match cfg._args:
 match cfg._args:
+	case [arg] if cfg.autosign and post_send_op:
+		init_autosign(arg)
+	case [] if cfg.autosign:
+		init_autosign(0)
 	case [infile]:
 	case [infile]:
 		from .fileutil import check_infile
 		from .fileutil import check_infile
 		check_infile(infile)
 		check_infile(infile)
-	case [] if cfg.autosign:
-		from .tx.util import mount_removable_device
-		from .autosign import Signable
-		asi = mount_removable_device(cfg)
-		si = Signable.automount_transaction(asi)
-		if cfg.abort:
-			si.shred_abortable() # prompts user, then raises exception or exits
-		elif cfg.status or cfg.receipt:
-			if si.unsent:
-				die(1, 'Transaction is unsent')
-			if si.unsigned:
-				die(1, 'Transaction is unsigned')
-		else:
-			infile = si.get_unsent()
-			cfg._util.qmsg(f'Got signed transaction file ‘{infile}’')
 	case _:
 	case _:
 		cfg._usage()
 		cfg._usage()
 
 
@@ -122,8 +143,8 @@ async def main():
 
 
 	global cfg
 	global cfg
 
 
-	if (cfg.status or cfg.receipt) and cfg.autosign:
-		tx = await si.get_last_created()
+	if cfg.autosign and post_send_op:
+		tx = await si.get_last_sent(idx=tx_idx)
 	else:
 	else:
 		tx = await OnlineSignedTX(
 		tx = await OnlineSignedTX(
 			cfg        = cfg,
 			cfg        = cfg,
@@ -149,7 +170,7 @@ async def main():
 		await tx.post_send(asi)
 		await tx.post_send(asi)
 		sys.exit(0)
 		sys.exit(0)
 
 
-	if not (cfg.status or cfg.receipt):
+	if not post_send_op:
 		if tx.is_swap and not tx.check_swap_expiry():
 		if tx.is_swap and not tx.check_swap_expiry():
 			die(1, 'Swap quote has expired. Please re-create the transaction')
 			die(1, 'Swap quote has expired. Please re-create the transaction')
 
 

+ 4 - 0
mmgen/proto/xmr/tx/completed.py

@@ -36,3 +36,7 @@ class Completed(Base):
 	@cached_property
 	@cached_property
 	def timestamp(self):
 	def timestamp(self):
 		return make_timestamp(self.compat_tx.data.create_time)
 		return make_timestamp(self.compat_tx.data.create_time)
+
+	@cached_property
+	def sent_timestamp(self):
+		return make_timestamp(self.compat_tx.data.submit_time)

+ 1 - 1
mmgen/proto/xmr/tx/online.py

@@ -27,7 +27,7 @@ class OnlineSigned(Completed):
 				msg(self.compat_tx.get_info())
 				msg(self.compat_tx.get_info())
 			elif not self.cfg.quiet:
 			elif not self.cfg.quiet:
 				from ....obj import CoinTxID
 				from ....obj import CoinTxID
-				msg('TxID: {}'.format(CoinTxID(txid).hl()))
+				msg('{} TxID: {}'.format(self.cfg.coin, CoinTxID(txid).hl()))
 			res = op.dc.call_raw('get_transactions', txs_hashes=[txid])
 			res = op.dc.call_raw('get_transactions', txs_hashes=[txid])
 			if res['status'] == 'OK':
 			if res['status'] == 'OK':
 				tx = res['txs'][0]
 				tx = res['txs'][0]

+ 13 - 12
mmgen/tw/view.py

@@ -175,11 +175,13 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 			'f': 'm_pg_down',
 			'f': 'm_pg_down',
 			'g': 'm_top',
 			'g': 'm_top',
 			'G': 'm_bot'},
 			'G': 'm_bot'},
-		'linux': {
-			'\x1b[A': 'm_cursor_up',
-			'\x1b[B': 'm_cursor_down',
-			'\x1b[5~': 'm_pg_up',
-			'\x1b[6~': 'm_pg_down',
+		'linux': {                      # Terminfo codes:
+			'\x1b[A': 'm_cursor_up',    # cuu
+			'\x1b[B': 'm_cursor_down',  # cud
+			'\x1b[5~': 'm_pg_up',       # kpp
+			'\x1b[6~': 'm_pg_down',     # knp
+			'\x1b[1~': 'm_top',         # khome
+			'\x1b[4~': 'm_bot',         # kend
 			'\x1b[7~': 'm_top',
 			'\x1b[7~': 'm_top',
 			'\x1b[8~': 'm_bot'},
 			'\x1b[8~': 'm_bot'},
 		'win32': {
 		'win32': {
@@ -584,8 +586,7 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 			if scroll:
 			if scroll:
 				for k in self.scroll_keys['vi']:
 				for k in self.scroll_keys['vi']:
 					assert k not in self.key_mappings, f'{k!r} is in key_mappings'
 					assert k not in self.key_mappings, f'{k!r} is in key_mappings'
-				self.key_mappings.update(self.scroll_keys['vi'])
-				self.key_mappings.update(self.scroll_keys[sys.platform])
+				self.key_mappings.update(self.scroll_keys['vi'] | self.scroll_keys[sys.platform])
 			return self.key_mappings
 			return self.key_mappings
 
 
 		def cleanup(add_nl=False):
 		def cleanup(add_nl=False):
@@ -773,12 +774,12 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 
 
 			while True:
 			while True:
 				# action_method return values:
 				# action_method return values:
-				#  True:   action successfully performed
-				#  False:  an error occurred
-				#  None:   action aborted by user or no action performed
-				#  'redo': user will be re-prompted for item number
+				#   True:    action successfully performed
+				#   False:   an error occurred
+				#   None:    action aborted by user or no action performed
+				#  'redo':   user will be re-prompted for item number
 				#  'redraw': action successfully performed, screen will be redrawn
 				#  'redraw': action successfully performed, screen will be redrawn
-				#  'erase': action successfully performed, screen will be erased
+				#  'erase':  action successfully performed, screen will be erased
 				if usr_ret := await parent.get_idx_from_user(action_method.__name__):
 				if usr_ret := await parent.get_idx_from_user(action_method.__name__):
 					ret = await action_method(parent, usr_ret.idx, usr_ret.acct_addr_idx)
 					ret = await action_method(parent, usr_ret.idx, usr_ret.acct_addr_idx)
 				else:
 				else:

+ 1 - 1
mmgen/tx/online.py

@@ -85,7 +85,7 @@ class OnlineSigned(Signed):
 			if coin_txid := getattr(self, f'coin_txid{idx}', None):
 			if coin_txid := getattr(self, f'coin_txid{idx}', None):
 				txhex = getattr(self, f'serialized{idx}')
 				txhex = getattr(self, f'serialized{idx}')
 				if cfg.status:
 				if cfg.status:
-					cfg._util.qmsg(f'{self.proto.coin} txid: {coin_txid.hl()}')
+					cfg._util.qmsg(f'{self.proto.coin} TxID: {coin_txid.hl()}')
 					if cfg.verbose:
 					if cfg.verbose:
 						await self.post_network_send(coin_txid)
 						await self.post_network_send(coin_txid)
 					status_exitval = await self.status.display(idx=idx)
 					status_exitval = await self.status.display(idx=idx)

+ 15 - 2
test/cmdtest_d/automount.py

@@ -82,6 +82,9 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
 		('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'),
 		('alice_txstatus5',                  'getting transaction status (in mempool)'),
 		('alice_txstatus5',                  'getting transaction status (in mempool)'),
+		('alice_txstatus6',                  'getting transaction status (idx=0, in mempool)'),
+		('alice_txstatus7',                  'getting transaction status (idx=1, replaced)'),
+		('alice_txstatus8',                  'getting transaction status (idx=3, 2 confirmations)'),
 		('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'),
@@ -221,7 +224,7 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
 	def alice_txsend5(self):
 	def alice_txsend5(self):
 		return self._user_txsend('alice', need_rbf=True)
 		return self._user_txsend('alice', need_rbf=True)
 
 
-	def _alice_txstatus(self, expect, exit_val=None, need_rbf=False):
+	def _alice_txstatus(self, expect, exit_val=None, need_rbf=False, idx=None):
 
 
 		if need_rbf and not self.proto.cap('rbf'):
 		if need_rbf and not self.proto.cap('rbf'):
 			return 'skip'
 			return 'skip'
@@ -229,7 +232,8 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
 		self.insert_device_online()
 		self.insert_device_online()
 		t = self.spawn(
 		t = self.spawn(
 				'mmgen-txsend',
 				'mmgen-txsend',
-				['--alice', '--autosign', '--status', '--verbose'],
+				['--alice', '--autosign', '--status', '--verbose']
+				+ ([] if idx is None else [str(idx)]),
 				no_passthru_opts = ['coin'],
 				no_passthru_opts = ['coin'],
 				exit_val = exit_val)
 				exit_val = exit_val)
 		t.expect(expect)
 		t.expect(expect)
@@ -255,6 +259,15 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
 	def alice_txstatus5(self):
 	def alice_txstatus5(self):
 		return self._alice_txstatus('in mempool', need_rbf=True)
 		return self._alice_txstatus('in mempool', need_rbf=True)
 
 
+	def alice_txstatus6(self):
+		return self._alice_txstatus('in mempool', need_rbf=True, idx=0)
+
+	def alice_txstatus7(self):
+		return self._alice_txstatus('replaced', need_rbf=True, idx=1)
+
+	def alice_txstatus8(self):
+		return self._alice_txstatus('2 confirmations', need_rbf=True, idx=3)
+
 	def alice_txsend_bad_no_unsent(self):
 	def alice_txsend_bad_no_unsent(self):
 		self.insert_device_online()
 		self.insert_device_online()
 		t = self.spawn('mmgen-txsend', ['--quiet', '--autosign'], exit_val=2, no_passthru_opts=['coin'])
 		t = self.spawn('mmgen-txsend', ['--quiet', '--autosign'], exit_val=2, no_passthru_opts=['coin'])