Skip to content

Commit 441d42e

Browse files
Merge #6725: backport: merge bitcoin#25228, bitcoin#24941, bitcoin#25356, bitcoin#25435, bitcoin#23017, bitcoin#25522, bitcoin#26414, partial bitcoin#24587, bitcoin#24637, bitcoin#24623, bitcoin#25445 (implement better tx capabilities in MiniWallet)
cfa906e merge bitcoin#26414: Move tx creation to create_self_transfer_multi (Kittywhiskers Van Gogh) e167162 merge bitcoin#25522: Remove -acceptnonstdtxn=1 from feature_rbf.py (Kittywhiskers Van Gogh) a5cb9b4 partial bitcoin#25445: Return new_utxo from create_self_transfer in MiniWallet (Kittywhiskers Van Gogh) b28d90f merge bitcoin#23017: Replace MiniWallet scan_blocks with rescan_utxos (Kittywhiskers Van Gogh) ebfb239 merge bitcoin#25435: Remove from_node from create_self_transfer* MiniWallet helpers (Kittywhiskers Van Gogh) 144228b merge bitcoin#25356: Remove MiniWallet mempool_valid option (Kittywhiskers Van Gogh) 0622788 merge bitcoin#24941: support skipping mempool checks (feature_fee_estimation.py performance fix) (Kittywhiskers Van Gogh) 9c93e3a merge bitcoin#25228: add BIP-125 rule 5 testcase with default mempool (Kittywhiskers Van Gogh) b84bcad partial bitcoin#24623: Add diamond-shape prioritisetransaction test (Kittywhiskers Van Gogh) 4621320 partial bitcoin#24637: use MiniWallet for mempool_package_onemore.py (Kittywhiskers Van Gogh) a21dfd2 partial bitcoin#24587: use MiniWallet for rpc_createmultisig.py (Kittywhiskers Van Gogh) Pull request description: ## Additional Information * Dependency for #6726 * Since [bitcoin#15891](bitcoin#15891) (8ca90f3), we enforce standard transactions on regtest and regrettably, the logic introduced in [bitcoin#24637](bitcoin#24637) (`*self_transfer*()`) had a tendency to create transactions that were non-standard (see below) <details> <summary>Test failure:</summary> ``` dash@b90780fda2d1:/src/dash$ ./test/functional/mempool_package_onemore.py 2025-06-18T17:50:19.492000Z TestFramework (INFO): PRNG seed is: 8344907287561424619 2025-06-18T17:50:19.492000Z TestFramework (INFO): Initializing test directory /tmp/dash_func_test_emt8bu6u 2025-06-18T17:50:19.765000Z TestFramework (ERROR): JSONRPC error Traceback (most recent call last): File "/src/dash/test/functional/test_framework/test_framework.py", line 162, in main self.run_test() File "/src/dash/./test/functional/mempool_package_onemore.py", line 41, in run_test utxo, utxo2 = self.chain_tx([utxo], num_outputs=2) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/src/dash/./test/functional/mempool_package_onemore.py", line 28, in chain_tx return self.wallet.send_self_transfer_multi( ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/src/dash/test/functional/test_framework/wallet.py", line 207, in send_self_transfer_multi txid = self.sendrawtransaction(from_node=kwargs['from_node'], tx_hex=tx.serialize().hex()) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/src/dash/test/functional/test_framework/wallet.py", line 269, in sendrawtransaction txid = from_node.sendrawtransaction(hexstring=tx_hex, maxfeerate=maxfeerate, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/src/dash/test/functional/test_framework/coverage.py", line 49, in __call__ return_val = self.auth_service_proxy_instance.__call__(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/src/dash/test/functional/test_framework/authproxy.py", line 143, in __call__ raise JSONRPCException(response['error'], status) test_framework.authproxy.JSONRPCException: bad-txns-nonstandard-inputs (-26) 2025-06-18T17:50:20.266000Z TestFramework (INFO): Stopping nodes ``` </details> Two options presented themselves, either temporary relaxing `fRequireStandard` on regtest or backporting all the changes needed to make `*self_transfer*()` create standard transactions and then backporting `MiniWallet` conversions in a separate pull request. The latter approach was taken. * The correctness of this pull request can be validated by observing the CI results of [dash#6726](#6726) * The backport for [bitcoin#23017](bitcoin#23017) does not include changes to `rpc_blockchain.py` as the relevant `scan_blocks()` call was removed in the partial backport of [bitcoin#23371](bitcoin#23371) (081897c). ## Breaking Changes None expected. Affects only functional tests. ## Checklist - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas **(note: N/A)** - [x] I have added or updated relevant unit/integration/functional/e2e tests - [x] I have made corresponding changes to the documentation **(note: N/A)** - [x] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)_ ACKs for top commit: PastaPastaPasta: utACK cfa906e UdjinM6: utACK cfa906e Tree-SHA512: 81107b6352cc673cd4d4358da0eba23f971546fae9df0f34bea5b584bfe1a3f820872444314ec65bf181158aeca1cc3880a0b6925101c953db19ecd45d28aa38
2 parents ba93745 + cfa906e commit 441d42e

File tree

7 files changed

+120
-66
lines changed

7 files changed

+120
-66
lines changed

test/functional/feature_csv_activation.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,13 +120,15 @@ def create_bip112special(self, input, txversion):
120120
tx.nVersion = txversion
121121
self.miniwallet.sign_tx(tx)
122122
tx.vin[0].scriptSig = CScript([-1, OP_CHECKSEQUENCEVERIFY, OP_DROP] + list(CScript(tx.vin[0].scriptSig)))
123+
tx.rehash()
123124
return tx
124125

125126
def create_bip112emptystack(self, input, txversion):
126127
tx = self.create_self_transfer_from_utxo(input)
127128
tx.nVersion = txversion
128129
self.miniwallet.sign_tx(tx)
129130
tx.vin[0].scriptSig = CScript([OP_CHECKSEQUENCEVERIFY] + list(CScript(tx.vin[0].scriptSig)))
131+
tx.rehash()
130132
return tx
131133

132134
def send_generic_input_tx(self, coinbases):
@@ -144,7 +146,6 @@ def create_bip68txs(self, bip68inputs, txversion, locktime_delta=0):
144146
tx.nVersion = txversion
145147
tx.vin[0].nSequence = locktime + locktime_delta
146148
self.miniwallet.sign_tx(tx)
147-
tx.rehash()
148149
txs.append({'tx': tx, 'sdf': sdf, 'stf': stf})
149150

150151
return txs
@@ -350,20 +351,16 @@ def run_test(self):
350351
# BIP 113 tests should now fail regardless of version number if nLockTime isn't satisfied by new rules
351352
bip113tx_v1.nLockTime = self.last_block_time - 600 * 5 # = MTP of prior block (not <) but < time put on current block
352353
self.miniwallet.sign_tx(bip113tx_v1)
353-
bip113tx_v1.rehash()
354354
bip113tx_v2.nLockTime = self.last_block_time - 600 * 5 # = MTP of prior block (not <) but < time put on current block
355355
self.miniwallet.sign_tx(bip113tx_v2)
356-
bip113tx_v2.rehash()
357356
for bip113tx in [bip113tx_v1, bip113tx_v2]:
358357
self.send_blocks([self.create_test_block([bip113tx])], success=False, reject_reason='bad-txns-nonfinal')
359358

360359
# BIP 113 tests should now pass if the locktime is < MTP
361360
bip113tx_v1.nLockTime = self.last_block_time - 600 * 5 - 1 # < MTP of prior block
362361
self.miniwallet.sign_tx(bip113tx_v1)
363-
bip113tx_v1.rehash()
364362
bip113tx_v2.nLockTime = self.last_block_time - 600 * 5 - 1 # < MTP of prior block
365363
self.miniwallet.sign_tx(bip113tx_v2)
366-
bip113tx_v2.rehash()
367364
for bip113tx in [bip113tx_v1, bip113tx_v2]:
368365
self.send_blocks([self.create_test_block([bip113tx])])
369366
self.nodes[0].invalidateblock(self.nodes[0].getbestblockhash())
@@ -488,7 +485,6 @@ def run_test(self):
488485
for tx in [tx['tx'] for tx in bip112txs_vary_OP_CSV_v2 if not tx['sdf'] and tx['stf']]:
489486
tx.vin[0].nSequence = BASE_RELATIVE_LOCKTIME | SEQ_TYPE_FLAG
490487
self.miniwallet.sign_tx(tx)
491-
tx.rehash()
492488
time_txs.append(tx)
493489

494490
self.send_blocks([self.create_test_block(time_txs)])

test/functional/mempool_limit.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def set_test_params(self):
3131

3232
def send_large_txs(self, node, miniwallet, txouts, fee, tx_batch_size):
3333
for _ in range(tx_batch_size):
34-
tx = miniwallet.create_self_transfer(from_node=node, fee_rate=0, mempool_valid=False)['tx']
34+
tx = miniwallet.create_self_transfer(fee_rate=0)['tx']
3535
for txout in txouts:
3636
tx.vout.append(txout)
3737
tx.vout[0].nValue -= int(fee * COIN)
@@ -85,7 +85,7 @@ def run_test(self):
8585

8686
# Deliberately try to create a tx with a fee less than the minimum mempool fee to assert that it does not get added to the mempool
8787
self.log.info('Create a mempool tx that will not pass mempoolminfee')
88-
assert_raises_rpc_error(-26, "mempool min fee not met", miniwallet.send_self_transfer, from_node=node, fee_rate=relayfee, mempool_valid=False)
88+
assert_raises_rpc_error(-26, "mempool min fee not met", miniwallet.send_self_transfer, from_node=node, fee_rate=relayfee)
8989

9090
self.log.info('Test passing a value below the minimum (5 MB) to -maxmempool throws an error')
9191
self.stop_node(0)

test/functional/mempool_reorg.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def run_test(self):
3131
self.log.info("Add 4 coinbase utxos to the miniwallet")
3232
# Block 76 contains the first spendable coinbase txs.
3333
first_block = 76
34-
wallet.scan_blocks(start=first_block, num=4)
34+
wallet.rescan_utxos()
3535

3636
# Three scenarios for re-orging coinbase spends in the memory pool:
3737
# 1. Direct coinbase spend : spend_1
@@ -53,8 +53,7 @@ def run_test(self):
5353
utxo = wallet.get_utxo(txid=coinbase_txids[0])
5454
timelock_tx = wallet.create_self_transfer(
5555
utxo_to_spend=utxo,
56-
mempool_valid=False,
57-
locktime=self.nodes[0].getblockcount() + 2
56+
locktime=self.nodes[0].getblockcount() + 2,
5857
)['hex']
5958

6059
self.log.info("Check that the time-locked transaction is too immature to spend")
@@ -69,10 +68,8 @@ def run_test(self):
6968
assert_raises_rpc_error(-26, 'non-final', self.nodes[0].sendrawtransaction, timelock_tx)
7069

7170
self.log.info("Create spend_2_1 and spend_3_1")
72-
spend_2_utxo = wallet.get_utxo(txid=spend_2['txid'])
73-
spend_2_1 = wallet.create_self_transfer(utxo_to_spend=spend_2_utxo)
74-
spend_3_utxo = wallet.get_utxo(txid=spend_3['txid'])
75-
spend_3_1 = wallet.create_self_transfer(utxo_to_spend=spend_3_utxo)
71+
spend_2_1 = wallet.create_self_transfer(utxo_to_spend=spend_2["new_utxo"])
72+
spend_3_1 = wallet.create_self_transfer(utxo_to_spend=spend_3["new_utxo"])
7673

7774
self.log.info("Broadcast and mine spend_3_1")
7875
spend_3_1_id = self.nodes[0].sendrawtransaction(spend_3_1['hex'])

test/functional/mempool_spend_coinbase.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,19 @@ def run_test(self):
2828
chain_height = 198
2929
self.nodes[0].invalidateblock(self.nodes[0].getblockhash(chain_height + 1))
3030
assert_equal(chain_height, self.nodes[0].getblockcount())
31+
wallet.rescan_utxos()
3132

3233
# Coinbase at height chain_height-100+1 ok in mempool, should
3334
# get mined. Coinbase at height chain_height-100+2 is
3435
# too immature to spend.
35-
wallet.scan_blocks(start=chain_height - 100 + 1, num=1)
36-
utxo_mature = wallet.get_utxo()
37-
wallet.scan_blocks(start=chain_height - 100 + 2, num=1)
38-
utxo_immature = wallet.get_utxo()
36+
coinbase_txid = lambda h: self.nodes[0].getblock(self.nodes[0].getblockhash(h))['tx'][0]
37+
utxo_mature = wallet.get_utxo(txid=coinbase_txid(chain_height - 100 + 1))
38+
utxo_immature = wallet.get_utxo(txid=coinbase_txid(chain_height - 100 + 2))
3939

4040
spend_mature_id = wallet.send_self_transfer(from_node=self.nodes[0], utxo_to_spend=utxo_mature)["txid"]
4141

4242
# other coinbase should be too immature to spend
43-
immature_tx = wallet.create_self_transfer(utxo_to_spend=utxo_immature, mempool_valid=False)
43+
immature_tx = wallet.create_self_transfer(utxo_to_spend=utxo_immature)
4444
assert_raises_rpc_error(-26,
4545
"bad-txns-premature-spend-of-coinbase",
4646
lambda: self.nodes[0].sendrawtransaction(immature_tx['hex']))

test/functional/mempool_unbroadcast.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def test_broadcast(self):
4040
wallet_tx_hsh = node.sendtoaddress(addr, 0.0001)
4141

4242
# generate a txn using sendrawtransaction
43-
txFS = self.wallet.create_self_transfer(from_node=node)
43+
txFS = self.wallet.create_self_transfer()
4444
rpc_tx_hsh = node.sendrawtransaction(txFS["hex"])
4545

4646
# check transactions are in unbroadcast using rpc

test/functional/test_framework/util.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -642,7 +642,7 @@ def mine_large_block(test_framework, mini_wallet, node):
642642
from .messages import COIN
643643
fee = 100 * int(node.getnetworkinfo()["relayfee"] * COIN)
644644
for _ in range(14):
645-
tx = mini_wallet.create_self_transfer(from_node=node, fee_rate=0, mempool_valid=False)['tx']
645+
tx = mini_wallet.create_self_transfer(fee_rate=0)['tx']
646646
tx.vout[0].nValue -= fee
647647
tx.vout.extend(txouts)
648648
mini_wallet.sendrawtransaction(from_node=node, tx_hex=tx.serialize().hex())

test/functional/test_framework/wallet.py

Lines changed: 105 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from random import choice
1111
from typing import (
1212
Any,
13+
List,
1314
Optional,
1415
)
1516
from test_framework.address import (
@@ -89,26 +90,34 @@ def __init__(self, test_node, *, mode=MiniWalletMode.ADDRESS_OP_TRUE):
8990
self._address = ADDRESS_BCRT1_P2SH_OP_TRUE
9091
self._scriptPubKey = bytes.fromhex(self._test_node.validateaddress(self._address)['scriptPubKey'])
9192

93+
def _create_utxo(self, *, txid, vout, value, height):
94+
return {"txid": txid, "vout": vout, "value": value, "height": height}
95+
96+
def get_balance(self):
97+
return sum(u['value'] for u in self._utxos)
98+
9299
def rescan_utxos(self):
93100
"""Drop all utxos and rescan the utxo set"""
94101
self._utxos = []
95102
res = self._test_node.scantxoutset(action="start", scanobjects=[self.get_descriptor()])
96103
assert_equal(True, res['success'])
97104
for utxo in res['unspents']:
98-
self._utxos.append({'txid': utxo['txid'], 'vout': utxo['vout'], 'value': utxo['amount'], 'height': utxo['height']})
99-
100-
def scan_blocks(self, *, start=1, num):
101-
"""Scan the blocks for self._address outputs and add them to self._utxos"""
102-
for i in range(start, start + num):
103-
block = self._test_node.getblock(blockhash=self._test_node.getblockhash(i), verbosity=2)
104-
for tx in block['tx']:
105-
self.scan_tx(tx)
105+
self._utxos.append(self._create_utxo(txid=utxo["txid"], vout=utxo["vout"], value=utxo["amount"], height=utxo["height"]))
106106

107107
def scan_tx(self, tx):
108-
"""Scan the tx for self._scriptPubKey outputs and add them to self._utxos"""
108+
"""Scan the tx and adjust the internal list of owned utxos"""
109+
for spent in tx["vin"]:
110+
# Mark spent. This may happen when the caller has ownership of a
111+
# utxo that remained in this wallet. For example, by passing
112+
# mark_as_spent=False to get_utxo or by using an utxo returned by a
113+
# create_self_transfer* call.
114+
try:
115+
self.get_utxo(txid=spent["txid"], vout=spent["vout"])
116+
except StopIteration:
117+
pass
109118
for out in tx['vout']:
110119
if out['scriptPubKey']['hex'] == self._scriptPubKey.hex():
111-
self._utxos.append({'txid': tx['txid'], 'vout': out['n'], 'value': out['value'], 'height': 0})
120+
self._utxos.append(self._create_utxo(txid=tx["txid"], vout=out["n"], value=out["value"], height=0))
112121

113122
def sign_tx(self, tx, fixed_length=True):
114123
"""Sign tx that has been created by MiniWallet in P2PK mode"""
@@ -124,14 +133,19 @@ def sign_tx(self, tx, fixed_length=True):
124133
if not fixed_length:
125134
break
126135
tx.vin[0].scriptSig = CScript([der_sig + bytes(bytearray([SIGHASH_ALL]))])
136+
tx.rehash()
127137

128138
def generate(self, num_blocks, **kwargs):
129-
"""Generate blocks with coinbase outputs to the internal address, and append the outputs to the internal list"""
139+
"""Generate blocks with coinbase outputs to the internal address, and call rescan_utxos"""
130140
blocks = self._test_node.generatetodescriptor(num_blocks, self.get_descriptor(), **kwargs)
131-
for b in blocks:
132-
block_info = self._test_node.getblock(blockhash=b, verbosity=2)
133-
cb_tx = block_info['tx'][0]
134-
self._utxos.append({'txid': cb_tx['txid'], 'vout': 0, 'value': cb_tx['vout'][0]['value'], 'height': block_info['height']})
141+
# Calling rescan_utxos here makes sure that after a generate the utxo
142+
# set is in a clean state. For example, the wallet will update
143+
# - if the caller consumed utxos, but never used them
144+
# - if the caller sent a transaction that is not mined or got rbf'd
145+
# - after block re-orgs
146+
# - the utxo height for mined mempool txs
147+
# - However, the wallet will not consider remaining mempool txs
148+
self.rescan_utxos()
135149
return blocks
136150

137151
def get_scriptPubKey(self):
@@ -144,7 +158,7 @@ def get_address(self):
144158
assert_equal(self._mode, MiniWalletMode.ADDRESS_OP_TRUE)
145159
return self._address
146160

147-
def get_utxo(self, *, txid: str = '', vout: Optional[int] = None, mark_as_spent=True):
161+
def get_utxo(self, *, txid: str = '', vout: Optional[int] = None, mark_as_spent=True) -> dict:
148162
"""
149163
Returns a utxo and marks it as spent (pops it from the internal list)
150164
@@ -164,10 +178,10 @@ def get_utxo(self, *, txid: str = '', vout: Optional[int] = None, mark_as_spent=
164178
else:
165179
return self._utxos[index]
166180

167-
def send_self_transfer(self, **kwargs):
168-
"""Create and send a tx with the specified fee_rate. Fee may be exact or at most one satoshi higher than needed."""
181+
def send_self_transfer(self, *, from_node, **kwargs):
182+
"""Call create_self_transfer and send the transaction."""
169183
tx = self.create_self_transfer(**kwargs)
170-
self.sendrawtransaction(from_node=kwargs['from_node'], tx_hex=tx['hex'])
184+
self.sendrawtransaction(from_node=from_node, tx_hex=tx['hex'])
171185
return tx
172186

173187
def send_to(self, *, from_node, scriptPubKey, amount, fee=1000):
@@ -182,46 +196,93 @@ def send_to(self, *, from_node, scriptPubKey, amount, fee=1000):
182196
183197
Returns a tuple (txid, n) referring to the created external utxo outpoint.
184198
"""
185-
tx = self.create_self_transfer(from_node=from_node, fee_rate=0, mempool_valid=False)['tx']
199+
tx = self.create_self_transfer(fee_rate=0)["tx"]
186200
assert_greater_than_or_equal(tx.vout[0].nValue, amount + fee)
187201
tx.vout[0].nValue -= (amount + fee) # change output -> MiniWallet
188202
tx.vout.append(CTxOut(amount, scriptPubKey)) # arbitrary output -> to be returned
189203
txid = self.sendrawtransaction(from_node=from_node, tx_hex=tx.serialize().hex())
190204
return txid, 1
191205

192-
def create_self_transfer(self, *, fee_rate=Decimal("0.003"), from_node=None, utxo_to_spend=None, mempool_valid=True, locktime=0, sequence=0):
193-
"""Create and return a tx with the specified fee_rate. Fee may be exact or at most one satoshi higher than needed."""
194-
from_node = from_node or self._test_node
195-
utxo_to_spend = utxo_to_spend or self.get_utxo()
196-
if self._mode in (MiniWalletMode.RAW_OP_TRUE, MiniWalletMode.ADDRESS_OP_TRUE):
197-
vsize = Decimal(85) # anyone-can-spend
198-
elif self._mode == MiniWalletMode.RAW_P2PK:
199-
vsize = Decimal(168) # P2PK (73 bytes scriptSig + 35 bytes scriptPubKey + 60 bytes other)
200-
else:
201-
assert False
202-
send_value = int(COIN * (utxo_to_spend['value'] - fee_rate * (vsize / 1000)))
203-
assert send_value > 0
206+
def send_self_transfer_multi(self, *, from_node, **kwargs):
207+
"""Call create_self_transfer_multi and send the transaction."""
208+
tx = self.create_self_transfer_multi(**kwargs)
209+
self.sendrawtransaction(from_node=from_node, tx_hex=tx["hex"])
210+
return tx
211+
212+
def create_self_transfer_multi(
213+
self,
214+
*,
215+
utxos_to_spend: Optional[List[dict]] = None,
216+
num_outputs=1,
217+
amount_per_output=0,
218+
locktime=0,
219+
sequence=0,
220+
fee_per_output=1000,
221+
):
222+
"""
223+
Create and return a transaction that spends the given UTXOs and creates a
224+
certain number of outputs with equal amounts. The output amounts can be
225+
set by amount_per_output or automatically calculated with a fee_per_output.
226+
"""
227+
utxos_to_spend = utxos_to_spend or [self.get_utxo()]
228+
sequence = [sequence] * len(utxos_to_spend) if type(sequence) is int else sequence
229+
assert_equal(len(utxos_to_spend), len(sequence))
204230

231+
# calculate output amount
232+
inputs_value_total = sum([int(COIN * utxo['value']) for utxo in utxos_to_spend])
233+
outputs_value_total = inputs_value_total - fee_per_output * num_outputs
234+
amount_per_output = amount_per_output or (outputs_value_total // num_outputs)
235+
236+
# create tx
205237
tx = CTransaction()
206-
tx.vin = [CTxIn(COutPoint(int(utxo_to_spend['txid'], 16), utxo_to_spend['vout']), nSequence=sequence)]
207-
tx.vout = [CTxOut(send_value, self._scriptPubKey)]
238+
tx.vin = [CTxIn(COutPoint(int(utxo_to_spend['txid'], 16), utxo_to_spend['vout']), nSequence=seq) for utxo_to_spend,seq in zip(utxos_to_spend, sequence)]
239+
tx.vout = [CTxOut(amount_per_output, bytearray(self._scriptPubKey)) for _ in range(num_outputs)]
208240
tx.nLockTime = locktime
241+
209242
if self._mode == MiniWalletMode.RAW_P2PK:
210243
self.sign_tx(tx)
211244
elif self._mode == MiniWalletMode.RAW_OP_TRUE:
212-
tx.vin[0].scriptSig = CScript([OP_NOP] * 24) # pad to identical size
245+
for i in range(len(utxos_to_spend)):
246+
tx.vin[i].scriptSig = CScript([OP_NOP] * 24) # pad to identical size
213247
elif self._mode == MiniWalletMode.ADDRESS_OP_TRUE:
214-
tx.vin[0].scriptSig = CScript([CScript([OP_TRUE])])
248+
for i in range(len(utxos_to_spend)):
249+
tx.vin[i].scriptSig = CScript([CScript([OP_TRUE])])
250+
else:
251+
assert False
252+
253+
txid = tx.rehash()
254+
return {
255+
"new_utxos": [self._create_utxo(
256+
txid=txid,
257+
vout=i,
258+
value=Decimal(tx.vout[i].nValue) / COIN,
259+
height=0,
260+
) for i in range(len(tx.vout))],
261+
"txid": txid,
262+
"hex": tx.serialize().hex(),
263+
"tx": tx,
264+
}
265+
266+
def create_self_transfer(self, *, fee_rate=Decimal("0.003"), fee=Decimal("0"), utxo_to_spend=None, locktime=0, sequence=0):
267+
"""Create and return a tx with the specified fee. If fee is 0, use fee_rate, where the resulting fee may be exact or at most one satoshi higher than needed."""
268+
utxo_to_spend = utxo_to_spend or self.get_utxo()
269+
assert fee_rate >= 0
270+
assert fee >= 0
271+
# calculate fee
272+
if self._mode in (MiniWalletMode.RAW_OP_TRUE, MiniWalletMode.ADDRESS_OP_TRUE):
273+
vsize = Decimal(85) # anyone-can-spend
274+
elif self._mode == MiniWalletMode.RAW_P2PK:
275+
vsize = Decimal(168) # P2PK (73 bytes scriptSig + 35 bytes scriptPubKey + 60 bytes other)
215276
else:
216277
assert False
217-
tx_hex = tx.serialize().hex()
218-
219-
tx_info = from_node.testmempoolaccept([tx_hex])[0]
220-
assert_equal(mempool_valid, tx_info['allowed'])
221-
if mempool_valid:
222-
assert_equal(len(tx_hex) // 2, vsize) # 1 byte = 2 character
223-
assert_equal(tx_info['fees']['base'], utxo_to_spend['value'] - Decimal(send_value) / COIN)
224-
return {'txid': tx_info['txid'], 'hex': tx_hex, 'tx': tx}
278+
send_value = utxo_to_spend["value"] - (fee or (fee_rate * vsize / 1000))
279+
assert send_value > 0
280+
281+
# create tx
282+
tx = self.create_self_transfer_multi(utxos_to_spend=[utxo_to_spend], locktime=locktime, sequence=sequence, amount_per_output=int(COIN * send_value))
283+
assert_equal(tx["tx"].get_vsize(), vsize)
284+
285+
return {"txid": tx["txid"], "hex": tx["hex"], "tx": tx["tx"], "new_utxo": tx["new_utxos"][0]}
225286

226287
def sendrawtransaction(self, *, from_node, tx_hex, maxfeerate=0, **kwargs):
227288
txid = from_node.sendrawtransaction(hexstring=tx_hex, maxfeerate=maxfeerate, **kwargs)

0 commit comments

Comments
 (0)