@@ -2159,7 +2159,7 @@ async def _rbf_sufficient_fee_increase_adding_outputs_to_base_tx(self, *, simula
21592159 tx2_feerate_sat_vb = tx2.get_fee() / tx2.estimated_size()
21602160 self.assertGreater(tx2_feerate_sat_vb, tx1_feerate_sat_vb, msg="tx2 feerate lower than tx1")
21612161
2162- def _create_cause_carbon_wallet(self):
2162+ def _create_cause_carbon_wallet(self) -> tuple[Standard_Wallet, str, str] :
21632163 data = read_test_vector('cause_carbon_wallet.json')
21642164 ks = keystore.from_seed(data['seed'], passphrase='', for_multisig=False)
21652165 wallet = WalletIntegrityHelper.create_standard_wallet(ks, gap_limit=2, gap_limit_for_change=2, config=self.config)
@@ -2356,6 +2356,66 @@ async def test_rbf_batching__merge_duplicate_outputs(self):
23562356 wallet.adb.receive_tx_callback(tx3, tx_height=TX_HEIGHT_UNCONFIRMED)
23572357 self.assertEqual((0, 197_000, 0), wallet.get_balance())
23582358
2359+ async def test_rbf_batching__happypath_using_get_candidates_for_batching(self):
2360+ """Test GUI simple-send RBF 'batch with' functionality:
2361+ - while there is already an unconfirmed outgoing tx1 in the wallet history,
2362+ - try making another payment, and batch it with tx1.
2363+ """
2364+ wallet_faucet = self._create_cause_carbon_wallet()[0]
2365+ mywallet = self.create_standard_wallet_from_seed(
2366+ 'response era cable net spike again observe dumb wage wonder sail tortoise',
2367+ config=self.config)
2368+
2369+ # fund mywallet with 2 UTXOs
2370+ def fund_mywallet_from_faucet():
2371+ addr = mywallet.get_receiving_address()
2372+ assert not mywallet.adb.is_used(addr)
2373+ funding_tx1 = wallet_faucet.make_unsigned_transaction(
2374+ outputs=[PartialTxOutput.from_address_and_value(addr, 40_000)],
2375+ fee_policy=FixedFeePolicy(200), locktime=4_000_000, rbf=True,
2376+ )
2377+ wallet_faucet.sign_transaction(funding_tx1, password=None)
2378+ wallet_faucet.adb.receive_tx_callback(funding_tx1, tx_height=TX_HEIGHT_UNCONFIRMED)
2379+ mywallet.adb.receive_tx_callback(funding_tx1, tx_height=TX_HEIGHT_UNCONFIRMED)
2380+ assert mywallet.adb.is_used(addr)
2381+ fund_mywallet_from_faucet()
2382+ fund_mywallet_from_faucet()
2383+
2384+ external_addr1 = wallet_faucet.get_receiving_addresses()[0]
2385+ external_addr2 = wallet_faucet.get_receiving_addresses()[1]
2386+ assert external_addr1 and external_addr2 and (external_addr1 != external_addr2)
2387+
2388+ # create outgoing tx1
2389+ output1 = PartialTxOutput.from_address_and_value(external_addr1, 30_000)
2390+ candidates1 = mywallet.get_candidates_for_batching([output1], coins=mywallet.get_spendable_coins(domain=None))
2391+ self.assertEqual(candidates1, [])
2392+ outgoing_tx1 = mywallet.make_unsigned_transaction(
2393+ outputs=[output1],
2394+ fee_policy=FixedFeePolicy(200), locktime=4_000_001, rbf=True,
2395+ )
2396+ mywallet.sign_transaction(outgoing_tx1, password=None)
2397+ mywallet.adb.receive_tx_callback(outgoing_tx1, tx_height=TX_HEIGHT_UNCONFIRMED)
2398+
2399+ # create outgoing tx2, and try to batch it with tx1
2400+ output2 = PartialTxOutput.from_address_and_value(external_addr2, 30_000)
2401+ candidates2 = mywallet.get_candidates_for_batching([output2], coins=mywallet.get_spendable_coins(domain=None))
2402+ self.assertEqual(1, len(candidates2))
2403+ base_tx = candidates2[0]
2404+ self.assertEqual(outgoing_tx1.txid(), base_tx.txid())
2405+ outgoing_tx2 = mywallet.make_unsigned_transaction(
2406+ outputs=[output2],
2407+ fee_policy=FixedFeePolicy(200), locktime=4_000_001, rbf=True,
2408+ base_tx=base_tx,
2409+ )
2410+ mywallet.sign_transaction(outgoing_tx2, password=None)
2411+ mywallet.adb.receive_tx_callback(outgoing_tx2, tx_height=TX_HEIGHT_UNCONFIRMED)
2412+
2413+ self.assertEqual(3, len(outgoing_tx2.outputs()))
2414+ self.assertIn(output1, outgoing_tx2.outputs())
2415+ self.assertIn(output2, outgoing_tx2.outputs())
2416+ self.assertEqual('02000000000102ab99cf51f3e219735c49491a9ecf354517a215a2b462227df7e7624188a7ff830000000000fdffffff417bbd802768510a0c641fc79af6e691a01fd10f37b84162f253b594e5cb87c50100000000fdffffff032c4c0000000000001600144e1b662f616fe134430054e29295ea6e5c18f17330750000000000001600142266c890fad71396f106319368107d5b2a1146fe307500000000000016001476efaaa243327bf3a2c0f5380cb3914099448cec02473044022033f8ddcb07ad7e06cef417208a5244507157cccdd6817029e968ec60a2395add02200720eb6a7c8cea86b470398ce75d69abcc66624a2253b18ea81cd1566eb5132c0121029b1a61d66896486ab893741b38dbafb9673b91a82237d6e4ca0da3cda7cbeb7c0247304402205e00156d74bcd85ed26ee9d0bdbd72890656881e25c04b2ac94f1c6b91f1176f02205f94d055e6385b48fbd3ac1a2dc2f57a640f4b33740cd9fb9e0fc20eb0cf5dcb012102c1ed648e71f15643950b444b864ab784b9d0e31e6ca6ec7d849d3dda4d98da0501093d00',
2417+ str(outgoing_tx2))
2418+
23592419 async def test_join_psbts__merge_duplicate_outputs(self):
23602420 """txos paying to the same address might be merged into a single output with a larger value"""
23612421 rawtx1 = "70736274ff01007102000000019264597cffcce8f0c17b16a02adca7a95ae90f2ea51bd4b4df60c76dfe86686e0000000000fdffffff02400d03000000000016001458aee0f1201d1ae12dbd241209bbe92ed45e39b6108c0400000000001600144e1b662f616fe134430054e29295ea6e5c18f173c2ad26000001011f20a1070000000000160014542266519a44eb9b903761d40c6fe1055d33fa050100de02000000000101013548c9019890e27ce9e58766de05f18ea40ede70751fb6cd7a3a1715ece0a30100000000fdffffff0220a1070000000000160014542266519a44eb9b903761d40c6fe1055d33fa05485a080000000000160014bc69f7d82c403a9f35dfb6d1a4531d6b19cab0e3024730440220346b200f21c3024e1d51fb4ecddbdbd68bd24ae7b9dfd501519f6dcbeb7c052402200617e3ce7b0eb308e30caf23894fb0388b68fb1c15dd0681dd13ae5e735f148101210360d0c9ef15b8b6a16912d341ad218a4e4e4e07e9347f4a2dbc7ca8d974f8bc9ec1ad26002206029b1a61d66896486ab893741b38dbafb9673b91a82237d6e4ca0da3cda7cbeb7c101f1b48320000008000000000000000000000220203db4846ec1841f48484590e67fcd7d1039f124a04410c5794f38ec8625329ea23101f1b483200000080010000000000000000"
0 commit comments