mmgen-txsend --status: support transaction ranges

Example (assumes --autosign):

    # Display status of last four sent transactions:
    $ mmgen-txsend -s 0-3

Testing/demo:

    $ test/cmdtest.py -e -X alice_txstatus9 autosign_automount
This commit is contained in:
The MMGen Project 2026-02-01 09:11:08 +00:00
commit 48edcf412c
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
6 changed files with 71 additions and 35 deletions

View file

@ -334,22 +334,25 @@ 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, *, idx=0): async def get_last_sent(self, *, tx_range=None):
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,
idx = idx) tx_range = tx_range)
async def get_last_created(self, *, sort_key=lambda x: x.timestamp, idx=0): async def get_last_created(self, *, sort_key=lambda x: x.timestamp, tx_range=None):
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(
[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 fns], for txfile in fns],
key = sort_key) key = sort_key)
if not (0 <= idx < len(files)): if files:
die(2, f'{idx}: invalid transaction index (must be less than {len(files)})') return (
return files[-1 - idx] files[-1] if tx_range is None else
files[len(files) - 1 - tx_range.last:len(files) - tx_range.first])
else:
die(1, 'No sent automount transactions!')
class xmr_signable: # mixin class class xmr_signable: # mixin class
automount = True automount = True

View file

@ -1 +1 @@
January 2026 February 2026

View file

@ -1 +1 @@
16.1.dev28 16.1.dev29

View file

@ -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, is_int from .util import msg, async_run, die
opts_data = { opts_data = {
'sets': [ 'sets': [
@ -35,7 +35,7 @@ opts_data = {
'usage2': [ 'usage2': [
'[opts] <signed transaction file>', '[opts] <signed transaction file>',
'[opts] --autosign', '[opts] --autosign',
'[opts] --autosign (--status | --receipt) [IDX]', '[opts] --autosign (--status | --receipt) [index or range]',
], ],
'options': """ 'options': """
-h, --help Print this help message -h, --help Print this help message
@ -71,10 +71,13 @@ opts_data = {
-y, --yes Answer 'yes' to prompts, suppress non-essential output -y, --yes Answer 'yes' to prompts, suppress non-essential output
""", """,
'notes': """ 'notes': """
With --autosign, combined with --status or --receipt, the optional IDX arg With --autosign, combined with --status or --receipt, the optional index or
represents an index into the list of sent transaction files on the removable range arg represents an index or range into the list of sent transaction files
device, in reverse chronological order. 0 (the default) specifies the on the removable device, in reverse chronological order. 0 (the default)
last sent transaction, 1 the next-to-last, and so on. specifies the last sent transaction, 1 the next-to-last, and so on. Hyphen-
separated ranges are also supported. For example, specifying a range 0-3
would output data for the last four sent transactions, beginning with the most
recent.
""" """
}, },
'code': { 'code': {
@ -99,12 +102,12 @@ if cfg.dump_hex and cfg.dump_hex != '-':
check_outfile_dir(cfg.dump_hex) check_outfile_dir(cfg.dump_hex)
post_send_op = cfg.status or cfg.receipt post_send_op = cfg.status or cfg.receipt
asi, tx_range = (None, None)
asi = None
def init_autosign(arg): def init_autosign(arg):
global asi, si, infile, tx_idx global asi, si, infile, tx_range
from .tx.util import mount_removable_device from .tx.util import mount_removable_device
from .tx.online import SentTXRange
from .autosign import Signable from .autosign import Signable
asi = mount_removable_device(cfg) asi = mount_removable_device(cfg)
si = Signable.automount_transaction(asi) si = Signable.automount_transaction(asi)
@ -113,9 +116,11 @@ def init_autosign(arg):
elif post_send_op and (si.unsent or si.unsigned): elif post_send_op and (si.unsent or si.unsigned):
die(1, 'Transaction is {}'.format('unsent' if si.unsent else 'unsigned')) die(1, 'Transaction is {}'.format('unsent' if si.unsent else 'unsigned'))
elif post_send_op: elif post_send_op:
if not is_int(arg): try:
die(2, f'{arg}: invalid transaction index (must be a non-negative integer)') tx_range = SentTXRange(arg)
tx_idx = int(arg) except:
die(2, f'{arg}: invalid transaction index arg '
'(must be a non-negative integer or hyphen-separated range)')
else: else:
infile = si.get_unsent() infile = si.get_unsent()
cfg._util.qmsg(f'Got signed transaction file ‘{infile}') cfg._util.qmsg(f'Got signed transaction file ‘{infile}')
@ -124,7 +129,7 @@ match cfg._args:
case [arg] if cfg.autosign and post_send_op: case [arg] if cfg.autosign and post_send_op:
init_autosign(arg) init_autosign(arg)
case [] if cfg.autosign: case [] if cfg.autosign:
init_autosign(0) init_autosign('0')
case [infile]: case [infile]:
from .fileutil import check_infile from .fileutil import check_infile
check_infile(infile) check_infile(infile)
@ -137,8 +142,15 @@ if not cfg.status:
from .tx import OnlineSignedTX from .tx import OnlineSignedTX
batch = tx_range and (tx_range.last != tx_range.first)
def do_sep():
if batch:
msg('-' * 74)
async def process_tx(tx): async def process_tx(tx):
do_sep()
cfg._util.vmsg(f'Getting {tx.desc}{tx.infile}') cfg._util.vmsg(f'Getting {tx.desc}{tx.infile}')
if tx.is_compat: if tx.is_compat:
@ -168,11 +180,14 @@ async def process_tx(tx):
if not cfg.autosign: if not cfg.autosign:
tx.file.write(ask_write_default_yes=True) tx.file.write(ask_write_default_yes=True)
return await tx.send(txcfg, asi) return await tx.send(txcfg, asi, batch=batch)
async def main(): async def main():
if cfg.autosign and post_send_op: if cfg.autosign and post_send_op:
return await process_tx(await si.get_last_sent(idx=tx_idx)) exitvals = [await process_tx(tx)
for tx in reversed(await si.get_last_sent(tx_range=tx_range))]
do_sep()
return max(exitvals)
else: else:
return await process_tx(await OnlineSignedTX( return await process_tx(await OnlineSignedTX(
cfg = cfg, cfg = cfg,

View file

@ -14,11 +14,16 @@ tx.online: online signed transaction class
import time, asyncio import time, asyncio
from ..obj import MMGenRange
from ..util import msg, Msg, gmsg, ymsg, make_timestr, die from ..util import msg, Msg, gmsg, ymsg, make_timestr, die
from ..color import pink, yellow from ..color import pink, yellow
from .signed import Signed, AutomountSigned from .signed import Signed, AutomountSigned
class SentTXRange(MMGenRange):
min_idx = 0
max_idx = 1_000_000
class OnlineSigned(Signed): class OnlineSigned(Signed):
@property @property
@ -62,7 +67,7 @@ class OnlineSigned(Signed):
ask_overwrite = False, ask_overwrite = False,
ask_write = False) ask_write = False)
async def send(self, cfg, asi): async def send(self, cfg, asi, batch=False):
""" """
returns integer exit val to system returns integer exit val to system
""" """
@ -149,7 +154,10 @@ class OnlineSigned(Signed):
if status_exitval is not None: if status_exitval is not None:
if cfg.verbose: if cfg.verbose:
self.info.view_with_prompt('View transaction details?', pause=False) if batch:
self.info.view(pause=False, terse=True)
else:
self.info.view_with_prompt('View transaction details?', pause=False)
return status_exitval return status_exitval
return 0 return 0

View file

@ -82,9 +82,10 @@ 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_txstatus6', 'getting transaction status (tx_range=0, in mempool)'),
('alice_txstatus7', 'getting transaction status (idx=1, replaced)'), ('alice_txstatus7', 'getting transaction status (tx_range=1, replaced)'),
('alice_txstatus8', 'getting transaction status (idx=3, 2 confirmations)'), ('alice_txstatus8', 'getting transaction status (tx_range=3, 2 confirmations)'),
('alice_txstatus9', 'getting transaction status (tx_range=0-3)'),
('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'),
@ -229,8 +230,9 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
expect, expect,
exit_val = None, exit_val = None,
need_rbf = False, need_rbf = False,
idx = None, tx_range = None,
verbose = True): verbose = True,
batch = False):
if need_rbf and not self.proto.cap('rbf'): if need_rbf and not self.proto.cap('rbf'):
return 'skip' return 'skip'
@ -240,11 +242,11 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
'mmgen-txsend', 'mmgen-txsend',
['--alice', '--autosign', '--status'] ['--alice', '--autosign', '--status']
+ (['--verbose'] if verbose else []) + (['--verbose'] if verbose else [])
+ ([] if idx is None else [str(idx)]), + ([] if tx_range is None else [tx_range]),
no_passthru_opts = ['coin'], no_passthru_opts = ['coin'],
exit_val = exit_val) exit_val = exit_val)
t.expect(expect, regex=True) t.expect(expect, regex=True)
if not exit_val: if not (exit_val or batch):
t.expect('view: ', 'n') t.expect('view: ', 'n')
t.read() t.read()
self.remove_device_online() self.remove_device_online()
@ -267,13 +269,21 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
return self._alice_txstatus('in mempool', need_rbf=True) return self._alice_txstatus('in mempool', need_rbf=True)
def alice_txstatus6(self): def alice_txstatus6(self):
return self._alice_txstatus('in mempool', need_rbf=True, idx=0) return self._alice_txstatus('in mempool', need_rbf=True, tx_range='0')
def alice_txstatus7(self): def alice_txstatus7(self):
return self._alice_txstatus('replaced', need_rbf=True, idx=1) return self._alice_txstatus('replaced', need_rbf=True, tx_range='1')
def alice_txstatus8(self): def alice_txstatus8(self):
return self._alice_txstatus('2 confirmations', need_rbf=True, idx=3) return self._alice_txstatus('2 confirmations', need_rbf=True, tx_range='3')
def alice_txstatus9(self):
return self._alice_txstatus(
'in mempool.*replaced.*replaced.*2 confirmations',
need_rbf = True,
tx_range = '0-3',
verbose = False,
batch = True)
def alice_txsend_bad_no_unsent(self): def alice_txsend_bad_no_unsent(self):
self.insert_device_online() self.insert_device_online()