Browse Source

mmgen-txbump: support new outputs in the replacement TX

The former behavior permitted only increasing the transaction fee.  Now the
replacement TX can contain entirely new outputs.

From mmgen-txbump --help:

    If no outputs are specified, the original outputs will be used for the
    replacement transaction, otherwise a new transaction will be created with
    the outputs listed on the command line.  The syntax for the output
    arguments is identical to that of ‘mmgen-txcreate’.

Testing:

    $ test/cmdtest.py regtest_legacy.main autosign_automount
    $ test/cmdtest.py --coin=eth ethdev.main
The MMGen Project 3 weeks ago
parent
commit
ef5f6e4b22

+ 1 - 1
mmgen/data/version

@@ -1 +1 @@
-15.1.dev15
+15.1.dev16

+ 41 - 33
mmgen/main_txbump.py

@@ -33,7 +33,10 @@ opts_data = {
                 Create, and optionally send and sign, a replacement transaction
                 Create, and optionally send and sign, a replacement transaction
                 on networks that support replace-by-fee (RBF)
                 on networks that support replace-by-fee (RBF)
 		 """,
 		 """,
-		'usage':   f'[opts] [{gc.proj_name} TX file] [seed source] ...',
+		'usage2':   (
+			f'[opts] [{gc.proj_name} TX file] [seed source] ...',
+			f'[opts] {{u_args}} [{gc.proj_name} TX file] [seed source] ...',
+		),
 		'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
@@ -80,6 +83,18 @@ opts_data = {
 			-- -z, --show-hash-presets Show information on available hash presets
 			-- -z, --show-hash-presets Show information on available hash presets
 """,
 """,
 	'notes': """
 	'notes': """
+
+With --autosign, the TX file argument is omitted, and the last submitted TX
+file on the removable device will be used.
+
+If no outputs are specified, the original outputs will be used for the
+replacement transaction, otherwise a new transaction will be created with the
+outputs listed on the command line.  The syntax for the output arguments is
+identical to that of ‘mmgen-txcreate’.
+
+The user should take care to select a fee sufficient to ensure the original
+transaction is replaced in the mempool.
+
 {e}{s}
 {e}{s}
 Seed source files must have the canonical extensions listed in the 'FileExt'
 Seed source files must have the canonical extensions listed in the 'FileExt'
 column below:
 column below:
@@ -88,6 +103,8 @@ column below:
 """
 """
 	},
 	},
 	'code': {
 	'code': {
+		'usage': lambda cfg, proto, help_notes, s: s.format(
+			u_args = help_notes('txcreate_args', 'tx')),
 		'options': lambda cfg, help_notes, proto, s: s.format(
 		'options': lambda cfg, help_notes, proto, s: s.format(
 			cfg     = cfg,
 			cfg     = cfg,
 			gc      = gc,
 			gc      = gc,
@@ -108,15 +125,22 @@ column below:
 
 
 cfg = Config(opts_data=opts_data)
 cfg = Config(opts_data=opts_data)
 
 
-if not cfg.autosign:
-	tx_file = cfg._args.pop(0)
-	from .fileutil import check_infile
-	check_infile(tx_file)
-
 from .tx import CompletedTX, BumpTX, UnsignedTX, OnlineSignedTX
 from .tx import CompletedTX, BumpTX, UnsignedTX, OnlineSignedTX
 from .tx.sign import txsign, get_seed_files, get_keyaddrlist, get_keylist
 from .tx.sign import txsign, get_seed_files, get_keyaddrlist, get_keylist
 
 
-seed_files = get_seed_files(cfg, cfg._args) if (cfg._args or cfg.send) else None
+seed_files = get_seed_files(
+	cfg,
+	cfg._args,
+	ignore_dfl_wallet = not cfg.send,
+	empty_ok = not cfg.send)
+
+if cfg.autosign:
+	if cfg.send:
+		die(1, '--send cannot be used together with --autosign')
+else:
+	tx_file = cfg._args.pop()
+	from .fileutil import check_infile
+	check_infile(tx_file)
 
 
 from .ui import do_license_msg
 from .ui import do_license_msg
 do_license_msg(cfg)
 do_license_msg(cfg)
@@ -158,33 +182,17 @@ async def main():
 		check_sent = cfg.autosign or sign_and_send,
 		check_sent = cfg.autosign or sign_and_send,
 		twctl = await TwCtl(cfg, orig_tx.proto) if orig_tx.proto.tokensym else None)
 		twctl = await TwCtl(cfg, orig_tx.proto) if orig_tx.proto.tokensym else None)
 
 
-	from .rpc import rpc_init
-	tx.rpc = await rpc_init(cfg, tx.proto)
-
-	msg('Creating replacement transaction')
-
-	tx.check_sufficient_funds_for_bump()
+	tx.orig_rel_fee = tx.get_orig_rel_fee()
 
 
-	output_idx = tx.choose_output()
-
-	if not silent:
-		msg(f'Minimum fee for new transaction: {tx.min_fee.hl()} {tx.proto.coin}')
-
-	tx.usr_fee = tx.get_usr_fee_interactive(fee=cfg.fee, desc='User-selected')
-
-	tx.bump_fee(output_idx, tx.usr_fee)
-
-	assert tx.fee <= tx.proto.max_tx_fee
-
-	if not cfg.yes:
-		tx.add_comment()   # edits an existing comment
-
-	await tx.create_serialized(bump=True)
-
-	tx.add_timestamp()
-	tx.add_blockcount()
-
-	cfg._util.qmsg('Fee successfully increased')
+	if cfg._args:
+		tx.new_outputs = True
+		tx.is_swap = False
+		tx.outputs = tx.OutputList(tx)
+		tx.cfg = cfg # NB: with --automount, must use current cfg opts, not those from orig_tx
+		await tx.create(cfg._args, caller='txdo' if sign_and_send else 'txcreate')
+	else:
+		tx.new_outputs = False
+		await tx.create_feebump(silent=silent)
 
 
 	if not silent:
 	if not silent:
 		msg(green('\nREPLACEMENT TRANSACTION:'))
 		msg(green('\nREPLACEMENT TRANSACTION:'))

+ 4 - 1
mmgen/proto/btc/tx/bump.py

@@ -21,6 +21,9 @@ from .unsigned import AutomountUnsigned
 class Bump(Completed, New, TxBase.Bump):
 class Bump(Completed, New, TxBase.Bump):
 	desc = 'fee-bumped transaction'
 	desc = 'fee-bumped transaction'
 
 
+	def get_orig_rel_fee(self):
+		return self.fee_abs2rel(self.sum_inputs() - self.sum_outputs())
+
 	@property
 	@property
 	def min_fee(self):
 	def min_fee(self):
 		return self.sum_inputs() - self.sum_outputs() + self.relay_fee
 		return self.sum_inputs() - self.sum_outputs() + self.relay_fee
@@ -33,7 +36,7 @@ class Bump(Completed, New, TxBase.Bump):
 
 
 	def convert_and_check_fee(self, fee, desc):
 	def convert_and_check_fee(self, fee, desc):
 		ret = super().convert_and_check_fee(fee, desc)
 		ret = super().convert_and_check_fee(fee, desc)
-		if ret is False:
+		if ret is False or self.new_outputs:
 			return ret
 			return ret
 		if ret < self.min_fee:
 		if ret < self.min_fee:
 			msg('{} {c}: {} fee too small. Minimum fee: {} {c} ({} {})'.format(
 			msg('{} {c}: {} fee too small. Minimum fee: {} {c} ({} {})'.format(

+ 3 - 3
mmgen/proto/btc/tx/new.py

@@ -135,9 +135,9 @@ class New(Base, TxNew):
 		if len(self.nondata_outputs) > 1 and not self.chg_output.mmid:
 		if len(self.nondata_outputs) > 1 and not self.chg_output.mmid:
 			do_err()
 			do_err()
 
 
-	async def create_serialized(self, locktime=None, bump=None):
+	async def create_serialized(self, locktime=None):
 
 
-		if not bump:
+		if not self.is_bump:
 			# Set all sequence numbers to the same value, in conformity with the behavior of most modern wallets:
 			# Set all sequence numbers to the same value, in conformity with the behavior of most modern wallets:
 			do_rbf = self.proto.cap('rbf') and not self.cfg.no_rbf
 			do_rbf = self.proto.cap('rbf') and not self.cfg.no_rbf
 			seqnum_val = self.proto.max_int - (2 if do_rbf else 1 if locktime else 0)
 			seqnum_val = self.proto.max_int - (2 if do_rbf else 1 if locktime else 0)
@@ -158,7 +158,7 @@ class New(Base, TxNew):
 
 
 		ret = await self.rpc.call('createrawtransaction', inputs_list, outputs_dict)
 		ret = await self.rpc.call('createrawtransaction', inputs_list, outputs_dict)
 
 
-		if locktime and not bump:
+		if locktime and not self.is_bump:
 			msg(f'Setting nLockTime to {self.info.strfmt_locktime(locktime)}!')
 			msg(f'Setting nLockTime to {self.info.strfmt_locktime(locktime)}!')
 			assert isinstance(locktime, int), 'locktime value not an integer'
 			assert isinstance(locktime, int), 'locktime value not an integer'
 			self.locktime = locktime
 			self.locktime = locktime

+ 3 - 0
mmgen/proto/eth/tx/bump.py

@@ -21,6 +21,9 @@ from .new import New, TokenNew
 class Bump(Completed, New, TxBase.Bump):
 class Bump(Completed, New, TxBase.Bump):
 	desc = 'fee-bumped transaction'
 	desc = 'fee-bumped transaction'
 
 
+	def get_orig_rel_fee(self): # disable this check for ETH
+		return 0
+
 	@property
 	@property
 	def min_fee(self):
 	def min_fee(self):
 		return self.fee * Decimal('1.101')
 		return self.fee * Decimal('1.101')

+ 1 - 1
mmgen/proto/eth/tx/new.py

@@ -65,7 +65,7 @@ class New(Base, TxBase.New):
 	# Instead of serializing tx data as with BTC, just create a JSON dump.
 	# Instead of serializing tx data as with BTC, just create a JSON dump.
 	# This complicates things but means we avoid using the rlp library to deserialize the data,
 	# This complicates things but means we avoid using the rlp library to deserialize the data,
 	# thus removing an attack vector
 	# thus removing an attack vector
-	async def create_serialized(self, locktime=None, bump=None):
+	async def create_serialized(self, locktime=None):
 		assert len(self.inputs) == 1, 'Transaction has more than one input!'
 		assert len(self.inputs) == 1, 'Transaction has more than one input!'
 		o_num = len(self.outputs)
 		o_num = len(self.outputs)
 		o_ok = 0 if self.usr_contract_data else 1
 		o_ok = 0 if self.usr_contract_data else 1

+ 1 - 0
mmgen/tx/base.py

@@ -79,6 +79,7 @@ class Base(MMGenObject):
 	locktime     = None
 	locktime     = None
 	chain        = None
 	chain        = None
 	signed       = False
 	signed       = False
+	is_bump      = False
 	is_swap      = False
 	is_swap      = False
 	file_format  = 'json'
 	file_format  = 'json'
 	non_mmgen_inputs_msg = f"""
 	non_mmgen_inputs_msg = f"""

+ 44 - 1
mmgen/tx/bump.py

@@ -14,12 +14,13 @@ tx.bump: transaction bump class
 
 
 from .new import New
 from .new import New
 from .completed import Completed
 from .completed import Completed
-from ..util import msg, is_int, die
+from ..util import msg, ymsg, is_int, die
 
 
 class Bump(Completed, New):
 class Bump(Completed, New):
 	desc = 'fee-bumped transaction'
 	desc = 'fee-bumped transaction'
 	ext  = 'rawtx'
 	ext  = 'rawtx'
 	bump_output_idx = None
 	bump_output_idx = None
+	is_bump = True
 
 
 	def __init__(self, check_sent, *args, **kwargs):
 	def __init__(self, check_sent, *args, **kwargs):
 
 
@@ -35,6 +36,48 @@ class Bump(Completed, New):
 		self.coin_txid = ''
 		self.coin_txid = ''
 		self.sent_timestamp = None
 		self.sent_timestamp = None
 
 
+	async def get_inputs(self, outputs_sum):
+		return True
+
+	def check_bumped_fee_ok(self, abs_fee):
+		orig = int(self.orig_rel_fee)
+		new = int(self.fee_abs2rel(abs_fee))
+		if new <= orig:
+			ymsg('New fee ({b} {d}) <= original fee ({a} {d}). Please choose a higher fee'.format(
+				a=orig, b=new, d=self.rel_fee_disp))
+			return False
+		return True
+
+	async def create_feebump(self, silent):
+
+		from ..rpc import rpc_init
+		self.rpc = await rpc_init(self.cfg, self.proto)
+
+		msg('Creating replacement transaction')
+
+		self.check_sufficient_funds_for_bump()
+
+		output_idx = self.choose_output()
+
+		if not silent:
+			msg(f'Minimum fee for new transaction: {self.min_fee.hl()} {self.proto.coin}')
+
+		self.usr_fee = self.get_usr_fee_interactive(fee=self.cfg.fee, desc='User-selected')
+
+		self.bump_fee(output_idx, self.usr_fee)
+
+		assert self.fee <= self.proto.max_tx_fee
+
+		if not self.cfg.yes:
+			self.add_comment()   # edits an existing comment
+
+		await self.create_serialized()
+
+		self.add_timestamp()
+		self.add_blockcount()
+
+		self.cfg._util.qmsg('Fee successfully increased')
+
 	def check_sufficient_funds_for_bump(self):
 	def check_sufficient_funds_for_bump(self):
 		if not [o.amt for o in self.outputs if o.amt >= self.min_fee]:
 		if not [o.amt for o in self.outputs if o.amt >= self.min_fee]:
 			die(1,
 			die(1,

+ 35 - 25
mmgen/tx/new.py

@@ -137,21 +137,24 @@ class New(Base):
 			if fee:
 			if fee:
 				abs_fee = self.convert_and_check_fee(fee, desc)
 				abs_fee = self.convert_and_check_fee(fee, desc)
 			if abs_fee:
 			if abs_fee:
-				prompt = '{a} TX fee{b}: {c}{d} {e} ({f} {g})\n'.format(
-					a = desc,
-					b = (f' (after {self.cfg.fee_adjust:.2f}X adjustment)'
-							if self.cfg.fee_adjust != 1 and desc.startswith('Network-estimated')
-								else ''),
-					c = ('', '≈')[self.fee_is_approximate],
-					d = abs_fee.hl(),
-					e = self.coin,
-					f = pink(self.fee_abs2rel(abs_fee)),
-					g = self.rel_fee_disp)
-				from ..ui import keypress_confirm
-				if self.cfg.yes or keypress_confirm(self.cfg, prompt+'OK?', default_yes=True):
-					if self.cfg.yes:
-						msg(prompt)
-					return abs_fee
+				if self.is_bump and not self.check_bumped_fee_ok(abs_fee):
+					pass
+				else:
+					prompt = '{a} TX fee{b}: {c}{d} {e} ({f} {g})\n'.format(
+						a = desc,
+						b = (f' (after {self.cfg.fee_adjust:.2f}X adjustment)'
+								if self.cfg.fee_adjust != 1 and desc.startswith('Network-estimated')
+									else ''),
+						c = ('', '≈')[self.fee_is_approximate],
+						d = abs_fee.hl(),
+						e = self.coin,
+						f = pink(self.fee_abs2rel(abs_fee)),
+						g = self.rel_fee_disp)
+					from ..ui import keypress_confirm
+					if self.cfg.yes or keypress_confirm(self.cfg, prompt+'OK?', default_yes=True):
+						if self.cfg.yes:
+							msg(prompt)
+						return abs_fee
 			fee = line_input(self.cfg, self.usr_fee_prompt)
 			fee = line_input(self.cfg, self.usr_fee_prompt)
 			desc = 'User-selected'
 			desc = 'User-selected'
 
 
@@ -431,21 +434,22 @@ class New(Base):
 				self.get_addrdata_from_files(self.proto, addrfile_args),
 				self.get_addrdata_from_files(self.proto, addrfile_args),
 				await TwAddrData(self.cfg, self.proto, twctl=self.twctl))
 				await TwAddrData(self.cfg, self.proto, twctl=self.twctl))
 
 
-		self.twuo = await TwUnspentOutputs(
-			self.cfg,
-			self.proto,
-			minconf = self.cfg.minconf,
-			addrs = await self.get_input_addrs_from_inputs_opt())
-
-		await self.twuo.get_data()
+		if not self.is_bump:
+			self.twuo = await TwUnspentOutputs(
+				self.cfg,
+				self.proto,
+				minconf = self.cfg.minconf,
+				addrs = await self.get_input_addrs_from_inputs_opt())
+			await self.twuo.get_data()
 
 
 		from ..ui import do_license_msg
 		from ..ui import do_license_msg
 		do_license_msg(self.cfg)
 		do_license_msg(self.cfg)
 
 
-		if not self.cfg.inputs:
+		if not (self.is_bump or self.cfg.inputs):
 			await self.twuo.view_filter_and_sort()
 			await self.twuo.view_filter_and_sort()
 
 
-		self.twuo.display_total()
+		if not self.is_bump:
+			self.twuo.display_total()
 
 
 		if do_info:
 		if do_info:
 			del self.twuo.twctl
 			del self.twuo.twctl
@@ -461,7 +465,10 @@ class New(Base):
 		while True:
 		while True:
 			if not await self.get_inputs(outputs_sum):
 			if not await self.get_inputs(outputs_sum):
 				continue
 				continue
-			if funds_left := await self.get_fee(self.cfg.fee, outputs_sum):
+			fee_hint = None
+			if self.is_swap:
+				fee_hint = self.update_vault_output(self.vault_output.amt or self.sum_inputs())
+			if funds_left := await self.get_fee(fee_hint or self.cfg.fee, outputs_sum):
 				break
 				break
 
 
 		self.check_non_mmgen_inputs(caller)
 		self.check_non_mmgen_inputs(caller)
@@ -482,6 +489,9 @@ class New(Base):
 
 
 		self.cfg._util.qmsg('Transaction successfully created')
 		self.cfg._util.qmsg('Transaction successfully created')
 
 
+		if self.is_bump:
+			return
+
 		from . import UnsignedTX
 		from . import UnsignedTX
 		new = UnsignedTX(cfg=self.cfg, data=self.__dict__, automount=self.cfg.autosign)
 		new = UnsignedTX(cfg=self.cfg, data=self.__dict__, automount=self.cfg.autosign)
 
 

+ 5 - 5
mmgen/tx/sign.py

@@ -128,15 +128,15 @@ def get_tx_files(cfg, args):
 		die(1, 'You must specify a raw transaction file!')
 		die(1, 'You must specify a raw transaction file!')
 	return ret
 	return ret
 
 
-def get_seed_files(cfg, args):
+def get_seed_files(cfg, args, ignore_dfl_wallet=False, empty_ok=False):
 	# favor unencrypted seed sources first, as they don't require passwords
 	# favor unencrypted seed sources first, as they don't require passwords
 	ret = _pop_matching_fns(args, get_wallet_extensions('unenc'))
 	ret = _pop_matching_fns(args, get_wallet_extensions('unenc'))
 	from ..filename import find_file_in_dir
 	from ..filename import find_file_in_dir
-	wf = find_file_in_dir(get_wallet_cls('mmgen'), cfg.data_dir) # Make this the first encrypted ss in the list
-	if wf:
-		ret.append(wf)
+	if not ignore_dfl_wallet: # Make this the first encrypted ss in the list
+		if wf := find_file_in_dir(get_wallet_cls('mmgen'), cfg.data_dir):
+			ret.append(wf)
 	ret += _pop_matching_fns(args, get_wallet_extensions('enc'))
 	ret += _pop_matching_fns(args, get_wallet_extensions('enc'))
-	if not (ret or cfg.mmgen_keys_from_file or cfg.keys_from_file): # or cfg.use_wallet_dat
+	if not (ret or empty_ok or cfg.mmgen_keys_from_file or cfg.keys_from_file): # or cfg.use_wallet_dat
 		die(1, 'You must specify a seed or key source!')
 		die(1, 'You must specify a seed or key source!')
 	return ret
 	return ret
 
 

+ 54 - 6
test/cmdtest_d/ct_automount.py

@@ -61,6 +61,13 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtestBDBWallet)
 		('alice_txsend2',                    'sending the transaction'),
 		('alice_txsend2',                    'sending the transaction'),
 		('alice_txbump3',                    'bumping the transaction'),
 		('alice_txbump3',                    'bumping the transaction'),
 		('alice_txsend3',                    'sending the bumped transaction'),
 		('alice_txsend3',                    'sending the bumped transaction'),
+		('alice_txbump4',                    'bumping the transaction (new outputs, fee too low)'),
+		('alice_txbump_abort1',              'aborting the transaction'),
+		('alice_txbump5',                    'bumping the transaction (new outputs)'),
+		('alice_txsend5',                    'sending the bumped transaction'),
+		('alice_txstatus5',                  'getting transaction status (in mempool)'),
+		('generate',                         'mining a block'),
+		('alice_bal2',                       'checking Alice’s balance'),
 		('wait_loop_kill',                   'stopping autosign wait loop'),
 		('wait_loop_kill',                   'stopping autosign wait loop'),
 		('stop',                             'stopping regtest daemon'),
 		('stop',                             'stopping regtest daemon'),
 		('txview',                           'viewing transactions'),
 		('txview',                           'viewing transactions'),
@@ -176,6 +183,9 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtestBDBWallet)
 	def alice_txsend3(self):
 	def alice_txsend3(self):
 		return self._alice_txsend(need_rbf=True)
 		return self._alice_txsend(need_rbf=True)
 
 
+	def alice_txsend5(self):
+		return self._alice_txsend(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):
 
 
 		if need_rbf and not self.proto.cap('rbf'):
 		if need_rbf and not self.proto.cap('rbf'):
@@ -204,6 +214,9 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtestBDBWallet)
 	def alice_txstatus4(self):
 	def alice_txstatus4(self):
 		return self._alice_txstatus('1 confirmation', 0)
 		return self._alice_txstatus('1 confirmation', 0)
 
 
+	def alice_txstatus5(self):
+		return self._alice_txstatus('in mempool', need_rbf=True)
+
 	def _alice_txsend(self, comment=None, no_wait=False, need_rbf=False):
 	def _alice_txsend(self, comment=None, no_wait=False, need_rbf=False):
 
 
 		if need_rbf and not self.proto.cap('rbf'):
 		if need_rbf and not self.proto.cap('rbf'):
@@ -211,6 +224,7 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtestBDBWallet)
 
 
 		if not no_wait:
 		if not no_wait:
 			self._wait_signed('transaction')
 			self._wait_signed('transaction')
+
 		self.insert_device_online()
 		self.insert_device_online()
 		t = self.spawn('mmgen-txsend', ['--alice', '--quiet', '--autosign'])
 		t = self.spawn('mmgen-txsend', ['--alice', '--quiet', '--autosign'])
 		t.view_tx('t')
 		t.view_tx('t')
@@ -229,22 +243,30 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtestBDBWallet)
 		self.remove_device_online()
 		self.remove_device_online()
 		return t
 		return t
 
 
-	def _alice_txbump(self, bad_tx_expect=None):
-		if cfg.coin == 'BCH':
+	def _alice_txbump(self, fee_opt=None, output_args=[], bad_tx_expect=None, low_fee_fix=None):
+		if not self.proto.cap('rbf'):
 			return 'skip'
 			return 'skip'
 		self.insert_device_online()
 		self.insert_device_online()
 		t = self.spawn(
 		t = self.spawn(
 				'mmgen-txbump',
 				'mmgen-txbump',
-				['--alice', '--autosign'],
+				['--alice', '--autosign']
+				+ ([fee_opt] if fee_opt else [])
+				+ output_args,
 				exit_val = 1 if bad_tx_expect else None)
 				exit_val = 1 if bad_tx_expect else None)
 		if bad_tx_expect:
 		if bad_tx_expect:
 			time.sleep(0.5)
 			time.sleep(0.5)
 			t.expect('Only sent transactions')
 			t.expect('Only sent transactions')
 			t.expect(bad_tx_expect)
 			t.expect(bad_tx_expect)
 		else:
 		else:
-			t.expect(r'to deduct the fee from .* change output\): ', '\n', regex=True)
-			t.expect(r'(Y/n): ', 'y')  # output OK?
-			t.expect('transaction fee: ', '200s\n')
+			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?
+			if low_fee_fix or not fee_opt:
+				if low_fee_fix:
+					t.expect('Please choose a higher fee')
+				t.expect('transaction fee: ', (low_fee_fix or '200s') + '\n')
+			if output_args:
+				t.expect(r'(Y/n): ', 'y')
 			t.expect(r'(Y/n): ', 'y')  # fee OK?
 			t.expect(r'(Y/n): ', 'y')  # fee OK?
 			t.expect(r'(y/N): ', '\n') # add comment?
 			t.expect(r'(y/N): ', '\n') # add comment?
 			t.expect(r'(y/N): ', 'y')  # save?
 			t.expect(r'(y/N): ', 'y')  # save?
@@ -261,3 +283,29 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtestBDBWallet)
 
 
 	def alice_txbump3(self):
 	def alice_txbump3(self):
 		return self._alice_txbump()
 		return self._alice_txbump()
+
+	def alice_txbump4(self):
+		sid = self._user_sid('alice')
+		return self._alice_txbump(
+			fee_opt = '--fee=3s',
+			output_args = [f'{self.burn_addr},7.654321', f'{sid}:C:1'],
+			low_fee_fix = '300s')
+
+	def alice_txbump_abort1(self):
+		if not self.proto.cap('rbf'):
+			return 'skip'
+		return self._alice_txsend_abort(shred_expect=['Shredding .*arawtx'])
+
+	def alice_txbump5(self):
+		sid = self._user_sid('alice')
+		return self._alice_txbump(
+			fee_opt = '--fee=400s',
+			output_args = ['data:message for posterity', f'{self.burn_addr},7.654321', f'{sid}:C:1'])
+
+	def alice_bal2(self):
+		bals = {
+			'btc': '491.11002204',
+			'ltc': '5491.11002204',
+			'bch': '498.7653392',
+		}
+		return self.user_bal('alice', bals.get(self.coin, None))

+ 21 - 8
test/cmdtest_d/ct_regtest.py

@@ -71,9 +71,9 @@ rt_data = {
 	'tx_fee': {'btc':'0.0001', 'bch':'0.001', 'ltc':'0.01'},
 	'tx_fee': {'btc':'0.0001', 'bch':'0.001', 'ltc':'0.01'},
 	'rtFundAmt': {'btc':'500', 'bch':'500', 'ltc':'5500'},
 	'rtFundAmt': {'btc':'500', 'bch':'500', 'ltc':'5500'},
 	'rtFee': {
 	'rtFee': {
-		'btc': ('20s', '10s', '60s', '31s', '10s', '20s'),
-		'bch': ('20s', '10s', '60s', '0.0001', '10s', '20s'),
-		'ltc': ('1000s', '500s', '1500s', '0.05', '400s', '1000s')
+		'btc': ('20s', '10s', '60s', '31s', '10s', '20s', '40s'),
+		'bch': ('20s', '10s', '60s', '0.0001', '10s', '20s', '40s'),
+		'ltc': ('1000s', '500s', '1500s', '0.05', '400s', '1000s', '1200s')
 	},
 	},
 	'rtBals': {
 	'rtBals': {
 		'btc': ('499.9999488', '399.9998282', '399.9998147', '399.9996877',
 		'btc': ('499.9999488', '399.9998282', '399.9998147', '399.9996877',
@@ -291,8 +291,10 @@ class CmdTestRegtest(CmdTestBase, CmdTestShared):
 		('bob_send_maybe_rbf',         'sending funds to Alice (RBF, if supported)'),
 		('bob_send_maybe_rbf',         'sending funds to Alice (RBF, if supported)'),
 		('get_mempool1',               'mempool (before RBF bump)'),
 		('get_mempool1',               'mempool (before RBF bump)'),
 		('bob_rbf_status1',            'getting status of transaction'),
 		('bob_rbf_status1',            'getting status of transaction'),
-		('bob_rbf_bump',               'bumping RBF transaction'),
+		('bob_rbf_bump_newoutputs',    'bumping RBF transaction (new outputs)'),
 		('get_mempool2',               'mempool (after RBF bump)'),
 		('get_mempool2',               'mempool (after RBF bump)'),
+		('bob_rbf_bump',               'bumping RBF transaction'),
+		('get_mempool3',               'mempool (after RBF bump)'),
 		('bob_rbf_status2',            'getting status of transaction after replacement'),
 		('bob_rbf_status2',            'getting status of transaction after replacement'),
 		('bob_rbf_status3',            'getting status of replacement transaction (mempool)'),
 		('bob_rbf_status3',            'getting status of replacement transaction (mempool)'),
 		('generate',                   'mining a block'),
 		('generate',                   'mining a block'),
@@ -1173,10 +1175,18 @@ class CmdTestRegtest(CmdTestBase, CmdTestShared):
 			t.written_to_file('Fee-bumped transaction')
 			t.written_to_file('Fee-bumped transaction')
 		return t
 		return t
 
 
+	def bob_rbf_bump_newoutputs(self):
+		return self._bob_rbf_bump(
+			['--send', 'data:embedded forever', f'{self.burn_addr},0.1', f'{self._user_sid("bob")}:C:5'],
+			rtFee[6])
+
 	def bob_rbf_bump(self):
 	def bob_rbf_bump(self):
+		return self._bob_rbf_bump(['--send'], rtFee[2])
+
+	def _bob_rbf_bump(self, add_args, fee):
 		ext = ',{}]{x}.regtest.sigtx'.format(rtFee[1][:-1], x='-α' if cfg.debug_utf8 else '')
 		ext = ',{}]{x}.regtest.sigtx'.format(rtFee[1][:-1], x='-α' if cfg.debug_utf8 else '')
 		txfile = self.get_file_with_ext(ext, delete=False, no_dot=True)
 		txfile = self.get_file_with_ext(ext, delete=False, no_dot=True)
-		return self.user_txbump('bob', self.tmpdir, txfile, rtFee[2], add_args=['--send'])
+		return self.user_txbump('bob', self.tmpdir, txfile, fee, add_args=add_args)
 
 
 	def generate(self, num_blocks=1, add_opts=[]):
 	def generate(self, num_blocks=1, add_opts=[]):
 		int(num_blocks)
 		int(num_blocks)
@@ -1203,6 +1213,9 @@ class CmdTestRegtest(CmdTestBase, CmdTestShared):
 	def get_mempool2(self):
 	def get_mempool2(self):
 		return self._get_mempool_compare_txid('rbf_txid1', 'rbf_txid2')
 		return self._get_mempool_compare_txid('rbf_txid1', 'rbf_txid2')
 
 
+	def get_mempool3(self):
+		return self._get_mempool_compare_txid('rbf_txid2', 'rbf_txid3')
+
 	def _get_mempool(self, do_msg=False):
 	def _get_mempool(self, do_msg=False):
 		if do_msg:
 		if do_msg:
 			self.spawn('', msg_only=True)
 			self.spawn('', msg_only=True)
@@ -1243,19 +1256,19 @@ class CmdTestRegtest(CmdTestBase, CmdTestShared):
 		return self._bob_rbf_status(rtFee[1])
 		return self._bob_rbf_status(rtFee[1])
 
 
 	def bob_rbf_status2(self):
 	def bob_rbf_status2(self):
-		return self._bob_rbf_status(rtFee[1], txid='rbf_txid2')
+		return self._bob_rbf_status(rtFee[1], txid='rbf_txid3')
 
 
 	def bob_rbf_status3(self):
 	def bob_rbf_status3(self):
 		return self._bob_rbf_status(rtFee[2])
 		return self._bob_rbf_status(rtFee[2])
 
 
 	def bob_rbf_status4(self):
 	def bob_rbf_status4(self):
-		return self._bob_rbf_status(rtFee[1], txid='rbf_txid2', confirmations=1, exit_val=0)
+		return self._bob_rbf_status(rtFee[1], txid='rbf_txid3', confirmations=1, exit_val=0)
 
 
 	def bob_rbf_status5(self):
 	def bob_rbf_status5(self):
 		return self._bob_rbf_status(rtFee[2], confirmations=1, exit_val=0)
 		return self._bob_rbf_status(rtFee[2], confirmations=1, exit_val=0)
 
 
 	def bob_rbf_status6(self):
 	def bob_rbf_status6(self):
-		return self._bob_rbf_status(rtFee[1], txid='rbf_txid2', confirmations=2, exit_val=0)
+		return self._bob_rbf_status(rtFee[1], txid='rbf_txid3', confirmations=2, exit_val=0)
 
 
 	def _gen_pairs(self, n):
 	def _gen_pairs(self, n):
 		from mmgen.tool.api import tool_api
 		from mmgen.tool.api import tool_api