wallet: fix bump_fee and dscancel for "not all inputs ismine" case
we fetch the missing prev txs over network fixes #6551 fixes #6864
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import copy
|
||||
from datetime import datetime
|
||||
from typing import NamedTuple, Callable, TYPE_CHECKING
|
||||
from functools import partial
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.factory import Factory
|
||||
@@ -18,6 +19,7 @@ from electrum.util import InvalidPassword
|
||||
from electrum.address_synchronizer import TX_HEIGHT_LOCAL
|
||||
from electrum.wallet import CannotBumpFee, CannotDoubleSpendTx
|
||||
from electrum.transaction import Transaction, PartialTransaction
|
||||
from electrum.network import NetworkException
|
||||
from ...util import address_colors
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -137,6 +139,7 @@ class TxDialog(Factory.Popup):
|
||||
# As a result, e.g. we might learn an imported address tx is segwit,
|
||||
# or that a beyond-gap-limit address is is_mine.
|
||||
# note: this might fetch prev txs over the network.
|
||||
# note: this is a no-op for complete txs
|
||||
tx.add_info_from_wallet(self.wallet)
|
||||
|
||||
def on_open(self):
|
||||
@@ -231,22 +234,51 @@ class TxDialog(Factory.Popup):
|
||||
action_button = self.ids.action_button
|
||||
self._action_button_fn(action_button)
|
||||
|
||||
def _add_info_to_tx_from_wallet_and_network(self, tx: PartialTransaction) -> bool:
|
||||
"""Returns whether successful."""
|
||||
# note side-effect: tx is being mutated
|
||||
assert isinstance(tx, PartialTransaction)
|
||||
try:
|
||||
# note: this might download input utxos over network
|
||||
# FIXME network code in gui thread...
|
||||
tx.add_info_from_wallet(self.wallet, ignore_network_issues=False)
|
||||
except NetworkException as e:
|
||||
self.app.show_error(repr(e))
|
||||
return False
|
||||
return True
|
||||
|
||||
def do_rbf(self):
|
||||
from .bump_fee_dialog import BumpFeeDialog
|
||||
fee = self.wallet.get_wallet_delta(self.tx).fee
|
||||
if fee is None:
|
||||
self.app.show_error(_("Can't bump fee: unknown fee for original transaction."))
|
||||
tx = self.tx
|
||||
txid = tx.txid()
|
||||
assert txid
|
||||
if not isinstance(tx, PartialTransaction):
|
||||
tx = PartialTransaction.from_tx(tx)
|
||||
if not self._add_info_to_tx_from_wallet_and_network(tx):
|
||||
return
|
||||
size = self.tx.estimated_size()
|
||||
d = BumpFeeDialog(self.app, fee, size, self._do_rbf)
|
||||
fee = tx.get_fee()
|
||||
assert fee is not None
|
||||
size = tx.estimated_size()
|
||||
cb = partial(self._do_rbf, tx=tx, txid=txid)
|
||||
d = BumpFeeDialog(self.app, fee, size, cb)
|
||||
d.open()
|
||||
|
||||
def _do_rbf(self, new_fee_rate, is_final):
|
||||
def _do_rbf(
|
||||
self,
|
||||
new_fee_rate,
|
||||
is_final,
|
||||
*,
|
||||
tx: PartialTransaction,
|
||||
txid: str,
|
||||
):
|
||||
if new_fee_rate is None:
|
||||
return
|
||||
try:
|
||||
new_tx = self.wallet.bump_fee(tx=self.tx,
|
||||
new_fee_rate=new_fee_rate)
|
||||
new_tx = self.wallet.bump_fee(
|
||||
tx=tx,
|
||||
txid=txid,
|
||||
new_fee_rate=new_fee_rate,
|
||||
)
|
||||
except CannotBumpFee as e:
|
||||
self.app.show_error(str(e))
|
||||
return
|
||||
@@ -258,20 +290,33 @@ class TxDialog(Factory.Popup):
|
||||
|
||||
def do_dscancel(self):
|
||||
from .dscancel_dialog import DSCancelDialog
|
||||
fee = self.wallet.get_wallet_delta(self.tx).fee
|
||||
if fee is None:
|
||||
self.app.show_error(_('Cannot cancel transaction') + ': ' + _('unknown fee for original transaction'))
|
||||
tx = self.tx
|
||||
txid = tx.txid()
|
||||
assert txid
|
||||
if not isinstance(tx, PartialTransaction):
|
||||
tx = PartialTransaction.from_tx(tx)
|
||||
if not self._add_info_to_tx_from_wallet_and_network(tx):
|
||||
return
|
||||
size = self.tx.estimated_size()
|
||||
d = DSCancelDialog(self.app, fee, size, self._do_dscancel)
|
||||
fee = tx.get_fee()
|
||||
assert fee is not None
|
||||
size = tx.estimated_size()
|
||||
cb = partial(self._do_dscancel, tx=tx)
|
||||
d = DSCancelDialog(self.app, fee, size, cb)
|
||||
d.open()
|
||||
|
||||
def _do_dscancel(self, new_fee_rate):
|
||||
def _do_dscancel(
|
||||
self,
|
||||
new_fee_rate,
|
||||
*,
|
||||
tx: PartialTransaction,
|
||||
):
|
||||
if new_fee_rate is None:
|
||||
return
|
||||
try:
|
||||
new_tx = self.wallet.dscancel(tx=self.tx,
|
||||
new_fee_rate=new_fee_rate)
|
||||
new_tx = self.wallet.dscancel(
|
||||
tx=tx,
|
||||
new_fee_rate=new_fee_rate,
|
||||
)
|
||||
except CannotDoubleSpendTx as e:
|
||||
self.app.show_error(str(e))
|
||||
return
|
||||
|
||||
@@ -691,14 +691,13 @@ class HistoryList(MyTreeView, AcceptFileDragDrop):
|
||||
if channel_id:
|
||||
menu.addAction(_("View Channel"), lambda: self.parent.show_channel(bytes.fromhex(channel_id)))
|
||||
if is_unconfirmed and tx:
|
||||
# note: the current implementation of RBF *needs* the old tx fee
|
||||
if tx_details.can_bump and tx_details.fee is not None:
|
||||
if tx_details.can_bump:
|
||||
menu.addAction(_("Increase fee"), lambda: self.parent.bump_fee_dialog(tx))
|
||||
else:
|
||||
child_tx = self.wallet.cpfp(tx, 0)
|
||||
if child_tx:
|
||||
menu.addAction(_("Child pays for parent"), lambda: self.parent.cpfp(tx, child_tx))
|
||||
if tx_details.can_dscancel and tx_details.fee is not None:
|
||||
if tx_details.can_dscancel:
|
||||
menu.addAction(_("Cancel (double-spend)"), lambda: self.parent.dscancel_dialog(tx))
|
||||
invoices = self.wallet.get_relevant_invoices_for_tx(tx)
|
||||
if len(invoices) == 1:
|
||||
|
||||
@@ -70,7 +70,8 @@ from electrum.wallet import (Multisig_Wallet, CannotBumpFee, Abstract_Wallet,
|
||||
sweep_preparations, InternalAddressCorruption,
|
||||
CannotDoubleSpendTx)
|
||||
from electrum.version import ELECTRUM_VERSION
|
||||
from electrum.network import Network, TxBroadcastError, BestEffortRequestFailed, UntrustedServerReturnedError
|
||||
from electrum.network import (Network, TxBroadcastError, BestEffortRequestFailed,
|
||||
UntrustedServerReturnedError, NetworkException)
|
||||
from electrum.exchange_rate import FxThread
|
||||
from electrum.simple_config import SimpleConfig
|
||||
from electrum.logging import Logger
|
||||
@@ -90,7 +91,7 @@ from .util import (read_QIcon, ColorScheme, text_dialog, icon_path, WaitingDialo
|
||||
import_meta_gui, export_meta_gui,
|
||||
filename_field, address_field, char_width_in_lineedit, webopen,
|
||||
TRANSACTION_FILE_EXTENSION_FILTER_ANY, MONOSPACE_FONT,
|
||||
getOpenFileName, getSaveFileName)
|
||||
getOpenFileName, getSaveFileName, BlockingWaitingDialog)
|
||||
from .util import ButtonsTextEdit, ButtonsLineEdit
|
||||
from .installwizard import WIF_HELP_TEXT
|
||||
from .history_list import HistoryList, HistoryModel
|
||||
@@ -3189,13 +3190,31 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
||||
new_tx.set_rbf(True)
|
||||
self.show_transaction(new_tx)
|
||||
|
||||
def _add_info_to_tx_from_wallet_and_network(self, tx: PartialTransaction) -> bool:
|
||||
"""Returns whether successful."""
|
||||
# note side-effect: tx is being mutated
|
||||
assert isinstance(tx, PartialTransaction)
|
||||
try:
|
||||
# note: this might download input utxos over network
|
||||
BlockingWaitingDialog(
|
||||
self,
|
||||
_("Adding info to tx, from wallet and network..."),
|
||||
lambda: tx.add_info_from_wallet(self.wallet, ignore_network_issues=False),
|
||||
)
|
||||
except NetworkException as e:
|
||||
self.show_error(repr(e))
|
||||
return False
|
||||
return True
|
||||
|
||||
def bump_fee_dialog(self, tx: Transaction):
|
||||
txid = tx.txid()
|
||||
assert txid
|
||||
fee = self.wallet.get_tx_fee(txid)
|
||||
if fee is None:
|
||||
self.show_error(_("Can't bump fee: unknown fee for original transaction."))
|
||||
if not isinstance(tx, PartialTransaction):
|
||||
tx = PartialTransaction.from_tx(tx)
|
||||
if not self._add_info_to_tx_from_wallet_and_network(tx):
|
||||
return
|
||||
fee = tx.get_fee()
|
||||
assert fee is not None
|
||||
tx_label = self.wallet.get_label_for_txid(txid)
|
||||
tx_size = tx.estimated_size()
|
||||
old_fee_rate = fee / tx_size # sat/vbyte
|
||||
@@ -3236,7 +3255,12 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
||||
is_final = cb.isChecked()
|
||||
new_fee_rate = feerate_e.get_amount()
|
||||
try:
|
||||
new_tx = self.wallet.bump_fee(tx=tx, new_fee_rate=new_fee_rate, coins=self.get_coins())
|
||||
new_tx = self.wallet.bump_fee(
|
||||
tx=tx,
|
||||
txid=txid,
|
||||
new_fee_rate=new_fee_rate,
|
||||
coins=self.get_coins(),
|
||||
)
|
||||
except CannotBumpFee as e:
|
||||
self.show_error(str(e))
|
||||
return
|
||||
@@ -3247,10 +3271,12 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
||||
def dscancel_dialog(self, tx: Transaction):
|
||||
txid = tx.txid()
|
||||
assert txid
|
||||
fee = self.wallet.get_tx_fee(txid)
|
||||
if fee is None:
|
||||
self.show_error(_('Cannot cancel transaction') + ': ' + _('unknown fee for original transaction'))
|
||||
if not isinstance(tx, PartialTransaction):
|
||||
tx = PartialTransaction.from_tx(tx)
|
||||
if not self._add_info_to_tx_from_wallet_and_network(tx):
|
||||
return
|
||||
fee = tx.get_fee()
|
||||
assert fee is not None
|
||||
tx_size = tx.estimated_size()
|
||||
old_fee_rate = fee / tx_size # sat/vbyte
|
||||
d = WindowModalDialog(self, _('Cancel transaction'))
|
||||
|
||||
@@ -11,7 +11,7 @@ from electrum import Transaction
|
||||
from electrum import SimpleConfig
|
||||
from electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT
|
||||
from electrum.wallet import sweep, Multisig_Wallet, Standard_Wallet, Imported_Wallet, restore_wallet_from_text, Abstract_Wallet
|
||||
from electrum.util import bfh, bh2u
|
||||
from electrum.util import bfh, bh2u, create_and_start_event_loop
|
||||
from electrum.transaction import TxOutput, Transaction, PartialTransaction, PartialTxOutput, PartialTxInput, tx_from_any
|
||||
from electrum.mnemonic import seed_type
|
||||
|
||||
@@ -575,11 +575,11 @@ class TestWalletSending(TestCaseForTestnet):
|
||||
super().setUp()
|
||||
self.config = SimpleConfig({'electrum_path': self.electrum_path})
|
||||
|
||||
def create_standard_wallet_from_seed(self, seed_words, *, config=None):
|
||||
def create_standard_wallet_from_seed(self, seed_words, *, config=None, gap_limit=2):
|
||||
if config is None:
|
||||
config = self.config
|
||||
ks = keystore.from_seed(seed_words, '', False)
|
||||
return WalletIntegrityHelper.create_standard_wallet(ks, gap_limit=2, config=config)
|
||||
return WalletIntegrityHelper.create_standard_wallet(ks, gap_limit=gap_limit, config=config)
|
||||
|
||||
@mock.patch.object(wallet.Abstract_Wallet, 'save_db')
|
||||
def test_sending_between_p2wpkh_and_compressed_p2pkh(self, mock_save_db):
|
||||
@@ -929,6 +929,14 @@ class TestWalletSending(TestCaseForTestnet):
|
||||
self._rbf_batching(
|
||||
simulate_moving_txs=simulate_moving_txs,
|
||||
config=config)
|
||||
with self.subTest(msg="_bump_fee_when_not_all_inputs_are_ismine_subcase_some_outputs_are_ismine_but_not_all", simulate_moving_txs=simulate_moving_txs):
|
||||
self._bump_fee_when_not_all_inputs_are_ismine_subcase_some_outputs_are_ismine_but_not_all(
|
||||
simulate_moving_txs=simulate_moving_txs,
|
||||
config=config)
|
||||
with self.subTest(msg="_bump_fee_when_not_all_inputs_are_ismine_subcase_all_outputs_are_ismine", simulate_moving_txs=simulate_moving_txs):
|
||||
self._bump_fee_when_not_all_inputs_are_ismine_subcase_all_outputs_are_ismine(
|
||||
simulate_moving_txs=simulate_moving_txs,
|
||||
config=config)
|
||||
|
||||
def _bump_fee_p2pkh_when_there_is_a_change_address(self, *, simulate_moving_txs, config):
|
||||
wallet = self.create_standard_wallet_from_seed('fold object utility erase deputy output stadium feed stereo usage modify bean',
|
||||
@@ -1134,6 +1142,130 @@ class TestWalletSending(TestCaseForTestnet):
|
||||
wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
|
||||
self.assertEqual((0, 7490060, 0), wallet.get_balance())
|
||||
|
||||
def _bump_fee_when_not_all_inputs_are_ismine_subcase_some_outputs_are_ismine_but_not_all(self, *, simulate_moving_txs, config):
|
||||
class NetworkMock:
|
||||
relay_fee = 1000
|
||||
async def get_transaction(self, txid, timeout=None):
|
||||
if txid == "597098f9077cd2a7bf5bb2a03c9ae5fcd9d1f07c0891cb42cbb129cf9eaf57fd":
|
||||
return "02000000000102a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540000000000fdffffffbdeb0175b1c51c96843d1952f7e1c49c1703717d7d020048d4de0a8eed94dad50000000000fdffffff03b2a00700000000001600140cd6c9f8ce0aa73d77fcf7f156c74f5cbec6906bb2a00700000000001600146435504ddc95e6019a90bb7dfc7ca81a88a8633106d790000000000016001444bd3017ee214370abf683abaa7f6204c9f40210024730440220652a04a2a301d9a031a034f3ae48174e204e17acf7bfc27f0dcab14243f73e2202207b29e964c434dfb2c515232d36566a40dccd4dd93ccb7fd15260ecbda10f0d9801210231994e564a0530068d17a9b0f85bec58d1352517a2861ea99e5b3070d2c5dbda02473044022072186473874919019da0e3d92b6e0aa4f88cba448ed5434615e5a3c8e2b7c42a02203ec05cef66960d5bc45d0f3d25675190cf8035b11a05ed4b719fd9c3a894899b012102f5fdca8c4e30ba0a1babf9cf9ebe62519b08aead351c349ed1ffc8316c24f542d7f61c00"
|
||||
else:
|
||||
raise Exception("unexpected txid")
|
||||
def has_internet_connection(self):
|
||||
return True
|
||||
def run_from_another_thread(self, coro, *, timeout=None):
|
||||
loop, stop_loop, loop_thread = create_and_start_event_loop()
|
||||
fut = asyncio.run_coroutine_threadsafe(coro, loop)
|
||||
try:
|
||||
return fut.result(timeout)
|
||||
finally:
|
||||
loop.call_soon_threadsafe(stop_loop.set_result, 1)
|
||||
loop_thread.join(timeout=1)
|
||||
def get_local_height(self):
|
||||
return 0
|
||||
def blockchain(self):
|
||||
class BlockchainMock:
|
||||
def is_tip_stale(self):
|
||||
return True
|
||||
return BlockchainMock()
|
||||
|
||||
wallet = self.create_standard_wallet_from_seed('mix total present junior leader live state athlete mistake crack wall valve',
|
||||
config=config)
|
||||
wallet.network = NetworkMock()
|
||||
|
||||
# bootstrap wallet
|
||||
funding_tx = Transaction('02000000000101a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540100000000fdffffff0220a1070000000000160014db44724ac632ae47ee5765954d64796dd5fec72708de3c000000000016001424b32aadb42a89016c4de8f11741c3b29b15f21c02473044022045cc6c1cc875cbb0c0d8fe323dc1de9716e49ed5659741b0fb3dd9a196894066022077c242640071d12ec5763c5870f482a4823d8713e4bd14353dd621ed29a7f96d012102aea8d439a0f79d8b58e8d7bda83009f587e1f3da350adaa484329bf47cd03465fef61c00')
|
||||
funding_txid = funding_tx.txid()
|
||||
self.assertEqual('08557327673db61cc921e1a30826608599b86457836be3021105c13940d9a9a3', funding_txid)
|
||||
wallet.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
|
||||
|
||||
orig_rbf_tx = Transaction('02000000000102a3a9d94039c1051102e36b835764b89985602608a3e121c91cb63d67277355080000000000fdfffffffd57af9ecf29b1cb42cb91087cf0d1d9fce59a3ca0b25bbfa7d27c07f99870590200000000fdffffff03b2a00700000000001600145dc80fd43eb70fd21a6c4446e3ce043df94f100cb2a00700000000001600147db4ab480b7d2218fba561ff304178f4afcbc972be358900000000001600149d91f0053172fab394d277ae27e9fa5c5a49210902473044022003999f03be8b9e299b2cd3bc7bce05e273d5d9ce24fc47af8754f26a7a13e13f022004e668499a67061789f6ebd2932c969ece74417ae3f2307bf696428bbed4fe36012102a1c9b25b37aa31ccbb2d72caaffce81ec8253020a74017d92bbfc14a832fc9cb0247304402207121358a66c0e716e2ba2be928076736261c691b4fbf89ea8d255449a4f5837b022042cadf9fe1b4f3c03ede3cef6783b42f0ba319f2e0273b624009cd023488c4c1012103a5ba95fb1e0043428ed70680fc17db254b3f701dfccf91e48090aa17c1b7ea40fef61c00')
|
||||
orig_rbf_txid = orig_rbf_tx.txid()
|
||||
self.assertEqual('6057690010ddac93a371629e1f41866400623e13a9cd336d280fc3239086a983', orig_rbf_txid)
|
||||
wallet.receive_tx_callback(orig_rbf_txid, orig_rbf_tx, TX_HEIGHT_UNCONFIRMED)
|
||||
|
||||
# bump tx
|
||||
tx = wallet.bump_fee(tx=tx_from_any(orig_rbf_tx.serialize()), new_fee_rate=70)
|
||||
tx.locktime = 1898268
|
||||
tx.version = 2
|
||||
if simulate_moving_txs:
|
||||
partial_tx = tx.serialize_as_bytes().hex()
|
||||
self.assertEqual("70736274ff0100b90200000002a3a9d94039c1051102e36b835764b89985602608a3e121c91cb63d67277355080000000000fdfffffffd57af9ecf29b1cb42cb91087cf0d1d9fce59a3ca0b25bbfa7d27c07f99870590200000000fdffffff031660070000000000160014a36590fb127d05cf17a07a84a17f2f2d6cc90a7bb2a00700000000001600147db4ab480b7d2218fba561ff304178f4afcbc972be358900000000001600149d91f0053172fab394d277ae27e9fa5c5a4921091cf71c00000100de02000000000101a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540100000000fdffffff0220a1070000000000160014db44724ac632ae47ee5765954d64796dd5fec72708de3c000000000016001424b32aadb42a89016c4de8f11741c3b29b15f21c02473044022045cc6c1cc875cbb0c0d8fe323dc1de9716e49ed5659741b0fb3dd9a196894066022077c242640071d12ec5763c5870f482a4823d8713e4bd14353dd621ed29a7f96d012102aea8d439a0f79d8b58e8d7bda83009f587e1f3da350adaa484329bf47cd03465fef61c00220602a1c9b25b37aa31ccbb2d72caaffce81ec8253020a74017d92bbfc14a832fc9cb109c9fff98000000800000000000000000000100fd910102000000000102a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540000000000fdffffffbdeb0175b1c51c96843d1952f7e1c49c1703717d7d020048d4de0a8eed94dad50000000000fdffffff03b2a00700000000001600140cd6c9f8ce0aa73d77fcf7f156c74f5cbec6906bb2a00700000000001600146435504ddc95e6019a90bb7dfc7ca81a88a8633106d790000000000016001444bd3017ee214370abf683abaa7f6204c9f40210024730440220652a04a2a301d9a031a034f3ae48174e204e17acf7bfc27f0dcab14243f73e2202207b29e964c434dfb2c515232d36566a40dccd4dd93ccb7fd15260ecbda10f0d9801210231994e564a0530068d17a9b0f85bec58d1352517a2861ea99e5b3070d2c5dbda02473044022072186473874919019da0e3d92b6e0aa4f88cba448ed5434615e5a3c8e2b7c42a02203ec05cef66960d5bc45d0f3d25675190cf8035b11a05ed4b719fd9c3a894899b012102f5fdca8c4e30ba0a1babf9cf9ebe62519b08aead351c349ed1ffc8316c24f542d7f61c0000220203b1b437d6d3366441e63e387594ffacb80676d7d518971d1d284b775cd7d8c38b109c9fff98000000800100000000000000000000",
|
||||
partial_tx)
|
||||
tx = tx_from_any(partial_tx) # simulates moving partial txn between cosigners
|
||||
self.assertFalse(tx.is_complete())
|
||||
|
||||
wallet.sign_transaction(tx, password=None)
|
||||
self.assertFalse(tx.is_complete())
|
||||
self.assertTrue(tx.is_segwit())
|
||||
tx_copy = tx_from_any(tx.serialize())
|
||||
self.assertEqual('70736274ff0100b90200000002a3a9d94039c1051102e36b835764b89985602608a3e121c91cb63d67277355080000000000fdfffffffd57af9ecf29b1cb42cb91087cf0d1d9fce59a3ca0b25bbfa7d27c07f99870590200000000fdffffff031660070000000000160014a36590fb127d05cf17a07a84a17f2f2d6cc90a7bb2a00700000000001600147db4ab480b7d2218fba561ff304178f4afcbc972be358900000000001600149d91f0053172fab394d277ae27e9fa5c5a4921091cf71c00000100de02000000000101a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540100000000fdffffff0220a1070000000000160014db44724ac632ae47ee5765954d64796dd5fec72708de3c000000000016001424b32aadb42a89016c4de8f11741c3b29b15f21c02473044022045cc6c1cc875cbb0c0d8fe323dc1de9716e49ed5659741b0fb3dd9a196894066022077c242640071d12ec5763c5870f482a4823d8713e4bd14353dd621ed29a7f96d012102aea8d439a0f79d8b58e8d7bda83009f587e1f3da350adaa484329bf47cd03465fef61c0001070001086b0247304402201f5ea643f6bc59c96ab8f1a3935b455e8f9395a67b74d618d121d16ae76f7b440220574d05df88740f915798e7993158c08e544801a044d19ef140574da19c1937d7012102a1c9b25b37aa31ccbb2d72caaffce81ec8253020a74017d92bbfc14a832fc9cb000100fd910102000000000102a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540000000000fdffffffbdeb0175b1c51c96843d1952f7e1c49c1703717d7d020048d4de0a8eed94dad50000000000fdffffff03b2a00700000000001600140cd6c9f8ce0aa73d77fcf7f156c74f5cbec6906bb2a00700000000001600146435504ddc95e6019a90bb7dfc7ca81a88a8633106d790000000000016001444bd3017ee214370abf683abaa7f6204c9f40210024730440220652a04a2a301d9a031a034f3ae48174e204e17acf7bfc27f0dcab14243f73e2202207b29e964c434dfb2c515232d36566a40dccd4dd93ccb7fd15260ecbda10f0d9801210231994e564a0530068d17a9b0f85bec58d1352517a2861ea99e5b3070d2c5dbda02473044022072186473874919019da0e3d92b6e0aa4f88cba448ed5434615e5a3c8e2b7c42a02203ec05cef66960d5bc45d0f3d25675190cf8035b11a05ed4b719fd9c3a894899b012102f5fdca8c4e30ba0a1babf9cf9ebe62519b08aead351c349ed1ffc8316c24f542d7f61c0000220203b1b437d6d3366441e63e387594ffacb80676d7d518971d1d284b775cd7d8c38b109c9fff98000000800100000000000000000000',
|
||||
tx_copy.serialize_as_bytes().hex())
|
||||
self.assertEqual('6a8ed07cd97a10ace851b67a65035f04ff477d67cde62bb8679007e87b214e79', tx_copy.txid())
|
||||
|
||||
def _bump_fee_when_not_all_inputs_are_ismine_subcase_all_outputs_are_ismine(self, *, simulate_moving_txs, config):
|
||||
class NetworkMock:
|
||||
relay_fee = 1000
|
||||
async def get_transaction(self, txid, timeout=None):
|
||||
if txid == "08557327673db61cc921e1a30826608599b86457836be3021105c13940d9a9a3":
|
||||
return "02000000000101a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540100000000fdffffff0220a1070000000000160014db44724ac632ae47ee5765954d64796dd5fec72708de3c000000000016001424b32aadb42a89016c4de8f11741c3b29b15f21c02473044022045cc6c1cc875cbb0c0d8fe323dc1de9716e49ed5659741b0fb3dd9a196894066022077c242640071d12ec5763c5870f482a4823d8713e4bd14353dd621ed29a7f96d012102aea8d439a0f79d8b58e8d7bda83009f587e1f3da350adaa484329bf47cd03465fef61c00"
|
||||
else:
|
||||
raise Exception("unexpected txid")
|
||||
def has_internet_connection(self):
|
||||
return True
|
||||
def run_from_another_thread(self, coro, *, timeout=None):
|
||||
loop, stop_loop, loop_thread = create_and_start_event_loop()
|
||||
fut = asyncio.run_coroutine_threadsafe(coro, loop)
|
||||
try:
|
||||
return fut.result(timeout)
|
||||
finally:
|
||||
loop.call_soon_threadsafe(stop_loop.set_result, 1)
|
||||
loop_thread.join(timeout=1)
|
||||
def get_local_height(self):
|
||||
return 0
|
||||
def blockchain(self):
|
||||
class BlockchainMock:
|
||||
def is_tip_stale(self):
|
||||
return True
|
||||
return BlockchainMock()
|
||||
|
||||
wallet = self.create_standard_wallet_from_seed(
|
||||
'faint orbit extend hope moon head mercy still debate sick cotton path',
|
||||
config=config,
|
||||
gap_limit=4,
|
||||
)
|
||||
wallet.network = NetworkMock()
|
||||
|
||||
# bootstrap wallet
|
||||
funding_tx = Transaction('02000000000102c247447533b530cacc3e716aae84621857f04a483252374cbdccfdf8b4ef816b0000000000fdffffffc247447533b530cacc3e716aae84621857f04a483252374cbdccfdf8b4ef816b0100000000fdffffff01d63f0f00000000001600141ef4658adb12ec745a1a1fef6ab8897f04bade060247304402201dc5be86749d8ce33571a6f1a2f8bbfceba89b9dbf2b4683e66c8c17cf7df6090220729199516cb894569ebbe3e998d47fc74030231ed30f110c9babd8a9dc361115012102728251a5f5f55375eef3c14fe59ab0755ba4d5f388619895238033ac9b51aad20247304402202e5d416489c20810e96e931b98a84b0c0c4fc32d2d34d3470b7ee16810246a4c022040f86cf8030d2117d6487bbe6e23d68d6d70408b002d8055de1f33d038d3a0550121039c009e7e7dad07e74ec5a8ac9f9e3499420dd9fe9709995525c714170152512620f71c00')
|
||||
funding_txid = funding_tx.txid()
|
||||
self.assertEqual('59ff0dd3962db651444d9fa6a61311302e47158533714d006e7e024ce45777da', funding_txid)
|
||||
wallet.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
|
||||
|
||||
orig_rbf_tx = Transaction('02000000000102a3a9d94039c1051102e36b835764b89985602608a3e121c91cb63d67277355080000000000fdffffffda7757e44c027e6e004d71338515472e301113a6a69f4d4451b62d96d30dff590000000000fdffffff02b2a00700000000001600144710cfecc31828d31e68ad101dd022fe091a02b1683f0f00000000001600145fd89e3ff2f32c48d85ac65edb4fdf40112ffdfb02473044022032a64a01b0975b65b0adfee53baa6dfb2ca9917714ae3f3acbe609397cc4912d02207da348511a156f6b6eab9d4c762a421e629784108c61d128ad9409483c1e4819012102a1c9b25b37aa31ccbb2d72caaffce81ec8253020a74017d92bbfc14a832fc9cb024730440220620795910e9d96680a2d869024fc5048cb80d038e60a5b92850de65eb938a49c02201a550737b18eda5f93ce3ce0c5907d7b0a9856bbc3bb81cec14349c5b6c97c08012102999b1062a5acf7071a43fd6f2bd37a4e0f7162182490661949dbeeb7d1b03401eef61c00')
|
||||
orig_rbf_txid = orig_rbf_tx.txid()
|
||||
self.assertEqual('2dcc543035c90c25734c9381096cc2f211ac1c2467e072170bc9e51e4580029b', orig_rbf_txid)
|
||||
wallet.receive_tx_callback(orig_rbf_txid, orig_rbf_tx, TX_HEIGHT_UNCONFIRMED)
|
||||
|
||||
# bump tx
|
||||
tx = wallet.bump_fee(tx=tx_from_any(orig_rbf_tx.serialize()), new_fee_rate=50)
|
||||
tx.locktime = 1898273
|
||||
tx.version = 2
|
||||
if simulate_moving_txs:
|
||||
partial_tx = tx.serialize_as_bytes().hex()
|
||||
self.assertEqual("70736274ff01009a0200000002a3a9d94039c1051102e36b835764b89985602608a3e121c91cb63d67277355080000000000fdffffffda7757e44c027e6e004d71338515472e301113a6a69f4d4451b62d96d30dff590000000000fdffffff02bc780700000000001600144710cfecc31828d31e68ad101dd022fe091a02b1683f0f00000000001600145fd89e3ff2f32c48d85ac65edb4fdf40112ffdfb21f71c00000100de02000000000101a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540100000000fdffffff0220a1070000000000160014db44724ac632ae47ee5765954d64796dd5fec72708de3c000000000016001424b32aadb42a89016c4de8f11741c3b29b15f21c02473044022045cc6c1cc875cbb0c0d8fe323dc1de9716e49ed5659741b0fb3dd9a196894066022077c242640071d12ec5763c5870f482a4823d8713e4bd14353dd621ed29a7f96d012102aea8d439a0f79d8b58e8d7bda83009f587e1f3da350adaa484329bf47cd03465fef61c00000100fd530102000000000102c247447533b530cacc3e716aae84621857f04a483252374cbdccfdf8b4ef816b0000000000fdffffffc247447533b530cacc3e716aae84621857f04a483252374cbdccfdf8b4ef816b0100000000fdffffff01d63f0f00000000001600141ef4658adb12ec745a1a1fef6ab8897f04bade060247304402201dc5be86749d8ce33571a6f1a2f8bbfceba89b9dbf2b4683e66c8c17cf7df6090220729199516cb894569ebbe3e998d47fc74030231ed30f110c9babd8a9dc361115012102728251a5f5f55375eef3c14fe59ab0755ba4d5f388619895238033ac9b51aad20247304402202e5d416489c20810e96e931b98a84b0c0c4fc32d2d34d3470b7ee16810246a4c022040f86cf8030d2117d6487bbe6e23d68d6d70408b002d8055de1f33d038d3a0550121039c009e7e7dad07e74ec5a8ac9f9e3499420dd9fe9709995525c714170152512620f71c00220602999b1062a5acf7071a43fd6f2bd37a4e0f7162182490661949dbeeb7d1b0340110277f031200000080000000000000000000220202519a4072fd8c29362693439f441bd7a45c0d8dea26ce88872a4bca7e5d07cb4510277f03120000008000000000020000000022020314c9b46fce4c6111e4bbe89bb06b3dd29c6cbac586a4914bb18fe8bb7e0a463c10277f031200000080000000000100000000",
|
||||
partial_tx)
|
||||
tx = tx_from_any(partial_tx) # simulates moving partial txn between cosigners
|
||||
self.assertFalse(tx.is_complete())
|
||||
|
||||
wallet.sign_transaction(tx, password=None)
|
||||
self.assertFalse(tx.is_complete())
|
||||
self.assertTrue(tx.is_segwit())
|
||||
tx_copy = tx_from_any(tx.serialize())
|
||||
self.assertEqual('70736274ff01009a0200000002a3a9d94039c1051102e36b835764b89985602608a3e121c91cb63d67277355080000000000fdffffffda7757e44c027e6e004d71338515472e301113a6a69f4d4451b62d96d30dff590000000000fdffffff02bc780700000000001600144710cfecc31828d31e68ad101dd022fe091a02b1683f0f00000000001600145fd89e3ff2f32c48d85ac65edb4fdf40112ffdfb21f71c00000100de02000000000101a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540100000000fdffffff0220a1070000000000160014db44724ac632ae47ee5765954d64796dd5fec72708de3c000000000016001424b32aadb42a89016c4de8f11741c3b29b15f21c02473044022045cc6c1cc875cbb0c0d8fe323dc1de9716e49ed5659741b0fb3dd9a196894066022077c242640071d12ec5763c5870f482a4823d8713e4bd14353dd621ed29a7f96d012102aea8d439a0f79d8b58e8d7bda83009f587e1f3da350adaa484329bf47cd03465fef61c00000100fd530102000000000102c247447533b530cacc3e716aae84621857f04a483252374cbdccfdf8b4ef816b0000000000fdffffffc247447533b530cacc3e716aae84621857f04a483252374cbdccfdf8b4ef816b0100000000fdffffff01d63f0f00000000001600141ef4658adb12ec745a1a1fef6ab8897f04bade060247304402201dc5be86749d8ce33571a6f1a2f8bbfceba89b9dbf2b4683e66c8c17cf7df6090220729199516cb894569ebbe3e998d47fc74030231ed30f110c9babd8a9dc361115012102728251a5f5f55375eef3c14fe59ab0755ba4d5f388619895238033ac9b51aad20247304402202e5d416489c20810e96e931b98a84b0c0c4fc32d2d34d3470b7ee16810246a4c022040f86cf8030d2117d6487bbe6e23d68d6d70408b002d8055de1f33d038d3a0550121039c009e7e7dad07e74ec5a8ac9f9e3499420dd9fe9709995525c714170152512620f71c0001070001086b0247304402206842258bbe37829facadef81fa17eb1c97e6f9a4c66717c0cea37b61c9be804902203d291a2c9e3df57e3422f9b90589c2350f0168867c3320e994258169b8da402b012102999b1062a5acf7071a43fd6f2bd37a4e0f7162182490661949dbeeb7d1b0340100220202519a4072fd8c29362693439f441bd7a45c0d8dea26ce88872a4bca7e5d07cb4510277f03120000008000000000020000000022020314c9b46fce4c6111e4bbe89bb06b3dd29c6cbac586a4914bb18fe8bb7e0a463c10277f031200000080000000000100000000',
|
||||
tx_copy.serialize_as_bytes().hex())
|
||||
self.assertEqual('b46cdce7e7564dfd09618ab9008ec3a921c6372f3dcdab2f6094735b024485f0', tx_copy.txid())
|
||||
|
||||
|
||||
def _bump_fee_p2wpkh_when_there_is_only_a_single_output_and_that_is_a_change_address(self, *, simulate_moving_txs, config):
|
||||
wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage',
|
||||
config=config)
|
||||
@@ -1659,6 +1791,10 @@ class TestWalletSending(TestCaseForTestnet):
|
||||
self._dscancel_when_user_sends_max(
|
||||
simulate_moving_txs=simulate_moving_txs,
|
||||
config=config)
|
||||
with self.subTest(msg="_dscancel_when_not_all_inputs_are_ismine", simulate_moving_txs=simulate_moving_txs):
|
||||
self._dscancel_when_not_all_inputs_are_ismine(
|
||||
simulate_moving_txs=simulate_moving_txs,
|
||||
config=config)
|
||||
|
||||
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',
|
||||
@@ -1836,6 +1972,66 @@ class TestWalletSending(TestCaseForTestnet):
|
||||
wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
|
||||
self.assertEqual((0, 9992300, 0), wallet.get_balance())
|
||||
|
||||
def _dscancel_when_not_all_inputs_are_ismine(self, *, simulate_moving_txs, config):
|
||||
class NetworkMock:
|
||||
relay_fee = 1000
|
||||
async def get_transaction(self, txid, timeout=None):
|
||||
if txid == "597098f9077cd2a7bf5bb2a03c9ae5fcd9d1f07c0891cb42cbb129cf9eaf57fd":
|
||||
return "02000000000102a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540000000000fdffffffbdeb0175b1c51c96843d1952f7e1c49c1703717d7d020048d4de0a8eed94dad50000000000fdffffff03b2a00700000000001600140cd6c9f8ce0aa73d77fcf7f156c74f5cbec6906bb2a00700000000001600146435504ddc95e6019a90bb7dfc7ca81a88a8633106d790000000000016001444bd3017ee214370abf683abaa7f6204c9f40210024730440220652a04a2a301d9a031a034f3ae48174e204e17acf7bfc27f0dcab14243f73e2202207b29e964c434dfb2c515232d36566a40dccd4dd93ccb7fd15260ecbda10f0d9801210231994e564a0530068d17a9b0f85bec58d1352517a2861ea99e5b3070d2c5dbda02473044022072186473874919019da0e3d92b6e0aa4f88cba448ed5434615e5a3c8e2b7c42a02203ec05cef66960d5bc45d0f3d25675190cf8035b11a05ed4b719fd9c3a894899b012102f5fdca8c4e30ba0a1babf9cf9ebe62519b08aead351c349ed1ffc8316c24f542d7f61c00"
|
||||
else:
|
||||
raise Exception("unexpected txid")
|
||||
def has_internet_connection(self):
|
||||
return True
|
||||
def run_from_another_thread(self, coro, *, timeout=None):
|
||||
loop, stop_loop, loop_thread = create_and_start_event_loop()
|
||||
fut = asyncio.run_coroutine_threadsafe(coro, loop)
|
||||
try:
|
||||
return fut.result(timeout)
|
||||
finally:
|
||||
loop.call_soon_threadsafe(stop_loop.set_result, 1)
|
||||
loop_thread.join(timeout=1)
|
||||
def get_local_height(self):
|
||||
return 0
|
||||
def blockchain(self):
|
||||
class BlockchainMock:
|
||||
def is_tip_stale(self):
|
||||
return True
|
||||
return BlockchainMock()
|
||||
|
||||
wallet = self.create_standard_wallet_from_seed('mix total present junior leader live state athlete mistake crack wall valve',
|
||||
config=config)
|
||||
wallet.network = NetworkMock()
|
||||
|
||||
# bootstrap wallet
|
||||
funding_tx = Transaction('02000000000101a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540100000000fdffffff0220a1070000000000160014db44724ac632ae47ee5765954d64796dd5fec72708de3c000000000016001424b32aadb42a89016c4de8f11741c3b29b15f21c02473044022045cc6c1cc875cbb0c0d8fe323dc1de9716e49ed5659741b0fb3dd9a196894066022077c242640071d12ec5763c5870f482a4823d8713e4bd14353dd621ed29a7f96d012102aea8d439a0f79d8b58e8d7bda83009f587e1f3da350adaa484329bf47cd03465fef61c00')
|
||||
funding_txid = funding_tx.txid()
|
||||
self.assertEqual('08557327673db61cc921e1a30826608599b86457836be3021105c13940d9a9a3', funding_txid)
|
||||
wallet.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
|
||||
|
||||
orig_rbf_tx = Transaction('02000000000102a3a9d94039c1051102e36b835764b89985602608a3e121c91cb63d67277355080000000000fdfffffffd57af9ecf29b1cb42cb91087cf0d1d9fce59a3ca0b25bbfa7d27c07f99870590200000000fdffffff03b2a00700000000001600145dc80fd43eb70fd21a6c4446e3ce043df94f100cb2a00700000000001600147db4ab480b7d2218fba561ff304178f4afcbc972be358900000000001600149d91f0053172fab394d277ae27e9fa5c5a49210902473044022003999f03be8b9e299b2cd3bc7bce05e273d5d9ce24fc47af8754f26a7a13e13f022004e668499a67061789f6ebd2932c969ece74417ae3f2307bf696428bbed4fe36012102a1c9b25b37aa31ccbb2d72caaffce81ec8253020a74017d92bbfc14a832fc9cb0247304402207121358a66c0e716e2ba2be928076736261c691b4fbf89ea8d255449a4f5837b022042cadf9fe1b4f3c03ede3cef6783b42f0ba319f2e0273b624009cd023488c4c1012103a5ba95fb1e0043428ed70680fc17db254b3f701dfccf91e48090aa17c1b7ea40fef61c00')
|
||||
orig_rbf_txid = orig_rbf_tx.txid()
|
||||
self.assertEqual('6057690010ddac93a371629e1f41866400623e13a9cd336d280fc3239086a983', orig_rbf_txid)
|
||||
wallet.receive_tx_callback(orig_rbf_txid, orig_rbf_tx, TX_HEIGHT_UNCONFIRMED)
|
||||
|
||||
# bump tx
|
||||
tx = wallet.dscancel(tx=tx_from_any(orig_rbf_tx.serialize()), new_fee_rate=70)
|
||||
tx.locktime = 1898278
|
||||
tx.version = 2
|
||||
if simulate_moving_txs:
|
||||
partial_tx = tx.serialize_as_bytes().hex()
|
||||
self.assertEqual("70736274ff0100520200000001a3a9d94039c1051102e36b835764b89985602608a3e121c91cb63d67277355080000000000fdffffff010c830700000000001600145dc80fd43eb70fd21a6c4446e3ce043df94f100c26f71c00000100de02000000000101a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540100000000fdffffff0220a1070000000000160014db44724ac632ae47ee5765954d64796dd5fec72708de3c000000000016001424b32aadb42a89016c4de8f11741c3b29b15f21c02473044022045cc6c1cc875cbb0c0d8fe323dc1de9716e49ed5659741b0fb3dd9a196894066022077c242640071d12ec5763c5870f482a4823d8713e4bd14353dd621ed29a7f96d012102aea8d439a0f79d8b58e8d7bda83009f587e1f3da350adaa484329bf47cd03465fef61c00220602a1c9b25b37aa31ccbb2d72caaffce81ec8253020a74017d92bbfc14a832fc9cb109c9fff980000008000000000000000000022020353becea8bbfe746452e5d2fa2e0688013e43ca6409c8e30b6cc99e7625ff2265109c9fff9800000080000000000100000000",
|
||||
partial_tx)
|
||||
tx = tx_from_any(partial_tx) # simulates moving partial txn between cosigners
|
||||
self.assertFalse(tx.is_complete())
|
||||
|
||||
wallet.sign_transaction(tx, password=None)
|
||||
self.assertTrue(tx.is_complete())
|
||||
self.assertTrue(tx.is_segwit())
|
||||
tx_copy = tx_from_any(tx.serialize())
|
||||
self.assertEqual('02000000000101a3a9d94039c1051102e36b835764b89985602608a3e121c91cb63d67277355080000000000fdffffff010c830700000000001600145dc80fd43eb70fd21a6c4446e3ce043df94f100c0247304402202e75e1edceb8ce27d75814bc7895bc48a0d5c423b492b980b655908612485cc8022072a947c4516ab220d0825634efd8b1ad3a5503e63ed8fbb97700b5d73786c63f012102a1c9b25b37aa31ccbb2d72caaffce81ec8253020a74017d92bbfc14a832fc9cb26f71c00',
|
||||
str(tx_copy))
|
||||
self.assertEqual('3021a4fe24e33af9d0ccdf25c478387c97df671fe1fd8b4db0de4255b3a348c5', tx_copy.txid())
|
||||
|
||||
|
||||
class TestWalletOfflineSigning(TestCaseForTestnet):
|
||||
|
||||
|
||||
+17
-5
@@ -842,7 +842,7 @@ class Transaction:
|
||||
return None
|
||||
return bh2u(sha256d(bfh(ser))[::-1])
|
||||
|
||||
def add_info_from_wallet(self, wallet: 'Abstract_Wallet') -> None:
|
||||
def add_info_from_wallet(self, wallet: 'Abstract_Wallet', **kwargs) -> None:
|
||||
return # no-op
|
||||
|
||||
def is_final(self):
|
||||
@@ -1922,8 +1922,13 @@ class PartialTransaction(Transaction):
|
||||
txin.witness = None
|
||||
self.invalidate_ser_cache()
|
||||
|
||||
def add_info_from_wallet(self, wallet: 'Abstract_Wallet', *,
|
||||
include_xpubs: bool = False) -> None:
|
||||
def add_info_from_wallet(
|
||||
self,
|
||||
wallet: 'Abstract_Wallet',
|
||||
*,
|
||||
include_xpubs: bool = False,
|
||||
ignore_network_issues: bool = True,
|
||||
) -> None:
|
||||
if self.is_complete():
|
||||
return
|
||||
# only include xpubs for multisig wallets; currently only they need it in practice
|
||||
@@ -1941,9 +1946,16 @@ class PartialTransaction(Transaction):
|
||||
bip32node = BIP32Node.from_xkey(xpub)
|
||||
self.xpubs[bip32node] = (fp_bytes, der_full)
|
||||
for txin in self.inputs():
|
||||
wallet.add_input_info(txin, only_der_suffix=False)
|
||||
wallet.add_input_info(
|
||||
txin,
|
||||
only_der_suffix=False,
|
||||
ignore_network_issues=ignore_network_issues,
|
||||
)
|
||||
for txout in self.outputs():
|
||||
wallet.add_output_info(txout, only_der_suffix=False)
|
||||
wallet.add_output_info(
|
||||
txout,
|
||||
only_der_suffix=False,
|
||||
)
|
||||
|
||||
def remove_xpubs_and_bip32_paths(self) -> None:
|
||||
self.xpubs.clear()
|
||||
|
||||
+80
-30
@@ -1354,21 +1354,38 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
||||
max_conf = max(max_conf, tx_age)
|
||||
return max_conf >= req_conf
|
||||
|
||||
def bump_fee(self, *, tx: Transaction, new_fee_rate: Union[int, float, Decimal],
|
||||
coins: Sequence[PartialTxInput] = None) -> PartialTransaction:
|
||||
def bump_fee(
|
||||
self,
|
||||
*,
|
||||
tx: Transaction,
|
||||
txid: str = None,
|
||||
new_fee_rate: Union[int, float, Decimal],
|
||||
coins: Sequence[PartialTxInput] = None,
|
||||
) -> PartialTransaction:
|
||||
"""Increase the miner fee of 'tx'.
|
||||
'new_fee_rate' is the target min rate in sat/vbyte
|
||||
'coins' is a list of UTXOs we can choose from as potential new inputs to be added
|
||||
"""
|
||||
txid = txid or tx.txid()
|
||||
assert txid
|
||||
assert tx.txid() in (None, txid)
|
||||
if not isinstance(tx, PartialTransaction):
|
||||
tx = PartialTransaction.from_tx(tx)
|
||||
assert isinstance(tx, PartialTransaction)
|
||||
|
||||
if tx.is_final():
|
||||
raise CannotBumpFee(_('Cannot bump fee') + ': ' + _('transaction is final'))
|
||||
new_fee_rate = quantize_feerate(new_fee_rate) # strip excess precision
|
||||
old_tx_size = tx.estimated_size()
|
||||
old_txid = tx.txid()
|
||||
assert old_txid
|
||||
old_fee = self.get_tx_fee(old_txid)
|
||||
if old_fee is None:
|
||||
raise CannotBumpFee(_('Cannot bump fee') + ': ' + _('current fee unknown'))
|
||||
|
||||
try:
|
||||
# note: this might download input utxos over network
|
||||
tx.add_info_from_wallet(self, ignore_network_issues=False)
|
||||
except NetworkException as e:
|
||||
raise CannotBumpFee(_('Cannot bump fee') + ': ' + repr(e))
|
||||
|
||||
old_fee = tx.get_fee()
|
||||
assert old_fee is not None
|
||||
old_fee_rate = old_fee / old_tx_size # sat/vbyte
|
||||
if new_fee_rate <= old_fee_rate:
|
||||
raise CannotBumpFee(_('Cannot bump fee') + ': ' + _("The new fee rate needs to be higher than the old fee rate."))
|
||||
@@ -1377,7 +1394,11 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
||||
# method 1: keep all inputs, keep all not is_mine outputs,
|
||||
# allow adding new inputs
|
||||
tx_new = self._bump_fee_through_coinchooser(
|
||||
tx=tx, new_fee_rate=new_fee_rate, coins=coins)
|
||||
tx=tx,
|
||||
txid=txid,
|
||||
new_fee_rate=new_fee_rate,
|
||||
coins=coins,
|
||||
)
|
||||
method_used = 1
|
||||
except CannotBumpFee:
|
||||
# method 2: keep all inputs, no new inputs are added,
|
||||
@@ -1399,12 +1420,18 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
||||
tx_new.add_info_from_wallet(self)
|
||||
return tx_new
|
||||
|
||||
def _bump_fee_through_coinchooser(self, *, tx: Transaction, new_fee_rate: Union[int, Decimal],
|
||||
coins: Sequence[PartialTxInput] = None) -> PartialTransaction:
|
||||
old_txid = tx.txid()
|
||||
assert old_txid
|
||||
tx = PartialTransaction.from_tx(tx)
|
||||
def _bump_fee_through_coinchooser(
|
||||
self,
|
||||
*,
|
||||
tx: PartialTransaction,
|
||||
txid: str,
|
||||
new_fee_rate: Union[int, Decimal],
|
||||
coins: Sequence[PartialTxInput] = None,
|
||||
) -> PartialTransaction:
|
||||
assert txid
|
||||
tx = copy.deepcopy(tx)
|
||||
tx.add_info_from_wallet(self)
|
||||
assert tx.get_fee() is not None
|
||||
old_inputs = list(tx.inputs())
|
||||
old_outputs = list(tx.outputs())
|
||||
# change address
|
||||
@@ -1430,7 +1457,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
||||
if coins is None:
|
||||
coins = self.get_spendable_coins(None)
|
||||
# make sure we don't try to spend output from the tx-to-be-replaced:
|
||||
coins = [c for c in coins if c.prevout.txid.hex() != old_txid]
|
||||
coins = [c for c in coins if c.prevout.txid.hex() != txid]
|
||||
for item in coins:
|
||||
self.add_input_info(item)
|
||||
def fee_estimator(size):
|
||||
@@ -1446,10 +1473,15 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
||||
except NotEnoughFunds as e:
|
||||
raise CannotBumpFee(e)
|
||||
|
||||
def _bump_fee_through_decreasing_outputs(self, *, tx: Transaction,
|
||||
new_fee_rate: Union[int, Decimal]) -> PartialTransaction:
|
||||
tx = PartialTransaction.from_tx(tx)
|
||||
def _bump_fee_through_decreasing_outputs(
|
||||
self,
|
||||
*,
|
||||
tx: PartialTransaction,
|
||||
new_fee_rate: Union[int, Decimal],
|
||||
) -> PartialTransaction:
|
||||
tx = copy.deepcopy(tx)
|
||||
tx.add_info_from_wallet(self)
|
||||
assert tx.get_fee() is not None
|
||||
inputs = tx.inputs()
|
||||
outputs = list(tx.outputs())
|
||||
|
||||
@@ -1516,22 +1548,27 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
||||
its inputs, paying ourselves.
|
||||
'new_fee_rate' is the target min rate in sat/vbyte
|
||||
"""
|
||||
if not isinstance(tx, PartialTransaction):
|
||||
tx = PartialTransaction.from_tx(tx)
|
||||
assert isinstance(tx, PartialTransaction)
|
||||
|
||||
if tx.is_final():
|
||||
raise CannotDoubleSpendTx(_('Cannot cancel transaction') + ': ' + _('transaction is final'))
|
||||
new_fee_rate = quantize_feerate(new_fee_rate) # strip excess precision
|
||||
old_tx_size = tx.estimated_size()
|
||||
old_txid = tx.txid()
|
||||
assert old_txid
|
||||
old_fee = self.get_tx_fee(old_txid)
|
||||
if old_fee is None:
|
||||
raise CannotDoubleSpendTx(_('Cannot cancel transaction') + ': ' + _('current fee unknown'))
|
||||
|
||||
try:
|
||||
# note: this might download input utxos over network
|
||||
tx.add_info_from_wallet(self, ignore_network_issues=False)
|
||||
except NetworkException as e:
|
||||
raise CannotDoubleSpendTx(_('Cannot cancel transaction') + ': ' + repr(e))
|
||||
|
||||
old_fee = tx.get_fee()
|
||||
assert old_fee is not None
|
||||
old_fee_rate = old_fee / old_tx_size # sat/vbyte
|
||||
if new_fee_rate <= old_fee_rate:
|
||||
raise CannotDoubleSpendTx(_('Cannot cancel transaction') + ': ' + _("The new fee rate needs to be higher than the old fee rate."))
|
||||
|
||||
tx = PartialTransaction.from_tx(tx)
|
||||
tx.add_info_from_wallet(self)
|
||||
|
||||
# grab all ismine inputs
|
||||
inputs = [txin for txin in tx.inputs()
|
||||
if self.is_mine(self.get_txin_address(txin))]
|
||||
@@ -1564,10 +1601,14 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
||||
address: str, *, only_der_suffix: bool) -> None:
|
||||
pass # implemented by subclasses
|
||||
|
||||
def _add_input_utxo_info(self, txin: PartialTxInput, address: str) -> None:
|
||||
def _add_input_utxo_info(
|
||||
self,
|
||||
txin: PartialTxInput,
|
||||
*,
|
||||
ignore_network_issues: bool = True,
|
||||
) -> None:
|
||||
if txin.utxo is None:
|
||||
# note: for hw wallets, for legacy inputs, ignore_network_issues used to be False
|
||||
txin.utxo = self.get_input_tx(txin.prevout.txid.hex(), ignore_network_issues=True)
|
||||
txin.utxo = self.get_input_tx(txin.prevout.txid.hex(), ignore_network_issues=ignore_network_issues)
|
||||
txin.ensure_there_is_only_one_utxo()
|
||||
|
||||
def _learn_derivation_path_for_address_from_txinout(self, txinout: Union[PartialTxInput, PartialTxOutput],
|
||||
@@ -1578,7 +1619,15 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
||||
"""
|
||||
return False # implemented by subclasses
|
||||
|
||||
def add_input_info(self, txin: PartialTxInput, *, only_der_suffix: bool = False) -> None:
|
||||
def add_input_info(
|
||||
self,
|
||||
txin: PartialTxInput,
|
||||
*,
|
||||
only_der_suffix: bool = False,
|
||||
ignore_network_issues: bool = True,
|
||||
) -> None:
|
||||
# note: we add input utxos regardless of is_mine
|
||||
self._add_input_utxo_info(txin, ignore_network_issues=ignore_network_issues)
|
||||
address = self.get_txin_address(txin)
|
||||
if not self.is_mine(address):
|
||||
is_mine = self._learn_derivation_path_for_address_from_txinout(txin, address)
|
||||
@@ -1586,7 +1635,6 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
||||
return
|
||||
# set script_type first, as later checks might rely on it:
|
||||
txin.script_type = self.get_txin_type(address)
|
||||
self._add_input_utxo_info(txin, address)
|
||||
txin.num_sig = self.m if isinstance(self, Multisig_Wallet) else 1
|
||||
if txin.redeem_script is None:
|
||||
try:
|
||||
@@ -1636,6 +1684,8 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
||||
raise e
|
||||
else:
|
||||
tx = Transaction(raw_tx)
|
||||
if not tx and not ignore_network_issues:
|
||||
raise NetworkException('failed to get prev tx from network')
|
||||
return tx
|
||||
|
||||
def add_output_info(self, txout: PartialTxOutput, *, only_der_suffix: bool = False) -> None:
|
||||
|
||||
Reference in New Issue
Block a user