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)
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(
# compat fallback - ‘sent_timestamp’ attr is missing in some old TX files:
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
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)
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]
if files:
return (
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
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
from .cfg import gc, Config
from .util import async_run, die, is_int
from .util import msg, async_run, die
opts_data = {
'sets': [
@ -35,7 +35,7 @@ opts_data = {
'usage2': [
'[opts] <signed transaction file>',
'[opts] --autosign',
'[opts] --autosign (--status | --receipt) [IDX]',
'[opts] --autosign (--status | --receipt) [index or range]',
],
'options': """
-h, --help Print this help message
@ -71,10 +71,13 @@ opts_data = {
-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.
With --autosign, combined with --status or --receipt, the optional index or
range arg represents an index or range 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. 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': {
@ -99,12 +102,12 @@ if cfg.dump_hex and cfg.dump_hex != '-':
check_outfile_dir(cfg.dump_hex)
post_send_op = cfg.status or cfg.receipt
asi = None
asi, tx_range = (None, None)
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.online import SentTXRange
from .autosign import Signable
asi = mount_removable_device(cfg)
si = Signable.automount_transaction(asi)
@ -113,9 +116,11 @@ def init_autosign(arg):
elif post_send_op and (si.unsent or si.unsigned):
die(1, 'Transaction is {}'.format('unsent' if si.unsent else 'unsigned'))
elif post_send_op:
if not is_int(arg):
die(2, f'{arg}: invalid transaction index (must be a non-negative integer)')
tx_idx = int(arg)
try:
tx_range = SentTXRange(arg)
except:
die(2, f'{arg}: invalid transaction index arg '
'(must be a non-negative integer or hyphen-separated range)')
else:
infile = si.get_unsent()
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:
init_autosign(arg)
case [] if cfg.autosign:
init_autosign(0)
init_autosign('0')
case [infile]:
from .fileutil import check_infile
check_infile(infile)
@ -137,8 +142,15 @@ if not cfg.status:
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):
do_sep()
cfg._util.vmsg(f'Getting {tx.desc} ‘{tx.infile}’')
if tx.is_compat:
@ -168,11 +180,14 @@ async def process_tx(tx):
if not cfg.autosign:
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():
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:
return await process_tx(await OnlineSignedTX(
cfg = cfg,

View file

@ -14,11 +14,16 @@ tx.online: online signed transaction class
import time, asyncio
from ..obj import MMGenRange
from ..util import msg, Msg, gmsg, ymsg, make_timestr, die
from ..color import pink, yellow
from .signed import Signed, AutomountSigned
class SentTXRange(MMGenRange):
min_idx = 0
max_idx = 1_000_000
class OnlineSigned(Signed):
@property
@ -62,7 +67,7 @@ class OnlineSigned(Signed):
ask_overwrite = False,
ask_write = False)
async def send(self, cfg, asi):
async def send(self, cfg, asi, batch=False):
"""
returns integer exit val to system
"""
@ -149,7 +154,10 @@ class OnlineSigned(Signed):
if status_exitval is not None:
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 0

View file

@ -82,9 +82,10 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
('alice_txbump5', 'bumping the transaction (new outputs)'),
('alice_txsend5', 'sending the bumped transaction'),
('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)'),
('alice_txstatus6', 'getting transaction status (tx_range=0, in mempool)'),
('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)'),
('generate', 'mining a block'),
('alice_bal2', 'checking Alice’s balance'),
('wait_loop_kill', 'stopping autosign wait loop'),
@ -229,8 +230,9 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
expect,
exit_val = None,
need_rbf = False,
idx = None,
verbose = True):
tx_range = None,
verbose = True,
batch = False):
if need_rbf and not self.proto.cap('rbf'):
return 'skip'
@ -240,11 +242,11 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
'mmgen-txsend',
['--alice', '--autosign', '--status']
+ (['--verbose'] if verbose else [])
+ ([] if idx is None else [str(idx)]),
+ ([] if tx_range is None else [tx_range]),
no_passthru_opts = ['coin'],
exit_val = exit_val)
t.expect(expect, regex=True)
if not exit_val:
if not (exit_val or batch):
t.expect('view: ', 'n')
t.read()
self.remove_device_online()
@ -267,13 +269,21 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
return self._alice_txstatus('in mempool', need_rbf=True)
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):
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):
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):
self.insert_device_online()