mmgen-txbump: support transaction selection

Transaction selection logic is the same as with `mmgen-txsend --status`, except
ranges are not supported.

Examples (assumes --autosign):

    # Bump last sent transaction:
    $ mmgen-txbump

    # Same as above:
    $ mmgen-txbump 0

    # Bump next-to-last sent transaction:
    $ mmgen-txbump 1

Testing/demo:

    $ test/cmdtest.py -e -X alice_txbump6 autosign_automount
This commit is contained in:
The MMGen Project 2026-02-06 10:14:25 +00:00
commit c4ec627153
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
4 changed files with 38 additions and 16 deletions

View file

@ -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!')

View file

@ -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 = {
@ -37,8 +37,8 @@ opts_data = {
'usage2': (
f'[opts] [{gc.proj_name} TX file] [seed source] ...',
f'[opts] {{u_args}} [{gc.proj_name} TX file] [seed source] ...',
'--autosign [opts]',
'--autosign [opts] {u_args}',
'--autosign [opts] [index]',
'--autosign [opts] [index] {u_args}',
),
'options': """
-- -h, --help Print this help message
@ -96,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
@ -151,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
@ -174,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)

View file

@ -24,6 +24,9 @@ class SentTXRange(MMGenRange):
min_idx = 0
max_idx = 1_000_000
class CreatedTXRange(SentTXRange):
pass
class OnlineSigned(Signed):
@property

View file

@ -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])