diff --git a/tests/test_wallet_vertical.py b/tests/test_wallet_vertical.py index 293f854c4..8053de8a2 100644 --- a/tests/test_wallet_vertical.py +++ b/tests/test_wallet_vertical.py @@ -14,8 +14,9 @@ from electrum import util from electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE from electrum.wallet import (sweep, Multisig_Wallet, Standard_Wallet, Imported_Wallet, Abstract_Wallet, CannotBumpFee, BumpFeeStrategy, - TransactionPotentiallyDangerousException, TransactionDangerousException, - TxSighashRiskLevel) + TransactionPotentiallyDangerousException, + TransactionDangerousException, + TxSighashRiskLevel, CannotDoubleSpendTx) from electrum.util import bfh, NotEnoughFunds, UnrelatedTransactionException, UserFacingException, TxMinedInfo from electrum.fee_policy import FixedFeePolicy from electrum.transaction import Transaction, PartialTxOutput, tx_from_any, Sighash @@ -2684,6 +2685,10 @@ class TestWalletSending(ElectrumTestCase): await self._dscancel_when_not_all_inputs_are_ismine( simulate_moving_txs=simulate_moving_txs, config=config) + with self.subTest(msg="_dscancel_sufficient_fee_increase", simulate_moving_txs=simulate_moving_txs): + await self._dscancel_sufficient_fee_increase( + simulate_moving_txs=simulate_moving_txs, + config=config) async def _dscancel_when_all_outputs_are_ismine(self, *, simulate_moving_txs, config): wallet = self.create_standard_wallet_from_seed('fold object utility erase deputy output stadium feed stereo usage modify bean', @@ -2917,6 +2922,36 @@ class TestWalletSending(ElectrumTestCase): str(tx_copy)) self.assertEqual('3021a4fe24e33af9d0ccdf25c478387c97df671fe1fd8b4db0de4255b3a348c5', tx_copy.txid()) + async def _dscancel_sufficient_fee_increase(self, *, simulate_moving_txs, config): + """ + Tries to cancel a tx with a replacement tx of the same feerate as the original tx. This shouldn't + work as the feerate needs to be higher. + """ + wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage', + config=config) + # create tx + tx_to_cancel_raw = '70736274ff0100d10200000002032b1c2c7d66e528905a11e148234ffe94cc9b4af7dbbc18dad13b9eb18050610000000000fdffffff032b1c2c7d66e528905a11e148234ffe94cc9b4af7dbbc18dad13b9eb18050610100000000fdffffff0378d4030000000000220020f381d0d2c633cdd890015bb438c8e73e9960b21defa126c594e5cf67bd06419f78d40300000000002200204a1c25db1aa7165cb655638fb32319a945262fee7c8ce6f2f17f1223679bb0b84072070000000000160014f0fe5c1867a174a12e70165e728a072619455ed5000000000001011f20a10700000000001600141ab4b6d2f79cb0a1b5be5a2372eb668bc09261f80100fd1c01020000000001012cdd7dfc38d14f2c95425bb0afc4ee93df4c7b46e9f8bd8d43d845382f88b2b60100000000fdffffff0420a10700000000001600141ab4b6d2f79cb0a1b5be5a2372eb668bc09261f820a107000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9020a1070000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b7303b0c0100000000160014da837c758fa0bce9eb845f240a06484f8dfb862c0247304402201b47c7fe41a9b5b196f1ee628d8e5065d85cba4caa44a0f8199a603cb0ee40a80220159eb66a66a5a3a396922aa3ffd7101224de75b1b57dc0c4732e284cdd228c63012102d63196184adaa312705ec7f0d8c9565261e4c19a6746f5ac3ad30cc0b932501cc7d24900220603565a3904c7d2d6a6c2cf3fcdf89d9e5c60b509483104992cfdf85b196665170c10e8a903980000008000000000020000000001011f20a107000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab900100fd1c01020000000001012cdd7dfc38d14f2c95425bb0afc4ee93df4c7b46e9f8bd8d43d845382f88b2b60100000000fdffffff0420a10700000000001600141ab4b6d2f79cb0a1b5be5a2372eb668bc09261f820a107000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9020a1070000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b7303b0c0100000000160014da837c758fa0bce9eb845f240a06484f8dfb862c0247304402201b47c7fe41a9b5b196f1ee628d8e5065d85cba4caa44a0f8199a603cb0ee40a80220159eb66a66a5a3a396922aa3ffd7101224de75b1b57dc0c4732e284cdd228c63012102d63196184adaa312705ec7f0d8c9565261e4c19a6746f5ac3ad30cc0b932501cc7d24900220602a6ff1ffc189b4776b78e20edca969cc45da3e610cc0cc79925604be43fee469f10e8a90398000000800000000001000000000000220202105dd9133f33cbd4e50443ef9af428c0be61f097f8942aaa916f50b530125aea10e8a9039800000080010000000000000000' + tx_to_cancel = tx_from_any(tx_to_cancel_raw) + if simulate_moving_txs: + partial_tx = tx_to_cancel.serialize_as_bytes().hex() + self.assertEqual(tx_to_cancel_raw, + partial_tx) + tx_to_cancel = tx_from_any(partial_tx) # simulates moving partial txn between cosigners + tx_to_cancel = wallet.sign_transaction(tx_to_cancel, password=None) + wallet.adb.receive_tx_callback(tx_to_cancel, tx_height=TX_HEIGHT_UNCONFIRMED) + + self.assertTrue(tx_to_cancel.is_complete()) + self.assertTrue(tx_to_cancel.is_segwit()) + self.assertEqual(2, len(tx_to_cancel.inputs())) + self.assertEqual(3, len(tx_to_cancel.outputs())) + + # cancel tx + tx_details = wallet.get_tx_info(tx_to_cancel) + self.assertTrue(tx_details.can_dscancel) + tx_to_cancel_feerate = tx_to_cancel.get_fee() / tx_to_cancel.estimated_size() + with self.assertRaises(CannotDoubleSpendTx): + wallet.dscancel(tx=tx_to_cancel, new_fee_rate=tx_to_cancel_feerate) + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') async def test_wallet_history_chain_of_unsigned_transactions(self, mock_save_db): wallet = self.create_standard_wallet_from_seed('cross end slow expose giraffe fuel track awake turtle capital ranch pulp',