Skip to content

Commit 8f9c74c

Browse files
committed
Merge bitcoin/bitcoin#28414: wallet rpc: return final tx hex from walletprocesspsbt if complete
2e249b9 doc: add release note for PR #28414 (Matthew Zipkin) 4614332 test: remove unnecessary finalizepsbt rpc calls (ismaelsadeeq) e3d484b wallet rpc: return final tx hex from walletprocesspsbt if complete (Matthew Zipkin) Pull request description: See bitcoin/bitcoin#28363 (comment) `walletprocesspsbt` currently returns a base64-encoded PSBT and a boolean indicating if the tx is "complete". If it is complete, the base64 PSBT can be finalized with `finalizepsbt` which returns the hex-encoded transaction suitable for `sendrawtransaction`. With this patch, `walletprocesspsbt` return object will ALSO include the broadcast-able hex string if the tx is already final. This saves users the extra step of calling `finalizepsbt` assuming they have already inspected and approve the transaction from earlier steps. ACKs for top commit: ismaelsadeeq: re ACK 2e249b9 BrandonOdiwuor: re ACK 2e249b9 Randy808: Tested ACK 2e249b9 achow101: ACK 2e249b9 ishaanam: ACK 2e249b9 Tree-SHA512: 229c1103265a9b4248f080935a7ad5607c3be3f9a096a9ab6554093b2cd8aa8b4d1fa55b1b97d3925ba208dbc3ccba4e4d37c40e1491db0d27ba3d9fe98f931e
2 parents 7649431 + 2e249b9 commit 8f9c74c

8 files changed

+56
-46
lines changed

doc/release-notes-28414.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
RPC Wallet
2+
----------
3+
4+
- RPC `walletprocesspsbt` return object now includes field `hex` (if the transaction
5+
is complete) containing the serialized transaction suitable for RPC `sendrawtransaction`. (#28414)

src/wallet/rpc/spend.cpp

+9
Original file line numberDiff line numberDiff line change
@@ -1566,6 +1566,7 @@ RPCHelpMan walletprocesspsbt()
15661566
{
15671567
{RPCResult::Type::STR, "psbt", "The base64-encoded partially signed transaction"},
15681568
{RPCResult::Type::BOOL, "complete", "If the transaction has a complete set of signatures"},
1569+
{RPCResult::Type::STR_HEX, "hex", /*optional=*/true, "The hex-encoded network transaction if complete"},
15691570
}
15701571
},
15711572
RPCExamples{
@@ -1609,6 +1610,14 @@ RPCHelpMan walletprocesspsbt()
16091610
ssTx << psbtx;
16101611
result.pushKV("psbt", EncodeBase64(ssTx.str()));
16111612
result.pushKV("complete", complete);
1613+
if (complete) {
1614+
CMutableTransaction mtx;
1615+
// Returns true if complete, which we already think it is.
1616+
CHECK_NONFATAL(FinalizeAndExtractPSBT(psbtx, mtx));
1617+
CDataStream ssTx_final(SER_NETWORK, PROTOCOL_VERSION);
1618+
ssTx_final << mtx;
1619+
result.pushKV("hex", HexStr(ssTx_final));
1620+
}
16121621

16131622
return result;
16141623
},

test/functional/rpc_psbt.py

+34-29
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,11 @@ def test_utxo_conversion(self):
106106
assert "non_witness_utxo" in mining_node.decodepsbt(psbt_new.to_base64())["inputs"][0]
107107

108108
# Have the offline node sign the PSBT (which will remove the non-witness UTXO)
109-
signed_psbt = offline_node.walletprocesspsbt(psbt_new.to_base64())["psbt"]
110-
assert not "non_witness_utxo" in mining_node.decodepsbt(signed_psbt)["inputs"][0]
109+
signed_psbt = offline_node.walletprocesspsbt(psbt_new.to_base64())
110+
assert not "non_witness_utxo" in mining_node.decodepsbt(signed_psbt["psbt"])["inputs"][0]
111111

112112
# Make sure we can mine the resulting transaction
113-
txid = mining_node.sendrawtransaction(mining_node.finalizepsbt(signed_psbt)["hex"])
113+
txid = mining_node.sendrawtransaction(signed_psbt["hex"])
114114
self.generate(mining_node, nblocks=1, sync_fun=lambda: self.sync_all([online_node, mining_node]))
115115
assert_equal(online_node.gettxout(txid,0)["confirmations"], 1)
116116

@@ -142,9 +142,8 @@ def test_input_confs_control(self):
142142
utxo1 = tx1_inputs[0]
143143
assert_equal(unconfirmed_txid, utxo1['txid'])
144144

145-
signed_tx1 = wallet.walletprocesspsbt(psbtx1)['psbt']
146-
final_tx1 = wallet.finalizepsbt(signed_tx1)['hex']
147-
txid1 = self.nodes[0].sendrawtransaction(final_tx1)
145+
signed_tx1 = wallet.walletprocesspsbt(psbtx1)
146+
txid1 = self.nodes[0].sendrawtransaction(signed_tx1['hex'])
148147

149148
mempool = self.nodes[0].getrawmempool()
150149
assert txid1 in mempool
@@ -157,9 +156,8 @@ def test_input_confs_control(self):
157156

158157
self.log.info("Fail to broadcast a new PSBT with maxconf 0 due to BIP125 rules to verify it actually chose unconfirmed outputs")
159158
psbt_invalid = wallet.walletcreatefundedpsbt([{'txid': utxo1['txid'], 'vout': utxo1['vout']}], {target_address: 1}, 0, {'add_inputs': True, 'maxconf': 0, 'fee_rate': 10})['psbt']
160-
signed_invalid = wallet.walletprocesspsbt(psbt_invalid)['psbt']
161-
final_invalid = wallet.finalizepsbt(signed_invalid)['hex']
162-
assert_raises_rpc_error(-26, "bad-txns-spends-conflicting-tx", self.nodes[0].sendrawtransaction, final_invalid)
159+
signed_invalid = wallet.walletprocesspsbt(psbt_invalid)
160+
assert_raises_rpc_error(-26, "bad-txns-spends-conflicting-tx", self.nodes[0].sendrawtransaction, signed_invalid['hex'])
163161

164162
self.log.info("Craft a replacement adding inputs with highest confs possible")
165163
psbtx2 = wallet.walletcreatefundedpsbt([{'txid': utxo1['txid'], 'vout': utxo1['vout']}], {target_address: 1}, 0, {'add_inputs': True, 'minconf': 2, 'fee_rate': 10})['psbt']
@@ -169,9 +167,8 @@ def test_input_confs_control(self):
169167
if vin['txid'] != unconfirmed_txid:
170168
assert_greater_than_or_equal(self.nodes[0].gettxout(vin['txid'], vin['vout'])['confirmations'], 2)
171169

172-
signed_tx2 = wallet.walletprocesspsbt(psbtx2)['psbt']
173-
final_tx2 = wallet.finalizepsbt(signed_tx2)['hex']
174-
txid2 = self.nodes[0].sendrawtransaction(final_tx2)
170+
signed_tx2 = wallet.walletprocesspsbt(psbtx2)
171+
txid2 = self.nodes[0].sendrawtransaction(signed_tx2['hex'])
175172

176173
mempool = self.nodes[0].getrawmempool()
177174
assert txid1 not in mempool
@@ -217,12 +214,21 @@ def run_test(self):
217214

218215
self.nodes[0].walletpassphrase(passphrase="password", timeout=1000000)
219216

220-
# Sign the transaction and send
221-
signed_tx = self.nodes[0].walletprocesspsbt(psbt=psbtx, finalize=False)['psbt']
222-
finalized_tx = self.nodes[0].walletprocesspsbt(psbt=psbtx, finalize=True)['psbt']
223-
assert signed_tx != finalized_tx
224-
final_tx = self.nodes[0].finalizepsbt(signed_tx)['hex']
225-
self.nodes[0].sendrawtransaction(final_tx)
217+
# Sign the transaction but don't finalize
218+
processed_psbt = self.nodes[0].walletprocesspsbt(psbt=psbtx, finalize=False)
219+
assert "hex" not in processed_psbt
220+
signed_psbt = processed_psbt['psbt']
221+
222+
# Finalize and send
223+
finalized_hex = self.nodes[0].finalizepsbt(signed_psbt)['hex']
224+
self.nodes[0].sendrawtransaction(finalized_hex)
225+
226+
# Alternative method: sign AND finalize in one command
227+
processed_finalized_psbt = self.nodes[0].walletprocesspsbt(psbt=psbtx, finalize=True)
228+
finalized_psbt = processed_finalized_psbt['psbt']
229+
finalized_psbt_hex = processed_finalized_psbt['hex']
230+
assert signed_psbt != finalized_psbt
231+
assert finalized_psbt_hex == finalized_hex
226232

227233
# Manually selected inputs can be locked:
228234
assert_equal(len(self.nodes[0].listlockunspent()), 0)
@@ -296,7 +302,7 @@ def run_test(self):
296302
# Check decodepsbt fee calculation (input values shall only be counted once per UTXO)
297303
assert_equal(decoded['fee'], created_psbt['fee'])
298304
assert_equal(walletprocesspsbt_out['complete'], True)
299-
self.nodes[1].sendrawtransaction(self.nodes[1].finalizepsbt(walletprocesspsbt_out['psbt'])['hex'])
305+
self.nodes[1].sendrawtransaction(walletprocesspsbt_out['hex'])
300306

301307
self.log.info("Test walletcreatefundedpsbt fee rate of 10000 sat/vB and 0.1 BTC/kvB produces a total fee at or slightly below -maxtxfee (~0.05290000)")
302308
res1 = self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"fee_rate": 10000, "add_inputs": True})
@@ -387,7 +393,7 @@ def run_test(self):
387393
# partially sign with node 2. This should be complete and sendable
388394
walletprocesspsbt_out = self.nodes[2].walletprocesspsbt(psbtx)
389395
assert_equal(walletprocesspsbt_out['complete'], True)
390-
self.nodes[2].sendrawtransaction(self.nodes[2].finalizepsbt(walletprocesspsbt_out['psbt'])['hex'])
396+
self.nodes[2].sendrawtransaction(walletprocesspsbt_out['hex'])
391397

392398
# check that walletprocesspsbt fails to decode a non-psbt
393399
rawtx = self.nodes[1].createrawtransaction([{"txid":txid,"vout":p2wpkh_pos}], {self.nodes[1].getnewaddress():9.99})
@@ -739,14 +745,13 @@ def test_psbt_input_keys(psbt_input, keys):
739745
assert not signed['complete']
740746
signed = self.nodes[0].walletprocesspsbt(signed['psbt'])
741747
assert signed['complete']
742-
self.nodes[0].finalizepsbt(signed['psbt'])
743748

744749
psbt = wallet.walletcreatefundedpsbt([ext_utxo], {self.nodes[0].getnewaddress(): 15}, 0, {"add_inputs": True, "solving_data":{"descriptors": [desc]}})
745750
signed = wallet.walletprocesspsbt(psbt['psbt'])
746751
assert not signed['complete']
747752
signed = self.nodes[0].walletprocesspsbt(signed['psbt'])
748753
assert signed['complete']
749-
final = self.nodes[0].finalizepsbt(signed['psbt'], False)
754+
final = signed['hex']
750755

751756
dec = self.nodes[0].decodepsbt(signed["psbt"])
752757
for i, txin in enumerate(dec["tx"]["vin"]):
@@ -781,8 +786,8 @@ def test_psbt_input_keys(psbt_input, keys):
781786
)
782787
signed = wallet.walletprocesspsbt(psbt["psbt"])
783788
signed = self.nodes[0].walletprocesspsbt(signed["psbt"])
784-
final = self.nodes[0].finalizepsbt(signed["psbt"])
785-
assert self.nodes[0].testmempoolaccept([final["hex"]])[0]["allowed"]
789+
final = signed["hex"]
790+
assert self.nodes[0].testmempoolaccept([final])[0]["allowed"]
786791
# Reducing the weight should have a lower fee
787792
psbt2 = wallet.walletcreatefundedpsbt(
788793
inputs=[{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": low_input_weight}],
@@ -837,8 +842,8 @@ def test_psbt_input_keys(psbt_input, keys):
837842
self.nodes[0].importprivkey(privkey)
838843

839844
psbt = watchonly.sendall([wallet.getnewaddress()])["psbt"]
840-
psbt = self.nodes[0].walletprocesspsbt(psbt)["psbt"]
841-
self.nodes[0].sendrawtransaction(self.nodes[0].finalizepsbt(psbt)["hex"])
845+
signed_tx = self.nodes[0].walletprocesspsbt(psbt)
846+
self.nodes[0].sendrawtransaction(signed_tx["hex"])
842847

843848
# Same test but for taproot
844849
if self.options.descriptors:
@@ -853,8 +858,8 @@ def test_psbt_input_keys(psbt_input, keys):
853858
self.nodes[0].importdescriptors([{"desc": descsum_create("tr({})".format(privkey)), "timestamp":"now"}])
854859

855860
psbt = watchonly.sendall([wallet.getnewaddress(), addr])["psbt"]
856-
psbt = self.nodes[0].walletprocesspsbt(psbt)["psbt"]
857-
txid = self.nodes[0].sendrawtransaction(self.nodes[0].finalizepsbt(psbt)["hex"])
861+
processed_psbt = self.nodes[0].walletprocesspsbt(psbt)
862+
txid = self.nodes[0].sendrawtransaction(processed_psbt["hex"])
858863
vout = find_vout_for_address(self.nodes[0], txid, addr)
859864

860865
# Make sure tap tree is in psbt
@@ -871,7 +876,7 @@ def test_psbt_input_keys(psbt_input, keys):
871876
vout = find_vout_for_address(self.nodes[0], txid, addr)
872877
psbt = self.nodes[0].createpsbt([{"txid": txid, "vout": vout}], [{self.nodes[0].getnewaddress(): 0.9999}])
873878
signed = self.nodes[0].walletprocesspsbt(psbt)
874-
rawtx = self.nodes[0].finalizepsbt(signed["psbt"])["hex"]
879+
rawtx = signed["hex"]
875880
self.nodes[0].sendrawtransaction(rawtx)
876881
self.generate(self.nodes[0], 1)
877882

test/functional/wallet_bumpfee.py

+4-7
Original file line numberDiff line numberDiff line change
@@ -403,8 +403,7 @@ def test_notmine_bumpfee(self, rbf_node, peer_node, dest_address):
403403
def finish_psbtbumpfee(psbt):
404404
psbt = rbf_node.walletprocesspsbt(psbt)
405405
psbt = peer_node.walletprocesspsbt(psbt["psbt"])
406-
final = rbf_node.finalizepsbt(psbt["psbt"])
407-
res = rbf_node.testmempoolaccept([final["hex"]])
406+
res = rbf_node.testmempoolaccept([psbt["hex"]])
408407
assert res[0]["allowed"]
409408
assert_greater_than(res[0]["fees"]["base"], old_fee)
410409

@@ -638,8 +637,7 @@ def test_watchonly_psbt(self, peer_node, rbf_node, dest_address):
638637
psbt = watcher.walletcreatefundedpsbt([watcher.listunspent()[0]], {dest_address: 0.0005}, 0,
639638
{"fee_rate": 1, "add_inputs": False}, True)['psbt']
640639
psbt_signed = signer.walletprocesspsbt(psbt=psbt, sign=True, sighashtype="ALL", bip32derivs=True)
641-
psbt_final = watcher.finalizepsbt(psbt_signed["psbt"])
642-
original_txid = watcher.sendrawtransaction(psbt_final["hex"])
640+
original_txid = watcher.sendrawtransaction(psbt_signed["hex"])
643641
assert_equal(len(watcher.decodepsbt(psbt)["tx"]["vin"]), 1)
644642

645643
# bumpfee can't be used on watchonly wallets
@@ -654,11 +652,10 @@ def test_watchonly_psbt(self, peer_node, rbf_node, dest_address):
654652

655653
# Sign bumped transaction
656654
bumped_psbt_signed = signer.walletprocesspsbt(psbt=bumped_psbt["psbt"], sign=True, sighashtype="ALL", bip32derivs=True)
657-
bumped_psbt_final = watcher.finalizepsbt(bumped_psbt_signed["psbt"])
658-
assert bumped_psbt_final["complete"]
655+
assert bumped_psbt_signed["complete"]
659656

660657
# Broadcast bumped transaction
661-
bumped_txid = watcher.sendrawtransaction(bumped_psbt_final["hex"])
658+
bumped_txid = watcher.sendrawtransaction(bumped_psbt_signed["hex"])
662659
assert bumped_txid in rbf_node.getrawmempool()
663660
assert original_txid not in rbf_node.getrawmempool()
664661

test/functional/wallet_fundrawtransaction.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -556,8 +556,7 @@ def test_spend_2of2(self):
556556
funded_psbt = wmulti.walletcreatefundedpsbt(inputs=inputs, outputs=outputs, changeAddress=w2.getrawchangeaddress())['psbt']
557557

558558
signed_psbt = w2.walletprocesspsbt(funded_psbt)
559-
final_psbt = w2.finalizepsbt(signed_psbt['psbt'])
560-
self.nodes[2].sendrawtransaction(final_psbt['hex'])
559+
self.nodes[2].sendrawtransaction(signed_psbt['hex'])
561560
self.generate(self.nodes[2], 1)
562561

563562
# Make sure funds are received at node1.

test/functional/wallet_multisig_descriptor_psbt.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,7 @@ def run_test(self):
150150
signing_wallet = participants["signers"][m]
151151
psbt = signing_wallet.walletprocesspsbt(psbt["psbt"])
152152
assert_equal(psbt["complete"], m == self.M - 1)
153-
finalized = coordinator_wallet.finalizepsbt(psbt["psbt"])
154-
coordinator_wallet.sendrawtransaction(finalized["hex"])
153+
coordinator_wallet.sendrawtransaction(psbt["hex"])
155154

156155
self.log.info("Check that balances are correct after the transaction has been included in a block.")
157156
self.generate(self.nodes[0], 1)

test/functional/wallet_send.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -530,13 +530,11 @@ def run_test(self):
530530
signed = ext_wallet.walletprocesspsbt(res["psbt"])
531531
signed = ext_fund.walletprocesspsbt(res["psbt"])
532532
assert signed["complete"]
533-
self.nodes[0].finalizepsbt(signed["psbt"])
534533

535534
res = self.test_send(from_wallet=ext_wallet, to_wallet=self.nodes[0], amount=15, inputs=[ext_utxo], add_inputs=True, psbt=True, include_watching=True, solving_data={"descriptors": [desc]})
536535
signed = ext_wallet.walletprocesspsbt(res["psbt"])
537536
signed = ext_fund.walletprocesspsbt(res["psbt"])
538537
assert signed["complete"]
539-
self.nodes[0].finalizepsbt(signed["psbt"])
540538

541539
dec = self.nodes[0].decodepsbt(signed["psbt"])
542540
for i, txin in enumerate(dec["tx"]["vin"]):
@@ -574,8 +572,7 @@ def run_test(self):
574572
signed = ext_wallet.walletprocesspsbt(res["psbt"])
575573
signed = ext_fund.walletprocesspsbt(res["psbt"])
576574
assert signed["complete"]
577-
tx = self.nodes[0].finalizepsbt(signed["psbt"])
578-
testres = self.nodes[0].testmempoolaccept([tx["hex"]])[0]
575+
testres = self.nodes[0].testmempoolaccept([signed["hex"]])[0]
579576
assert_equal(testres["allowed"], True)
580577
assert_fee_amount(testres["fees"]["base"], testres["vsize"], Decimal(0.0001))
581578

test/functional/wallet_signer.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,7 @@ def test_valid_signer(self):
169169
dest = self.nodes[0].getnewaddress(address_type='bech32')
170170
mock_psbt = mock_wallet.walletcreatefundedpsbt([], {dest:0.5}, 0, {'replaceable': True}, True)['psbt']
171171
mock_psbt_signed = mock_wallet.walletprocesspsbt(psbt=mock_psbt, sign=True, sighashtype="ALL", bip32derivs=True)
172-
mock_psbt_final = mock_wallet.finalizepsbt(mock_psbt_signed["psbt"])
173-
mock_tx = mock_psbt_final["hex"]
172+
mock_tx = mock_psbt_signed["hex"]
174173
assert mock_wallet.testmempoolaccept([mock_tx])[0]["allowed"]
175174

176175
# # Create a new wallet and populate with specific public keys, in order

0 commit comments

Comments
 (0)