From 48edcf412c888eabb539aaab9fbbf8f7fa299234 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Sun, 1 Feb 2026 09:11:08 +0000 Subject: [PATCH] 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 --- mmgen/autosign.py | 15 ++++++++----- mmgen/data/release_date | 2 +- mmgen/data/version | 2 +- mmgen/main_txsend.py | 45 ++++++++++++++++++++++++------------- mmgen/tx/online.py | 12 ++++++++-- test/cmdtest_d/automount.py | 30 ++++++++++++++++--------- 6 files changed, 71 insertions(+), 35 deletions(-) diff --git a/mmgen/autosign.py b/mmgen/autosign.py index 3cf09d47..080e07bb 100755 --- a/mmgen/autosign.py +++ b/mmgen/autosign.py @@ -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 diff --git a/mmgen/data/release_date b/mmgen/data/release_date index 9de1a6b4..dfb1edf6 100644 --- a/mmgen/data/release_date +++ b/mmgen/data/release_date @@ -1 +1 @@ -January 2026 +February 2026 diff --git a/mmgen/data/version b/mmgen/data/version index 10ec5e99..bfaf773c 100644 --- a/mmgen/data/version +++ b/mmgen/data/version @@ -1 +1 @@ -16.1.dev28 +16.1.dev29 diff --git a/mmgen/main_txsend.py b/mmgen/main_txsend.py index 2f91f034..01faae34 100755 --- a/mmgen/main_txsend.py +++ b/mmgen/main_txsend.py @@ -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] ', '[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, diff --git a/mmgen/tx/online.py b/mmgen/tx/online.py index b3098fc3..7c1131af 100755 --- a/mmgen/tx/online.py +++ b/mmgen/tx/online.py @@ -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 diff --git a/test/cmdtest_d/automount.py b/test/cmdtest_d/automount.py index a73f33e2..7c22ae94 100755 --- a/test/cmdtest_d/automount.py +++ b/test/cmdtest_d/automount.py @@ -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()