Browse Source

wallet: add_input_info to no longer do network requests

- wallet.add_input_info() previously had a fallback to download parent
  prev txs from the network (after a lookup in wallet.db failed).
  wallet.add_input_info() is not async, so the network request cannot
  be done cleanly there and was really just a hack.
- tx.add_info_from_wallet() calls wallet.add_input_info() on each txin,
  in which case these network requests were done sequentially, not concurrently
- the network part of wallet.add_input_info() is now split out into new method:
  txin.add_info_from_network()
- in addition to tx.add_info_from_wallet(), there is now also tx.add_info_from_network()
  - callers of old tx.add_info_from_wallet() should now called either
    - tx.add_info_from_wallet(), then tx.add_info_from_network(), preferably in that order
    - tx.add_info_from_wallet() alone is sufficient if the tx is complete,
      or typically when not in a signing context
- callers of wallet.bump_fee and wallet.dscancel are now expected to have already
  called tx.add_info_from_network(), as it cannot be done in a non-async context
  (but for the common case of all-inputs-are-ismine, bump_fee/dscancel should work regardless)
- PartialTxInput.utxo was moved to the baseclass, TxInput.utxo
master
SomberNight 3 years ago
parent
commit
81772faf6c
No known key found for this signature in database
GPG Key ID: B33B5F232C6271E9
  1. 1
      electrum/address_synchronizer.py
  2. 2
      electrum/commands.py
  3. 27
      electrum/gui/kivy/uix/dialogs/tx_dialog.py
  4. 10
      electrum/gui/qml/qetransactionlistmodel.py
  5. 12
      electrum/gui/qml/qetxdetails.py
  6. 40
      electrum/gui/qml/qetxfinalizer.py
  7. 20
      electrum/gui/qt/main_window.py
  8. 11
      electrum/gui/qt/transaction_dialog.py
  9. 93
      electrum/tests/test_wallet_vertical.py
  10. 174
      electrum/transaction.py
  11. 65
      electrum/wallet.py

1
electrum/address_synchronizer.py

@ -138,7 +138,6 @@ class AddressSynchronizer(Logger, EventListener):
return len(self._history_local.get(addr, ())) return len(self._history_local.get(addr, ()))
def get_txin_address(self, txin: TxInput) -> Optional[str]: def get_txin_address(self, txin: TxInput) -> Optional[str]:
if isinstance(txin, PartialTxInput):
if txin.address: if txin.address:
return txin.address return txin.address
prevout_hash = txin.prevout.txid.hex() prevout_hash = txin.prevout.txid.hex()

2
electrum/commands.py

@ -762,6 +762,8 @@ class Commands:
coins = wallet.get_spendable_coins(None) coins = wallet.get_spendable_coins(None)
if domain_coins is not None: if domain_coins is not None:
coins = [coin for coin in coins if (coin.prevout.to_str() in domain_coins)] coins = [coin for coin in coins if (coin.prevout.to_str() in domain_coins)]
tx.add_info_from_wallet(wallet)
await tx.add_info_from_network(self.network)
new_tx = wallet.bump_fee( new_tx = wallet.bump_fee(
tx=tx, tx=tx,
txid=tx.txid(), txid=tx.txid(),

27
electrum/gui/kivy/uix/dialogs/tx_dialog.py

@ -16,7 +16,7 @@ from electrum.util import InvalidPassword
from electrum.address_synchronizer import TX_HEIGHT_LOCAL from electrum.address_synchronizer import TX_HEIGHT_LOCAL
from electrum.wallet import CannotBumpFee, CannotCPFP, CannotDoubleSpendTx from electrum.wallet import CannotBumpFee, CannotCPFP, CannotDoubleSpendTx
from electrum.transaction import Transaction, PartialTransaction from electrum.transaction import Transaction, PartialTransaction
from electrum.network import NetworkException from electrum.network import NetworkException, Network
from electrum.gui.kivy.i18n import _ from electrum.gui.kivy.i18n import _
from electrum.gui.kivy.util import address_colors from electrum.gui.kivy.util import address_colors
@ -120,19 +120,21 @@ Builder.load_string('''
class TxDialog(Factory.Popup): class TxDialog(Factory.Popup):
def __init__(self, app, tx): def __init__(self, app, tx: Transaction):
Factory.Popup.__init__(self) Factory.Popup.__init__(self)
self.app = app # type: ElectrumWindow self.app = app # type: ElectrumWindow
self.wallet = self.app.wallet self.wallet = self.app.wallet
self.tx = tx # type: Transaction self.tx = tx
self.config = self.app.electrum_config self.config = self.app.electrum_config
# If the wallet can populate the inputs with more info, do it now. # If the wallet can populate the inputs with more info, do it now.
# As a result, e.g. we might learn an imported address tx is segwit, # As a result, e.g. we might learn an imported address tx is segwit,
# or that a beyond-gap-limit address is is_mine. # or that a beyond-gap-limit address is is_mine.
# note: this might fetch prev txs over the network. # 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) tx.add_info_from_wallet(self.wallet)
if not tx.is_complete() and tx.is_missing_info_from_network():
Network.run_from_another_thread(
tx.add_info_from_network(self.wallet.network)) # FIXME is this needed?...
def on_open(self): def on_open(self):
self.update() self.update()
@ -201,19 +203,6 @@ class TxDialog(Factory.Popup):
) )
action_dropdown.update(options=options) action_dropdown.update(options=options)
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): def do_rbf(self):
from .bump_fee_dialog import BumpFeeDialog from .bump_fee_dialog import BumpFeeDialog
tx = self.tx tx = self.tx
@ -221,7 +210,7 @@ class TxDialog(Factory.Popup):
assert txid assert txid
if not isinstance(tx, PartialTransaction): if not isinstance(tx, PartialTransaction):
tx = PartialTransaction.from_tx(tx) tx = PartialTransaction.from_tx(tx)
if not self._add_info_to_tx_from_wallet_and_network(tx): if not tx.add_info_from_wallet_and_network(wallet=self.wallet, show_error=self.app.show_error):
return return
fee = tx.get_fee() fee = tx.get_fee()
assert fee is not None assert fee is not None
@ -295,7 +284,7 @@ class TxDialog(Factory.Popup):
assert txid assert txid
if not isinstance(tx, PartialTransaction): if not isinstance(tx, PartialTransaction):
tx = PartialTransaction.from_tx(tx) tx = PartialTransaction.from_tx(tx)
if not self._add_info_to_tx_from_wallet_and_network(tx): if not tx.add_info_from_wallet_and_network(wallet=self.wallet, show_error=self.app.show_error):
return return
fee = tx.get_fee() fee = tx.get_fee()
assert fee is not None assert fee is not None

10
electrum/gui/qml/qetransactionlistmodel.py

@ -1,4 +1,5 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import TYPE_CHECKING
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
from PyQt5.QtCore import Qt, QAbstractListModel, QModelIndex from PyQt5.QtCore import Qt, QAbstractListModel, QModelIndex
@ -9,6 +10,10 @@ from electrum.util import Satoshis, TxMinedInfo
from .qetypes import QEAmount from .qetypes import QEAmount
from .util import QtEventListener, qt_event_listener from .util import QtEventListener, qt_event_listener
if TYPE_CHECKING:
from electrum.wallet import Abstract_Wallet
class QETransactionListModel(QAbstractListModel, QtEventListener): class QETransactionListModel(QAbstractListModel, QtEventListener):
_logger = get_logger(__name__) _logger = get_logger(__name__)
@ -22,7 +27,7 @@ class QETransactionListModel(QAbstractListModel, QtEventListener):
requestRefresh = pyqtSignal() requestRefresh = pyqtSignal()
def __init__(self, wallet, parent=None, *, onchain_domain=None, include_lightning=True): def __init__(self, wallet: 'Abstract_Wallet', parent=None, *, onchain_domain=None, include_lightning=True):
super().__init__(parent) super().__init__(parent)
self.wallet = wallet self.wallet = wallet
self.onchain_domain = onchain_domain self.onchain_domain = onchain_domain
@ -101,7 +106,8 @@ class QETransactionListModel(QAbstractListModel, QtEventListener):
item['balance'] = QEAmount(amount_sat=item['balance'].value) item['balance'] = QEAmount(amount_sat=item['balance'].value)
if 'txid' in item: if 'txid' in item:
tx = self.wallet.get_input_tx(item['txid']) tx = self.wallet.db.get_transaction(item['txid'])
assert tx is not None
item['complete'] = tx.is_complete() item['complete'] = tx.is_complete()
# newly arriving txs, or (partially/fully signed) local txs have no (block) timestamp # newly arriving txs, or (partially/fully signed) local txs have no (block) timestamp

12
electrum/gui/qml/qetxdetails.py

@ -1,9 +1,12 @@
from typing import Optional
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
from electrum.i18n import _ from electrum.i18n import _
from electrum.logging import get_logger from electrum.logging import get_logger
from electrum.util import format_time, AddTransactionException from electrum.util import format_time, AddTransactionException
from electrum.transaction import tx_from_any from electrum.transaction import tx_from_any
from electrum.network import Network
from .qewallet import QEWallet from .qewallet import QEWallet
from .qetypes import QEAmount from .qetypes import QEAmount
@ -23,7 +26,7 @@ class QETxDetails(QObject, QtEventListener):
self.register_callbacks() self.register_callbacks()
self.destroyed.connect(lambda: self.on_destroy()) self.destroyed.connect(lambda: self.on_destroy())
self._wallet = None self._wallet = None # type: Optional[QEWallet]
self._txid = '' self._txid = ''
self._rawtx = '' self._rawtx = ''
self._label = '' self._label = ''
@ -229,13 +232,16 @@ class QETxDetails(QObject, QtEventListener):
return return
if not self._rawtx: if not self._rawtx:
# abusing get_input_tx to get tx from txid self._tx = self._wallet.wallet.db.get_transaction(self._txid)
self._tx = self._wallet.wallet.get_input_tx(self._txid) assert self._tx is not None
#self._logger.debug(repr(self._tx.to_json())) #self._logger.debug(repr(self._tx.to_json()))
self._logger.debug('adding info from wallet') self._logger.debug('adding info from wallet')
self._tx.add_info_from_wallet(self._wallet.wallet) self._tx.add_info_from_wallet(self._wallet.wallet)
if not self._tx.is_complete() and self._tx.is_missing_info_from_network():
Network.run_from_another_thread(
self._tx.add_info_from_network(self._wallet.wallet.network)) # FIXME is this needed?...
self._inputs = list(map(lambda x: x.to_json(), self._tx.inputs())) self._inputs = list(map(lambda x: x.to_json(), self._tx.inputs()))
self._outputs = list(map(lambda x: { self._outputs = list(map(lambda x: {

40
electrum/gui/qml/qetxfinalizer.py

@ -494,7 +494,7 @@ class QETxRbfFeeBumper(TxFeeSlider, TxMonMixin):
def get_tx(self): def get_tx(self):
assert self._txid assert self._txid
self._orig_tx = self._wallet.wallet.get_input_tx(self._txid) self._orig_tx = self._wallet.wallet.db.get_transaction(self._txid)
assert self._orig_tx assert self._orig_tx
if self._wallet.wallet.get_swap_by_funding_tx(self._orig_tx): if self._wallet.wallet.get_swap_by_funding_tx(self._orig_tx):
@ -504,7 +504,7 @@ class QETxRbfFeeBumper(TxFeeSlider, TxMonMixin):
if not isinstance(self._orig_tx, PartialTransaction): if not isinstance(self._orig_tx, PartialTransaction):
self._orig_tx = PartialTransaction.from_tx(self._orig_tx) self._orig_tx = PartialTransaction.from_tx(self._orig_tx)
if not self._add_info_to_tx_from_wallet_and_network(self._orig_tx): if not self._orig_tx.add_info_from_wallet_and_network(wallet=self._wallet.wallet, show_error=self._logger.error):
return return
self.update_from_tx(self._orig_tx) self.update_from_tx(self._orig_tx)
@ -513,21 +513,6 @@ class QETxRbfFeeBumper(TxFeeSlider, TxMonMixin):
self.oldfeeRate = self.feeRate self.oldfeeRate = self.feeRate
self.update() self.update()
# TODO: duplicated from kivy gui, candidate for moving into backend wallet
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.wallet, ignore_network_issues=False)
except NetworkException as e:
# self.app.show_error(repr(e))
self._logger.error(repr(e))
return False
return True
def update(self): def update(self):
if not self._txid: if not self._txid:
# not initialized yet # not initialized yet
@ -616,13 +601,13 @@ class QETxCanceller(TxFeeSlider, TxMonMixin):
def get_tx(self): def get_tx(self):
assert self._txid assert self._txid
self._orig_tx = self._wallet.wallet.get_input_tx(self._txid) self._orig_tx = self._wallet.wallet.db.get_transaction(self._txid)
assert self._orig_tx assert self._orig_tx
if not isinstance(self._orig_tx, PartialTransaction): if not isinstance(self._orig_tx, PartialTransaction):
self._orig_tx = PartialTransaction.from_tx(self._orig_tx) self._orig_tx = PartialTransaction.from_tx(self._orig_tx)
if not self._add_info_to_tx_from_wallet_and_network(self._orig_tx): if not self._orig_tx.add_info_from_wallet_and_network(wallet=self._wallet.wallet, show_error=self._logger.error):
return return
self.update_from_tx(self._orig_tx) self.update_from_tx(self._orig_tx)
@ -631,21 +616,6 @@ class QETxCanceller(TxFeeSlider, TxMonMixin):
self.oldfeeRate = self.feeRate self.oldfeeRate = self.feeRate
self.update() self.update()
# TODO: duplicated from kivy gui, candidate for moving into backend wallet
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.wallet, ignore_network_issues=False)
except NetworkException as e:
# self.app.show_error(repr(e))
self._logger.error(repr(e))
return False
return True
def update(self): def update(self):
if not self._txid: if not self._txid:
# not initialized yet # not initialized yet
@ -757,7 +727,7 @@ class QETxCpfpFeeBumper(TxFeeSlider, TxMonMixin):
def get_tx(self): def get_tx(self):
assert self._txid assert self._txid
self._parent_tx = self._wallet.wallet.get_input_tx(self._txid) self._parent_tx = self._wallet.wallet.db.get_transaction(self._txid)
assert self._parent_tx assert self._parent_tx
if isinstance(self._parent_tx, PartialTransaction): if isinstance(self._parent_tx, PartialTransaction):

20
electrum/gui/qt/main_window.py

@ -2668,27 +2668,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
return return
self.show_transaction(new_tx) 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): def bump_fee_dialog(self, tx: Transaction):
txid = tx.txid() txid = tx.txid()
if not isinstance(tx, PartialTransaction): if not isinstance(tx, PartialTransaction):
tx = PartialTransaction.from_tx(tx) tx = PartialTransaction.from_tx(tx)
if not self._add_info_to_tx_from_wallet_and_network(tx): if not tx.add_info_from_wallet_and_network(wallet=self.wallet, show_error=self.show_error):
return return
d = BumpFeeDialog(main_window=self, tx=tx, txid=txid) d = BumpFeeDialog(main_window=self, tx=tx, txid=txid)
d.run() d.run()
@ -2697,7 +2681,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
txid = tx.txid() txid = tx.txid()
if not isinstance(tx, PartialTransaction): if not isinstance(tx, PartialTransaction):
tx = PartialTransaction.from_tx(tx) tx = PartialTransaction.from_tx(tx)
if not self._add_info_to_tx_from_wallet_and_network(tx): if not tx.add_info_from_wallet_and_network(wallet=self.wallet, show_error=self.show_error):
return return
d = DSCancelDialog(main_window=self, tx=tx, txid=txid) d = DSCancelDialog(main_window=self, tx=tx, txid=txid)
d.run() d.run()

11
electrum/gui/qt/transaction_dialog.py

@ -51,6 +51,7 @@ from electrum import simple_config
from electrum.transaction import SerializationError, Transaction, PartialTransaction, PartialTxInput, TxOutpoint from electrum.transaction import SerializationError, Transaction, PartialTransaction, PartialTxInput, TxOutpoint
from electrum.logging import get_logger from electrum.logging import get_logger
from electrum.util import ShortID from electrum.util import ShortID
from electrum.network import Network
from .util import (MessageBoxMixin, read_QIcon, Buttons, icon_path, from .util import (MessageBoxMixin, read_QIcon, Buttons, icon_path,
MONOSPACE_FONT, ColorScheme, ButtonsLineEdit, ShowQRLineEdit, text_dialog, MONOSPACE_FONT, ColorScheme, ButtonsLineEdit, ShowQRLineEdit, text_dialog,
@ -477,10 +478,13 @@ class TxDialog(QDialog, MessageBoxMixin):
# As a result, e.g. we might learn an imported address tx is segwit, # As a result, e.g. we might learn an imported address tx is segwit,
# or that a beyond-gap-limit address is is_mine. # or that a beyond-gap-limit address is is_mine.
# note: this might fetch prev txs over the network. # note: this might fetch prev txs over the network.
tx.add_info_from_wallet(self.wallet)
# TODO fetch prev txs for any tx; guarded with a config key
if not tx.is_complete() and tx.is_missing_info_from_network():
BlockingWaitingDialog( BlockingWaitingDialog(
self, self,
_("Adding info to tx, from wallet and network..."), _("Adding info to tx, from network..."),
lambda: tx.add_info_from_wallet(self.wallet), lambda: Network.run_from_another_thread(tx.add_info_from_network(self.wallet.network)),
) )
def do_broadcast(self): def do_broadcast(self):
@ -535,7 +539,8 @@ class TxDialog(QDialog, MessageBoxMixin):
if not isinstance(self.tx, PartialTransaction): if not isinstance(self.tx, PartialTransaction):
raise Exception("Can only export partial transactions for hardware device.") raise Exception("Can only export partial transactions for hardware device.")
tx = copy.deepcopy(self.tx) tx = copy.deepcopy(self.tx)
tx.prepare_for_export_for_hardware_device(self.wallet) Network.run_from_another_thread(
tx.prepare_for_export_for_hardware_device(self.wallet))
return tx return tx
def copy_to_clipboard(self, *, tx: Transaction = None): def copy_to_clipboard(self, *, tx: Transaction = None):

93
electrum/tests/test_wallet_vertical.py

@ -1047,61 +1047,61 @@ class TestWalletSending(ElectrumTestCase):
for simulate_moving_txs in (False, True): for simulate_moving_txs in (False, True):
with TmpConfig() as config: with TmpConfig() as config:
with self.subTest(msg="_bump_fee_p2pkh_when_there_is_a_change_address", simulate_moving_txs=simulate_moving_txs): with self.subTest(msg="_bump_fee_p2pkh_when_there_is_a_change_address", simulate_moving_txs=simulate_moving_txs):
self._bump_fee_p2pkh_when_there_is_a_change_address( await self._bump_fee_p2pkh_when_there_is_a_change_address(
simulate_moving_txs=simulate_moving_txs, simulate_moving_txs=simulate_moving_txs,
config=config) config=config)
with TmpConfig() as config: with TmpConfig() as config:
with self.subTest(msg="_bump_fee_p2wpkh_when_there_is_a_change_address", simulate_moving_txs=simulate_moving_txs): with self.subTest(msg="_bump_fee_p2wpkh_when_there_is_a_change_address", simulate_moving_txs=simulate_moving_txs):
self._bump_fee_p2wpkh_when_there_is_a_change_address( await self._bump_fee_p2wpkh_when_there_is_a_change_address(
simulate_moving_txs=simulate_moving_txs, simulate_moving_txs=simulate_moving_txs,
config=config) config=config)
with TmpConfig() as config: with TmpConfig() as config:
with self.subTest(msg="_bump_fee_p2pkh_when_there_are_two_ismine_outs_one_change_one_recv", simulate_moving_txs=simulate_moving_txs): with self.subTest(msg="_bump_fee_p2pkh_when_there_are_two_ismine_outs_one_change_one_recv", simulate_moving_txs=simulate_moving_txs):
self._bump_fee_p2pkh_when_there_are_two_ismine_outs_one_change_one_recv( await self._bump_fee_p2pkh_when_there_are_two_ismine_outs_one_change_one_recv(
simulate_moving_txs=simulate_moving_txs, simulate_moving_txs=simulate_moving_txs,
config=config) config=config)
with TmpConfig() as config: with TmpConfig() as config:
with self.subTest(msg="_bump_fee_when_user_sends_max", simulate_moving_txs=simulate_moving_txs): with self.subTest(msg="_bump_fee_when_user_sends_max", simulate_moving_txs=simulate_moving_txs):
self._bump_fee_when_user_sends_max( await self._bump_fee_when_user_sends_max(
simulate_moving_txs=simulate_moving_txs, simulate_moving_txs=simulate_moving_txs,
config=config) config=config)
with TmpConfig() as config: with TmpConfig() as config:
with self.subTest(msg="_bump_fee_when_new_inputs_need_to_be_added", simulate_moving_txs=simulate_moving_txs): with self.subTest(msg="_bump_fee_when_new_inputs_need_to_be_added", simulate_moving_txs=simulate_moving_txs):
self._bump_fee_when_new_inputs_need_to_be_added( await self._bump_fee_when_new_inputs_need_to_be_added(
simulate_moving_txs=simulate_moving_txs, simulate_moving_txs=simulate_moving_txs,
config=config) config=config)
with TmpConfig() as config: with TmpConfig() as config:
with self.subTest(msg="_bump_fee_p2wpkh_when_there_is_only_a_single_output_and_that_is_a_change_address", simulate_moving_txs=simulate_moving_txs): with self.subTest(msg="_bump_fee_p2wpkh_when_there_is_only_a_single_output_and_that_is_a_change_address", simulate_moving_txs=simulate_moving_txs):
self._bump_fee_p2wpkh_when_there_is_only_a_single_output_and_that_is_a_change_address( await self._bump_fee_p2wpkh_when_there_is_only_a_single_output_and_that_is_a_change_address(
simulate_moving_txs=simulate_moving_txs, simulate_moving_txs=simulate_moving_txs,
config=config) config=config)
with TmpConfig() as config: with TmpConfig() as config:
with self.subTest(msg="_rbf_batching", simulate_moving_txs=simulate_moving_txs): with self.subTest(msg="_rbf_batching", simulate_moving_txs=simulate_moving_txs):
self._rbf_batching( await self._rbf_batching(
simulate_moving_txs=simulate_moving_txs, simulate_moving_txs=simulate_moving_txs,
config=config) config=config)
with TmpConfig() as config: with TmpConfig() as 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): 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( await self._bump_fee_when_not_all_inputs_are_ismine_subcase_some_outputs_are_ismine_but_not_all(
simulate_moving_txs=simulate_moving_txs, simulate_moving_txs=simulate_moving_txs,
config=config) config=config)
with TmpConfig() as config: with TmpConfig() as config:
with self.subTest(msg="_bump_fee_when_not_all_inputs_are_ismine_subcase_all_outputs_are_ismine", simulate_moving_txs=simulate_moving_txs): 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( await self._bump_fee_when_not_all_inputs_are_ismine_subcase_all_outputs_are_ismine(
simulate_moving_txs=simulate_moving_txs, simulate_moving_txs=simulate_moving_txs,
config=config) config=config)
with TmpConfig() as config: with TmpConfig() as config:
with self.subTest(msg="_bump_fee_p2wpkh_decrease_payment", simulate_moving_txs=simulate_moving_txs): with self.subTest(msg="_bump_fee_p2wpkh_decrease_payment", simulate_moving_txs=simulate_moving_txs):
self._bump_fee_p2wpkh_decrease_payment( await self._bump_fee_p2wpkh_decrease_payment(
simulate_moving_txs=simulate_moving_txs, simulate_moving_txs=simulate_moving_txs,
config=config) config=config)
with TmpConfig() as config: with TmpConfig() as config:
with self.subTest(msg="_bump_fee_p2wpkh_decrease_payment_batch", simulate_moving_txs=simulate_moving_txs): with self.subTest(msg="_bump_fee_p2wpkh_decrease_payment_batch", simulate_moving_txs=simulate_moving_txs):
self._bump_fee_p2wpkh_decrease_payment_batch( await self._bump_fee_p2wpkh_decrease_payment_batch(
simulate_moving_txs=simulate_moving_txs, simulate_moving_txs=simulate_moving_txs,
config=config) config=config)
def _bump_fee_p2pkh_when_there_is_a_change_address(self, *, simulate_moving_txs, config): async 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', wallet = self.create_standard_wallet_from_seed('fold object utility erase deputy output stadium feed stereo usage modify bean',
config=config) config=config)
@ -1165,7 +1165,7 @@ class TestWalletSending(ElectrumTestCase):
wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
self.assertEqual((0, 7484320, 0), wallet.get_balance()) self.assertEqual((0, 7484320, 0), wallet.get_balance())
def _bump_fee_p2pkh_when_there_are_two_ismine_outs_one_change_one_recv(self, *, simulate_moving_txs, config): async def _bump_fee_p2pkh_when_there_are_two_ismine_outs_one_change_one_recv(self, *, simulate_moving_txs, config):
"""This tests a regression where sometimes we created a replacement tx """This tests a regression where sometimes we created a replacement tx
that spent from the original (which is clearly invalid). that spent from the original (which is clearly invalid).
""" """
@ -1207,7 +1207,7 @@ class TestWalletSending(ElectrumTestCase):
wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
self.assertEqual((0, 461600, 0), wallet.get_balance()) self.assertEqual((0, 461600, 0), wallet.get_balance())
def _bump_fee_p2wpkh_decrease_payment(self, *, simulate_moving_txs, config): async def _bump_fee_p2wpkh_decrease_payment(self, *, simulate_moving_txs, config):
wallet = self.create_standard_wallet_from_seed('leader company camera enlist crash sleep insane aware anger hole hammer label', wallet = self.create_standard_wallet_from_seed('leader company camera enlist crash sleep insane aware anger hole hammer label',
config=config) config=config)
@ -1249,7 +1249,7 @@ class TestWalletSending(ElectrumTestCase):
wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
self.assertEqual((0, 45000, 0), wallet.get_balance()) self.assertEqual((0, 45000, 0), wallet.get_balance())
def _bump_fee_p2wpkh_decrease_payment_batch(self, *, simulate_moving_txs, config): async def _bump_fee_p2wpkh_decrease_payment_batch(self, *, simulate_moving_txs, config):
wallet = self.create_standard_wallet_from_seed('leader company camera enlist crash sleep insane aware anger hole hammer label', wallet = self.create_standard_wallet_from_seed('leader company camera enlist crash sleep insane aware anger hole hammer label',
config=config) config=config)
@ -1324,7 +1324,7 @@ class TestWalletSending(ElectrumTestCase):
wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
self.assertEqual((0, funding_output_value - 50000, 0), wallet.get_balance()) self.assertEqual((0, funding_output_value - 50000, 0), wallet.get_balance())
def _bump_fee_p2wpkh_when_there_is_a_change_address(self, *, simulate_moving_txs, config): async def _bump_fee_p2wpkh_when_there_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', wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage',
config=config) config=config)
@ -1388,13 +1388,10 @@ class TestWalletSending(ElectrumTestCase):
wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
self.assertEqual((0, 7490060, 0), wallet.get_balance()) 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): async def _bump_fee_when_not_all_inputs_are_ismine_subcase_some_outputs_are_ismine_but_not_all(self, *, simulate_moving_txs, config):
class NetworkMock: class NetworkMock:
relay_fee = 1000 relay_fee = 1000
async def get_transaction(self, txid, timeout=None): async def get_transaction(self, txid, timeout=None):
return self._gettx(txid)
@staticmethod
def _gettx(txid):
if txid == "597098f9077cd2a7bf5bb2a03c9ae5fcd9d1f07c0891cb42cbb129cf9eaf57fd": if txid == "597098f9077cd2a7bf5bb2a03c9ae5fcd9d1f07c0891cb42cbb129cf9eaf57fd":
return "02000000000102a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540000000000fdffffffbdeb0175b1c51c96843d1952f7e1c49c1703717d7d020048d4de0a8eed94dad50000000000fdffffff03b2a00700000000001600140cd6c9f8ce0aa73d77fcf7f156c74f5cbec6906bb2a00700000000001600146435504ddc95e6019a90bb7dfc7ca81a88a8633106d790000000000016001444bd3017ee214370abf683abaa7f6204c9f40210024730440220652a04a2a301d9a031a034f3ae48174e204e17acf7bfc27f0dcab14243f73e2202207b29e964c434dfb2c515232d36566a40dccd4dd93ccb7fd15260ecbda10f0d9801210231994e564a0530068d17a9b0f85bec58d1352517a2861ea99e5b3070d2c5dbda02473044022072186473874919019da0e3d92b6e0aa4f88cba448ed5434615e5a3c8e2b7c42a02203ec05cef66960d5bc45d0f3d25675190cf8035b11a05ed4b719fd9c3a894899b012102f5fdca8c4e30ba0a1babf9cf9ebe62519b08aead351c349ed1ffc8316c24f542d7f61c00" return "02000000000102a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540000000000fdffffffbdeb0175b1c51c96843d1952f7e1c49c1703717d7d020048d4de0a8eed94dad50000000000fdffffff03b2a00700000000001600140cd6c9f8ce0aa73d77fcf7f156c74f5cbec6906bb2a00700000000001600146435504ddc95e6019a90bb7dfc7ca81a88a8633106d790000000000016001444bd3017ee214370abf683abaa7f6204c9f40210024730440220652a04a2a301d9a031a034f3ae48174e204e17acf7bfc27f0dcab14243f73e2202207b29e964c434dfb2c515232d36566a40dccd4dd93ccb7fd15260ecbda10f0d9801210231994e564a0530068d17a9b0f85bec58d1352517a2861ea99e5b3070d2c5dbda02473044022072186473874919019da0e3d92b6e0aa4f88cba448ed5434615e5a3c8e2b7c42a02203ec05cef66960d5bc45d0f3d25675190cf8035b11a05ed4b719fd9c3a894899b012102f5fdca8c4e30ba0a1babf9cf9ebe62519b08aead351c349ed1ffc8316c24f542d7f61c00"
else: else:
@ -1413,7 +1410,6 @@ class TestWalletSending(ElectrumTestCase):
wallet = self.create_standard_wallet_from_seed('mix total present junior leader live state athlete mistake crack wall valve', wallet = self.create_standard_wallet_from_seed('mix total present junior leader live state athlete mistake crack wall valve',
config=config) config=config)
wallet.network = NetworkMock() wallet.network = NetworkMock()
wallet._get_rawtx_from_network = NetworkMock._gettx
# bootstrap wallet # bootstrap wallet
funding_tx = Transaction('02000000000101a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540100000000fdffffff0220a1070000000000160014db44724ac632ae47ee5765954d64796dd5fec72708de3c000000000016001424b32aadb42a89016c4de8f11741c3b29b15f21c02473044022045cc6c1cc875cbb0c0d8fe323dc1de9716e49ed5659741b0fb3dd9a196894066022077c242640071d12ec5763c5870f482a4823d8713e4bd14353dd621ed29a7f96d012102aea8d439a0f79d8b58e8d7bda83009f587e1f3da350adaa484329bf47cd03465fef61c00') funding_tx = Transaction('02000000000101a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540100000000fdffffff0220a1070000000000160014db44724ac632ae47ee5765954d64796dd5fec72708de3c000000000016001424b32aadb42a89016c4de8f11741c3b29b15f21c02473044022045cc6c1cc875cbb0c0d8fe323dc1de9716e49ed5659741b0fb3dd9a196894066022077c242640071d12ec5763c5870f482a4823d8713e4bd14353dd621ed29a7f96d012102aea8d439a0f79d8b58e8d7bda83009f587e1f3da350adaa484329bf47cd03465fef61c00')
@ -1427,7 +1423,10 @@ class TestWalletSending(ElectrumTestCase):
wallet.adb.receive_tx_callback(orig_rbf_txid, orig_rbf_tx, TX_HEIGHT_UNCONFIRMED) wallet.adb.receive_tx_callback(orig_rbf_txid, orig_rbf_tx, TX_HEIGHT_UNCONFIRMED)
# bump tx # bump tx
tx = wallet.bump_fee(tx=tx_from_any(orig_rbf_tx.serialize()), new_fee_rate=70) orig_rbf_tx = tx_from_any(orig_rbf_tx.serialize())
orig_rbf_tx.add_info_from_wallet(wallet=wallet)
await orig_rbf_tx.add_info_from_network(network=wallet.network)
tx = wallet.bump_fee(tx=orig_rbf_tx, new_fee_rate=70)
tx.locktime = 1898268 tx.locktime = 1898268
tx.version = 2 tx.version = 2
if simulate_moving_txs: if simulate_moving_txs:
@ -1445,13 +1444,10 @@ class TestWalletSending(ElectrumTestCase):
tx_copy.serialize_as_bytes().hex()) tx_copy.serialize_as_bytes().hex())
self.assertEqual('6a8ed07cd97a10ace851b67a65035f04ff477d67cde62bb8679007e87b214e79', tx_copy.txid()) 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): async def _bump_fee_when_not_all_inputs_are_ismine_subcase_all_outputs_are_ismine(self, *, simulate_moving_txs, config):
class NetworkMock: class NetworkMock:
relay_fee = 1000 relay_fee = 1000
async def get_transaction(self, txid, timeout=None): async def get_transaction(self, txid, timeout=None):
return self._gettx(txid)
@staticmethod
def _gettx(txid):
if txid == "08557327673db61cc921e1a30826608599b86457836be3021105c13940d9a9a3": if txid == "08557327673db61cc921e1a30826608599b86457836be3021105c13940d9a9a3":
return "02000000000101a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540100000000fdffffff0220a1070000000000160014db44724ac632ae47ee5765954d64796dd5fec72708de3c000000000016001424b32aadb42a89016c4de8f11741c3b29b15f21c02473044022045cc6c1cc875cbb0c0d8fe323dc1de9716e49ed5659741b0fb3dd9a196894066022077c242640071d12ec5763c5870f482a4823d8713e4bd14353dd621ed29a7f96d012102aea8d439a0f79d8b58e8d7bda83009f587e1f3da350adaa484329bf47cd03465fef61c00" return "02000000000101a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540100000000fdffffff0220a1070000000000160014db44724ac632ae47ee5765954d64796dd5fec72708de3c000000000016001424b32aadb42a89016c4de8f11741c3b29b15f21c02473044022045cc6c1cc875cbb0c0d8fe323dc1de9716e49ed5659741b0fb3dd9a196894066022077c242640071d12ec5763c5870f482a4823d8713e4bd14353dd621ed29a7f96d012102aea8d439a0f79d8b58e8d7bda83009f587e1f3da350adaa484329bf47cd03465fef61c00"
else: else:
@ -1473,7 +1469,6 @@ class TestWalletSending(ElectrumTestCase):
gap_limit=4, gap_limit=4,
) )
wallet.network = NetworkMock() wallet.network = NetworkMock()
wallet._get_rawtx_from_network = NetworkMock._gettx
# bootstrap wallet # bootstrap wallet
funding_tx = Transaction('02000000000102c247447533b530cacc3e716aae84621857f04a483252374cbdccfdf8b4ef816b0000000000fdffffffc247447533b530cacc3e716aae84621857f04a483252374cbdccfdf8b4ef816b0100000000fdffffff01d63f0f00000000001600141ef4658adb12ec745a1a1fef6ab8897f04bade060247304402201dc5be86749d8ce33571a6f1a2f8bbfceba89b9dbf2b4683e66c8c17cf7df6090220729199516cb894569ebbe3e998d47fc74030231ed30f110c9babd8a9dc361115012102728251a5f5f55375eef3c14fe59ab0755ba4d5f388619895238033ac9b51aad20247304402202e5d416489c20810e96e931b98a84b0c0c4fc32d2d34d3470b7ee16810246a4c022040f86cf8030d2117d6487bbe6e23d68d6d70408b002d8055de1f33d038d3a0550121039c009e7e7dad07e74ec5a8ac9f9e3499420dd9fe9709995525c714170152512620f71c00') funding_tx = Transaction('02000000000102c247447533b530cacc3e716aae84621857f04a483252374cbdccfdf8b4ef816b0000000000fdffffffc247447533b530cacc3e716aae84621857f04a483252374cbdccfdf8b4ef816b0100000000fdffffff01d63f0f00000000001600141ef4658adb12ec745a1a1fef6ab8897f04bade060247304402201dc5be86749d8ce33571a6f1a2f8bbfceba89b9dbf2b4683e66c8c17cf7df6090220729199516cb894569ebbe3e998d47fc74030231ed30f110c9babd8a9dc361115012102728251a5f5f55375eef3c14fe59ab0755ba4d5f388619895238033ac9b51aad20247304402202e5d416489c20810e96e931b98a84b0c0c4fc32d2d34d3470b7ee16810246a4c022040f86cf8030d2117d6487bbe6e23d68d6d70408b002d8055de1f33d038d3a0550121039c009e7e7dad07e74ec5a8ac9f9e3499420dd9fe9709995525c714170152512620f71c00')
@ -1487,7 +1482,10 @@ class TestWalletSending(ElectrumTestCase):
wallet.adb.receive_tx_callback(orig_rbf_txid, orig_rbf_tx, TX_HEIGHT_UNCONFIRMED) wallet.adb.receive_tx_callback(orig_rbf_txid, orig_rbf_tx, TX_HEIGHT_UNCONFIRMED)
# bump tx # bump tx
tx = wallet.bump_fee(tx=tx_from_any(orig_rbf_tx.serialize()), new_fee_rate=50) orig_rbf_tx = tx_from_any(orig_rbf_tx.serialize())
orig_rbf_tx.add_info_from_wallet(wallet=wallet)
await orig_rbf_tx.add_info_from_network(network=wallet.network)
tx = wallet.bump_fee(tx=orig_rbf_tx, new_fee_rate=50)
tx.locktime = 1898273 tx.locktime = 1898273
tx.version = 2 tx.version = 2
if simulate_moving_txs: if simulate_moving_txs:
@ -1506,7 +1504,7 @@ class TestWalletSending(ElectrumTestCase):
self.assertEqual('b46cdce7e7564dfd09618ab9008ec3a921c6372f3dcdab2f6094735b024485f0', tx_copy.txid()) 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): async 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', wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage',
config=config) config=config)
@ -1568,7 +1566,7 @@ class TestWalletSending(ElectrumTestCase):
wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
self.assertEqual((0, 9991750, 0), wallet.get_balance()) self.assertEqual((0, 9991750, 0), wallet.get_balance())
def _bump_fee_when_user_sends_max(self, *, simulate_moving_txs, config): async def _bump_fee_when_user_sends_max(self, *, simulate_moving_txs, config):
wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage', wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage',
config=config) config=config)
@ -1631,7 +1629,7 @@ class TestWalletSending(ElectrumTestCase):
wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
self.assertEqual((0, 0, 0), wallet.get_balance()) self.assertEqual((0, 0, 0), wallet.get_balance())
def _bump_fee_when_new_inputs_need_to_be_added(self, *, simulate_moving_txs, config): async def _bump_fee_when_new_inputs_need_to_be_added(self, *, simulate_moving_txs, config):
wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage', wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage',
config=config) config=config)
@ -1703,7 +1701,7 @@ class TestWalletSending(ElectrumTestCase):
wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
self.assertEqual((0, 4_990_300, 0), wallet.get_balance()) self.assertEqual((0, 4_990_300, 0), wallet.get_balance())
def _rbf_batching(self, *, simulate_moving_txs, config): async def _rbf_batching(self, *, simulate_moving_txs, config):
wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage', wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage',
config=config) config=config)
wallet.config.set_key('batch_rbf', True) wallet.config.set_key('batch_rbf', True)
@ -2177,23 +2175,23 @@ class TestWalletSending(ElectrumTestCase):
for simulate_moving_txs in (False, True): for simulate_moving_txs in (False, True):
with self.subTest(msg="_dscancel_when_all_outputs_are_ismine", simulate_moving_txs=simulate_moving_txs): with self.subTest(msg="_dscancel_when_all_outputs_are_ismine", simulate_moving_txs=simulate_moving_txs):
self._dscancel_when_all_outputs_are_ismine( await self._dscancel_when_all_outputs_are_ismine(
simulate_moving_txs=simulate_moving_txs, simulate_moving_txs=simulate_moving_txs,
config=config) config=config)
with self.subTest(msg="_dscancel_p2wpkh_when_there_is_a_change_address", simulate_moving_txs=simulate_moving_txs): with self.subTest(msg="_dscancel_p2wpkh_when_there_is_a_change_address", simulate_moving_txs=simulate_moving_txs):
self._dscancel_p2wpkh_when_there_is_a_change_address( await self._dscancel_p2wpkh_when_there_is_a_change_address(
simulate_moving_txs=simulate_moving_txs, simulate_moving_txs=simulate_moving_txs,
config=config) config=config)
with self.subTest(msg="_dscancel_when_user_sends_max", simulate_moving_txs=simulate_moving_txs): with self.subTest(msg="_dscancel_when_user_sends_max", simulate_moving_txs=simulate_moving_txs):
self._dscancel_when_user_sends_max( await self._dscancel_when_user_sends_max(
simulate_moving_txs=simulate_moving_txs, simulate_moving_txs=simulate_moving_txs,
config=config) config=config)
with self.subTest(msg="_dscancel_when_not_all_inputs_are_ismine", simulate_moving_txs=simulate_moving_txs): 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( await self._dscancel_when_not_all_inputs_are_ismine(
simulate_moving_txs=simulate_moving_txs, simulate_moving_txs=simulate_moving_txs,
config=config) config=config)
def _dscancel_when_all_outputs_are_ismine(self, *, simulate_moving_txs, 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', wallet = self.create_standard_wallet_from_seed('fold object utility erase deputy output stadium feed stereo usage modify bean',
config=config) config=config)
@ -2238,7 +2236,7 @@ class TestWalletSending(ElectrumTestCase):
tx_details = wallet.get_tx_info(tx_from_any(tx.serialize())) tx_details = wallet.get_tx_info(tx_from_any(tx.serialize()))
self.assertFalse(tx_details.can_dscancel) self.assertFalse(tx_details.can_dscancel)
def _dscancel_p2wpkh_when_there_is_a_change_address(self, *, simulate_moving_txs, config): async def _dscancel_p2wpkh_when_there_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', wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage',
config=config) config=config)
@ -2304,7 +2302,7 @@ class TestWalletSending(ElectrumTestCase):
wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
self.assertEqual((0, 9992300, 0), wallet.get_balance()) self.assertEqual((0, 9992300, 0), wallet.get_balance())
def _dscancel_when_user_sends_max(self, *, simulate_moving_txs, config): async def _dscancel_when_user_sends_max(self, *, simulate_moving_txs, config):
wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage', wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage',
config=config) config=config)
@ -2369,13 +2367,10 @@ class TestWalletSending(ElectrumTestCase):
wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
self.assertEqual((0, 9992300, 0), wallet.get_balance()) self.assertEqual((0, 9992300, 0), wallet.get_balance())
def _dscancel_when_not_all_inputs_are_ismine(self, *, simulate_moving_txs, config): async def _dscancel_when_not_all_inputs_are_ismine(self, *, simulate_moving_txs, config):
class NetworkMock: class NetworkMock:
relay_fee = 1000 relay_fee = 1000
async def get_transaction(self, txid, timeout=None): async def get_transaction(self, txid, timeout=None):
return self._gettx(txid)
@staticmethod
def _gettx(txid):
if txid == "597098f9077cd2a7bf5bb2a03c9ae5fcd9d1f07c0891cb42cbb129cf9eaf57fd": if txid == "597098f9077cd2a7bf5bb2a03c9ae5fcd9d1f07c0891cb42cbb129cf9eaf57fd":
return "02000000000102a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540000000000fdffffffbdeb0175b1c51c96843d1952f7e1c49c1703717d7d020048d4de0a8eed94dad50000000000fdffffff03b2a00700000000001600140cd6c9f8ce0aa73d77fcf7f156c74f5cbec6906bb2a00700000000001600146435504ddc95e6019a90bb7dfc7ca81a88a8633106d790000000000016001444bd3017ee214370abf683abaa7f6204c9f40210024730440220652a04a2a301d9a031a034f3ae48174e204e17acf7bfc27f0dcab14243f73e2202207b29e964c434dfb2c515232d36566a40dccd4dd93ccb7fd15260ecbda10f0d9801210231994e564a0530068d17a9b0f85bec58d1352517a2861ea99e5b3070d2c5dbda02473044022072186473874919019da0e3d92b6e0aa4f88cba448ed5434615e5a3c8e2b7c42a02203ec05cef66960d5bc45d0f3d25675190cf8035b11a05ed4b719fd9c3a894899b012102f5fdca8c4e30ba0a1babf9cf9ebe62519b08aead351c349ed1ffc8316c24f542d7f61c00" return "02000000000102a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540000000000fdffffffbdeb0175b1c51c96843d1952f7e1c49c1703717d7d020048d4de0a8eed94dad50000000000fdffffff03b2a00700000000001600140cd6c9f8ce0aa73d77fcf7f156c74f5cbec6906bb2a00700000000001600146435504ddc95e6019a90bb7dfc7ca81a88a8633106d790000000000016001444bd3017ee214370abf683abaa7f6204c9f40210024730440220652a04a2a301d9a031a034f3ae48174e204e17acf7bfc27f0dcab14243f73e2202207b29e964c434dfb2c515232d36566a40dccd4dd93ccb7fd15260ecbda10f0d9801210231994e564a0530068d17a9b0f85bec58d1352517a2861ea99e5b3070d2c5dbda02473044022072186473874919019da0e3d92b6e0aa4f88cba448ed5434615e5a3c8e2b7c42a02203ec05cef66960d5bc45d0f3d25675190cf8035b11a05ed4b719fd9c3a894899b012102f5fdca8c4e30ba0a1babf9cf9ebe62519b08aead351c349ed1ffc8316c24f542d7f61c00"
else: else:
@ -2394,7 +2389,6 @@ class TestWalletSending(ElectrumTestCase):
wallet = self.create_standard_wallet_from_seed('mix total present junior leader live state athlete mistake crack wall valve', wallet = self.create_standard_wallet_from_seed('mix total present junior leader live state athlete mistake crack wall valve',
config=config) config=config)
wallet.network = NetworkMock() wallet.network = NetworkMock()
wallet._get_rawtx_from_network = NetworkMock._gettx
# bootstrap wallet # bootstrap wallet
funding_tx = Transaction('02000000000101a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540100000000fdffffff0220a1070000000000160014db44724ac632ae47ee5765954d64796dd5fec72708de3c000000000016001424b32aadb42a89016c4de8f11741c3b29b15f21c02473044022045cc6c1cc875cbb0c0d8fe323dc1de9716e49ed5659741b0fb3dd9a196894066022077c242640071d12ec5763c5870f482a4823d8713e4bd14353dd621ed29a7f96d012102aea8d439a0f79d8b58e8d7bda83009f587e1f3da350adaa484329bf47cd03465fef61c00') funding_tx = Transaction('02000000000101a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540100000000fdffffff0220a1070000000000160014db44724ac632ae47ee5765954d64796dd5fec72708de3c000000000016001424b32aadb42a89016c4de8f11741c3b29b15f21c02473044022045cc6c1cc875cbb0c0d8fe323dc1de9716e49ed5659741b0fb3dd9a196894066022077c242640071d12ec5763c5870f482a4823d8713e4bd14353dd621ed29a7f96d012102aea8d439a0f79d8b58e8d7bda83009f587e1f3da350adaa484329bf47cd03465fef61c00')
@ -2408,7 +2402,10 @@ class TestWalletSending(ElectrumTestCase):
wallet.adb.receive_tx_callback(orig_rbf_txid, orig_rbf_tx, TX_HEIGHT_UNCONFIRMED) wallet.adb.receive_tx_callback(orig_rbf_txid, orig_rbf_tx, TX_HEIGHT_UNCONFIRMED)
# bump tx # bump tx
tx = wallet.dscancel(tx=tx_from_any(orig_rbf_tx.serialize()), new_fee_rate=70) orig_rbf_tx = tx_from_any(orig_rbf_tx.serialize())
orig_rbf_tx.add_info_from_wallet(wallet=wallet)
await orig_rbf_tx.add_info_from_network(network=wallet.network)
tx = wallet.dscancel(tx=orig_rbf_tx, new_fee_rate=70)
tx.locktime = 1898278 tx.locktime = 1898278
tx.version = 2 tx.version = 2
if simulate_moving_txs: if simulate_moving_txs:
@ -2686,7 +2683,7 @@ class TestWalletSending(ElectrumTestCase):
tx.inputs()[0].to_json()['bip32_paths']) tx.inputs()[0].to_json()['bip32_paths'])
self.assertEqual("70736274ff01007d020000000122c3730eb6314cf59e11988c41bfdd73f70cb55b294ec6f2eda828b5c939c0980100000000fdffffff0240e20100000000001600147e45d43294b0ff2b08a5f45232649815e516cff058ab05000000000022002014d2823afee4d75f0f83b91a9d625972df41be222c1373d28e068c3eaae9e00a7b4a24000001012b20a10700000000002200207f50b9d6eb4d899c710d8c48903de33d966ff52445d5a57b5210d02a5dd7e3bf0100fd7e0102000000000102deab5844de4aadc177d992696fda2aa6e4692403633d31a4b4073710594d2fca0000000000fdffffffdeab5844de4aadc177d992696fda2aa6e4692403633d31a4b4073710594d2fca0100000000fdffffff02f49f070000000000160014473b34b7da0aa9f7add803019f649e0729fd39d220a10700000000002200207f50b9d6eb4d899c710d8c48903de33d966ff52445d5a57b5210d02a5dd7e3bf0247304402202a4ec3df7bf2b82505bcd4833eeb32875784b4e93d09ac3cf4a8981dc89a049b02205239bad290877fb810a12538a275d5467f3f6afc88d1e0be3d8f6dc4876e6793012103e48cae7f140e15440f4ad6b3d96cb0deb471bbb45daf527e6eb4d5f6c5e26ec802473044022031028192a8307e52829ad1428941000629de73726306ca71d18c5bcfcb98a4a602205ad0240f7dd6c83686ea257f3146ba595b787d7f68b514569962fd5d3692b07c0121033c8af340bd9abf4a56c7cf7554f52e84a1128e5206ffe5da166ca18a57a260077b4a24000105475221022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc482750492103cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca52ae2206022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc48275049109559fbd10f2700800000000000000000220603cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca0c015148ee000000000000000000000101475221027f7f2eaf9a44316c2cd98b67584d1e71ccaced29a347673f3364efe16f5919e221028d9b8ff374e0f60fbc698c5a494c12d9a31a3ce364b1f81ae4a46f48ae45acdd52ae2202027f7f2eaf9a44316c2cd98b67584d1e71ccaced29a347673f3364efe16f5919e2109559fbd10f27008001000000000000002202028d9b8ff374e0f60fbc698c5a494c12d9a31a3ce364b1f81ae4a46f48ae45acdd0c015148ee010000000000000000", self.assertEqual("70736274ff01007d020000000122c3730eb6314cf59e11988c41bfdd73f70cb55b294ec6f2eda828b5c939c0980100000000fdffffff0240e20100000000001600147e45d43294b0ff2b08a5f45232649815e516cff058ab05000000000022002014d2823afee4d75f0f83b91a9d625972df41be222c1373d28e068c3eaae9e00a7b4a24000001012b20a10700000000002200207f50b9d6eb4d899c710d8c48903de33d966ff52445d5a57b5210d02a5dd7e3bf0100fd7e0102000000000102deab5844de4aadc177d992696fda2aa6e4692403633d31a4b4073710594d2fca0000000000fdffffffdeab5844de4aadc177d992696fda2aa6e4692403633d31a4b4073710594d2fca0100000000fdffffff02f49f070000000000160014473b34b7da0aa9f7add803019f649e0729fd39d220a10700000000002200207f50b9d6eb4d899c710d8c48903de33d966ff52445d5a57b5210d02a5dd7e3bf0247304402202a4ec3df7bf2b82505bcd4833eeb32875784b4e93d09ac3cf4a8981dc89a049b02205239bad290877fb810a12538a275d5467f3f6afc88d1e0be3d8f6dc4876e6793012103e48cae7f140e15440f4ad6b3d96cb0deb471bbb45daf527e6eb4d5f6c5e26ec802473044022031028192a8307e52829ad1428941000629de73726306ca71d18c5bcfcb98a4a602205ad0240f7dd6c83686ea257f3146ba595b787d7f68b514569962fd5d3692b07c0121033c8af340bd9abf4a56c7cf7554f52e84a1128e5206ffe5da166ca18a57a260077b4a24000105475221022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc482750492103cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca52ae2206022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc48275049109559fbd10f2700800000000000000000220603cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca0c015148ee000000000000000000000101475221027f7f2eaf9a44316c2cd98b67584d1e71ccaced29a347673f3364efe16f5919e221028d9b8ff374e0f60fbc698c5a494c12d9a31a3ce364b1f81ae4a46f48ae45acdd52ae2202027f7f2eaf9a44316c2cd98b67584d1e71ccaced29a347673f3364efe16f5919e2109559fbd10f27008001000000000000002202028d9b8ff374e0f60fbc698c5a494c12d9a31a3ce364b1f81ae4a46f48ae45acdd0c015148ee010000000000000000",
tx.serialize_as_bytes().hex()) tx.serialize_as_bytes().hex())
tx.prepare_for_export_for_hardware_device(wallet) await tx.prepare_for_export_for_hardware_device(wallet)
# As the keystores were created from just xpubs, they are missing key origin information # As the keystores were created from just xpubs, they are missing key origin information
# (derivation prefix and root fingerprint). # (derivation prefix and root fingerprint).
# Note that info for ks1 contains the expected bip32 path (m/9999') and fingerprint, but not ks0. # Note that info for ks1 contains the expected bip32 path (m/9999') and fingerprint, but not ks0.
@ -2716,7 +2713,7 @@ class TestWalletSending(ElectrumTestCase):
tx.inputs()[0].to_json()['bip32_paths']) tx.inputs()[0].to_json()['bip32_paths'])
self.assertEqual("70736274ff01007d020000000122c3730eb6314cf59e11988c41bfdd73f70cb55b294ec6f2eda828b5c939c0980100000000fdffffff0240e20100000000001600147e45d43294b0ff2b08a5f45232649815e516cff058ab05000000000022002014d2823afee4d75f0f83b91a9d625972df41be222c1373d28e068c3eaae9e00a7b4a24000001012b20a10700000000002200207f50b9d6eb4d899c710d8c48903de33d966ff52445d5a57b5210d02a5dd7e3bf0100fd7e0102000000000102deab5844de4aadc177d992696fda2aa6e4692403633d31a4b4073710594d2fca0000000000fdffffffdeab5844de4aadc177d992696fda2aa6e4692403633d31a4b4073710594d2fca0100000000fdffffff02f49f070000000000160014473b34b7da0aa9f7add803019f649e0729fd39d220a10700000000002200207f50b9d6eb4d899c710d8c48903de33d966ff52445d5a57b5210d02a5dd7e3bf0247304402202a4ec3df7bf2b82505bcd4833eeb32875784b4e93d09ac3cf4a8981dc89a049b02205239bad290877fb810a12538a275d5467f3f6afc88d1e0be3d8f6dc4876e6793012103e48cae7f140e15440f4ad6b3d96cb0deb471bbb45daf527e6eb4d5f6c5e26ec802473044022031028192a8307e52829ad1428941000629de73726306ca71d18c5bcfcb98a4a602205ad0240f7dd6c83686ea257f3146ba595b787d7f68b514569962fd5d3692b07c0121033c8af340bd9abf4a56c7cf7554f52e84a1128e5206ffe5da166ca18a57a260077b4a24000105475221022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc482750492103cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca52ae2206022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc48275049109559fbd10f2700800000000000000000220603cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca1c30cf1be530000080010000800000008002000080000000000000000000000101475221027f7f2eaf9a44316c2cd98b67584d1e71ccaced29a347673f3364efe16f5919e221028d9b8ff374e0f60fbc698c5a494c12d9a31a3ce364b1f81ae4a46f48ae45acdd52ae2202027f7f2eaf9a44316c2cd98b67584d1e71ccaced29a347673f3364efe16f5919e2109559fbd10f27008001000000000000002202028d9b8ff374e0f60fbc698c5a494c12d9a31a3ce364b1f81ae4a46f48ae45acdd1c30cf1be530000080010000800000008002000080010000000000000000", self.assertEqual("70736274ff01007d020000000122c3730eb6314cf59e11988c41bfdd73f70cb55b294ec6f2eda828b5c939c0980100000000fdffffff0240e20100000000001600147e45d43294b0ff2b08a5f45232649815e516cff058ab05000000000022002014d2823afee4d75f0f83b91a9d625972df41be222c1373d28e068c3eaae9e00a7b4a24000001012b20a10700000000002200207f50b9d6eb4d899c710d8c48903de33d966ff52445d5a57b5210d02a5dd7e3bf0100fd7e0102000000000102deab5844de4aadc177d992696fda2aa6e4692403633d31a4b4073710594d2fca0000000000fdffffffdeab5844de4aadc177d992696fda2aa6e4692403633d31a4b4073710594d2fca0100000000fdffffff02f49f070000000000160014473b34b7da0aa9f7add803019f649e0729fd39d220a10700000000002200207f50b9d6eb4d899c710d8c48903de33d966ff52445d5a57b5210d02a5dd7e3bf0247304402202a4ec3df7bf2b82505bcd4833eeb32875784b4e93d09ac3cf4a8981dc89a049b02205239bad290877fb810a12538a275d5467f3f6afc88d1e0be3d8f6dc4876e6793012103e48cae7f140e15440f4ad6b3d96cb0deb471bbb45daf527e6eb4d5f6c5e26ec802473044022031028192a8307e52829ad1428941000629de73726306ca71d18c5bcfcb98a4a602205ad0240f7dd6c83686ea257f3146ba595b787d7f68b514569962fd5d3692b07c0121033c8af340bd9abf4a56c7cf7554f52e84a1128e5206ffe5da166ca18a57a260077b4a24000105475221022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc482750492103cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca52ae2206022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc48275049109559fbd10f2700800000000000000000220603cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca1c30cf1be530000080010000800000008002000080000000000000000000000101475221027f7f2eaf9a44316c2cd98b67584d1e71ccaced29a347673f3364efe16f5919e221028d9b8ff374e0f60fbc698c5a494c12d9a31a3ce364b1f81ae4a46f48ae45acdd52ae2202027f7f2eaf9a44316c2cd98b67584d1e71ccaced29a347673f3364efe16f5919e2109559fbd10f27008001000000000000002202028d9b8ff374e0f60fbc698c5a494c12d9a31a3ce364b1f81ae4a46f48ae45acdd1c30cf1be530000080010000800000008002000080010000000000000000",
tx.serialize_as_bytes().hex()) tx.serialize_as_bytes().hex())
tx.prepare_for_export_for_hardware_device(wallet) await tx.prepare_for_export_for_hardware_device(wallet)
self.assertEqual( self.assertEqual(
{'tpubDFF7YPCSGHZy55HkQj6HJkXCR8DWbKKXpTYBH38fSHf6VuoEzNmZQZdAoKEVy36S8zXkbGeV4XQU6vaRXGsQfgptFYPR4HSpAenqkY7J7Lg': ('30cf1be5', "m/48h/1h/0h/2h"), {'tpubDFF7YPCSGHZy55HkQj6HJkXCR8DWbKKXpTYBH38fSHf6VuoEzNmZQZdAoKEVy36S8zXkbGeV4XQU6vaRXGsQfgptFYPR4HSpAenqkY7J7Lg': ('30cf1be5', "m/48h/1h/0h/2h"),
'tpubD9MoDeHnEQnU5EMgt9mc4yKU6SURbfq2ooMToY5GH95B8Li1CEsuo9dBKXM2sdjuDGq4KCXLuigss3y22fZULzVrfVuZDxEN55Sp6CcU9DK': ('9559fbd1', "m/9999h")}, 'tpubD9MoDeHnEQnU5EMgt9mc4yKU6SURbfq2ooMToY5GH95B8Li1CEsuo9dBKXM2sdjuDGq4KCXLuigss3y22fZULzVrfVuZDxEN55Sp6CcU9DK': ('9559fbd1', "m/9999h")},
@ -2754,7 +2751,7 @@ class TestWalletSending(ElectrumTestCase):
self.assertEqual("70736274ff0100710200000001916fa04d7080ae0cb19bd08671d37dbe3dc925be383737bb34b3097d82830dc70000000000fdffffff0240e20100000000001600147e45d43294b0ff2b08a5f45232649815e516cff0ceaa05000000000016001456ec9cad206160ab578fa1dfbe13311b3be4a3107f4a24000001011f96a007000000000016001413ce91db66299806c4f35b2b4f8426b0bd4f2cd70100fd2e010200000000010122c3730eb6314cf59e11988c41bfdd73f70cb55b294ec6f2eda828b5c939c0980100000000fdffffff0196a007000000000016001413ce91db66299806c4f35b2b4f8426b0bd4f2cd704004730440220112840ce5486c6b2d15bc3b12e45c2a4518828e1b34f9bb0b3a78220c0cec52f02205b146a1f683289909ecbd3f53932d5acc321444101d8002e435b38a54adbf47201473044022058dfb4c75de119595119f35dcd7b1b2c28c40d7e2e746baeae83f09396c6bb9e02201c3c40fb684253638f12392af3934a90a6c6a512441aac861022f927473c952001475221022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc482750492103cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca52ae4a4a24002206029e65093d22877cbfcc27cb754c58d144ec96635af1fcc63e5a7b90b23bb6acb81830cf1be5540000800100008000000080000000000000000000002202031503b2e74b21d4583b7f0d9e65b2c0ef19fd6e8aae7d0524fc770a1d2b2127501830cf1be5540000800100008000000080010000000000000000", self.assertEqual("70736274ff0100710200000001916fa04d7080ae0cb19bd08671d37dbe3dc925be383737bb34b3097d82830dc70000000000fdffffff0240e20100000000001600147e45d43294b0ff2b08a5f45232649815e516cff0ceaa05000000000016001456ec9cad206160ab578fa1dfbe13311b3be4a3107f4a24000001011f96a007000000000016001413ce91db66299806c4f35b2b4f8426b0bd4f2cd70100fd2e010200000000010122c3730eb6314cf59e11988c41bfdd73f70cb55b294ec6f2eda828b5c939c0980100000000fdffffff0196a007000000000016001413ce91db66299806c4f35b2b4f8426b0bd4f2cd704004730440220112840ce5486c6b2d15bc3b12e45c2a4518828e1b34f9bb0b3a78220c0cec52f02205b146a1f683289909ecbd3f53932d5acc321444101d8002e435b38a54adbf47201473044022058dfb4c75de119595119f35dcd7b1b2c28c40d7e2e746baeae83f09396c6bb9e02201c3c40fb684253638f12392af3934a90a6c6a512441aac861022f927473c952001475221022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc482750492103cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca52ae4a4a24002206029e65093d22877cbfcc27cb754c58d144ec96635af1fcc63e5a7b90b23bb6acb81830cf1be5540000800100008000000080000000000000000000002202031503b2e74b21d4583b7f0d9e65b2c0ef19fd6e8aae7d0524fc770a1d2b2127501830cf1be5540000800100008000000080010000000000000000",
tx.serialize_as_bytes().hex()) tx.serialize_as_bytes().hex())
# if there are no multisig inputs, we never include xpubs in the psbt: # if there are no multisig inputs, we never include xpubs in the psbt:
tx.prepare_for_export_for_hardware_device(wallet) await tx.prepare_for_export_for_hardware_device(wallet)
self.assertEqual({}, tx.to_json()['xpubs']) self.assertEqual({}, tx.to_json()['xpubs'])
self.assertEqual("70736274ff0100710200000001916fa04d7080ae0cb19bd08671d37dbe3dc925be383737bb34b3097d82830dc70000000000fdffffff0240e20100000000001600147e45d43294b0ff2b08a5f45232649815e516cff0ceaa05000000000016001456ec9cad206160ab578fa1dfbe13311b3be4a3107f4a24000001011f96a007000000000016001413ce91db66299806c4f35b2b4f8426b0bd4f2cd70100fd2e010200000000010122c3730eb6314cf59e11988c41bfdd73f70cb55b294ec6f2eda828b5c939c0980100000000fdffffff0196a007000000000016001413ce91db66299806c4f35b2b4f8426b0bd4f2cd704004730440220112840ce5486c6b2d15bc3b12e45c2a4518828e1b34f9bb0b3a78220c0cec52f02205b146a1f683289909ecbd3f53932d5acc321444101d8002e435b38a54adbf47201473044022058dfb4c75de119595119f35dcd7b1b2c28c40d7e2e746baeae83f09396c6bb9e02201c3c40fb684253638f12392af3934a90a6c6a512441aac861022f927473c952001475221022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc482750492103cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca52ae4a4a24002206029e65093d22877cbfcc27cb754c58d144ec96635af1fcc63e5a7b90b23bb6acb81830cf1be5540000800100008000000080000000000000000000002202031503b2e74b21d4583b7f0d9e65b2c0ef19fd6e8aae7d0524fc770a1d2b2127501830cf1be5540000800100008000000080010000000000000000", self.assertEqual("70736274ff0100710200000001916fa04d7080ae0cb19bd08671d37dbe3dc925be383737bb34b3097d82830dc70000000000fdffffff0240e20100000000001600147e45d43294b0ff2b08a5f45232649815e516cff0ceaa05000000000016001456ec9cad206160ab578fa1dfbe13311b3be4a3107f4a24000001011f96a007000000000016001413ce91db66299806c4f35b2b4f8426b0bd4f2cd70100fd2e010200000000010122c3730eb6314cf59e11988c41bfdd73f70cb55b294ec6f2eda828b5c939c0980100000000fdffffff0196a007000000000016001413ce91db66299806c4f35b2b4f8426b0bd4f2cd704004730440220112840ce5486c6b2d15bc3b12e45c2a4518828e1b34f9bb0b3a78220c0cec52f02205b146a1f683289909ecbd3f53932d5acc321444101d8002e435b38a54adbf47201473044022058dfb4c75de119595119f35dcd7b1b2c28c40d7e2e746baeae83f09396c6bb9e02201c3c40fb684253638f12392af3934a90a6c6a512441aac861022f927473c952001475221022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc482750492103cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca52ae4a4a24002206029e65093d22877cbfcc27cb754c58d144ec96635af1fcc63e5a7b90b23bb6acb81830cf1be5540000800100008000000080000000000000000000002202031503b2e74b21d4583b7f0d9e65b2c0ef19fd6e8aae7d0524fc770a1d2b2127501830cf1be5540000800100008000000080010000000000000000",
tx.serialize_as_bytes().hex()) tx.serialize_as_bytes().hex())

174
electrum/transaction.py

@ -51,11 +51,12 @@ from .bitcoin import (TYPE_ADDRESS, TYPE_SCRIPT, hash_160,
base_encode, construct_witness, construct_script) base_encode, construct_witness, construct_script)
from .crypto import sha256d from .crypto import sha256d
from .logging import get_logger from .logging import get_logger
from .util import ShortID from .util import ShortID, OldTaskGroup
from .descriptor import Descriptor, MissingSolutionPiece, create_dummy_descriptor_from_address from .descriptor import Descriptor, MissingSolutionPiece, create_dummy_descriptor_from_address
if TYPE_CHECKING: if TYPE_CHECKING:
from .wallet import Abstract_Wallet from .wallet import Abstract_Wallet
from .network import Network
_logger = get_logger(__name__) _logger = get_logger(__name__)
@ -256,6 +257,7 @@ class TxInput:
self.block_txpos = None self.block_txpos = None
self.spent_height = None # type: Optional[int] # height at which the TXO got spent self.spent_height = None # type: Optional[int] # height at which the TXO got spent
self.spent_txid = None # type: Optional[str] # txid of the spender self.spent_txid = None # type: Optional[str] # txid of the spender
self._utxo = None # type: Optional[Transaction]
@property @property
def short_id(self): def short_id(self):
@ -264,6 +266,30 @@ class TxInput:
else: else:
return self.prevout.short_name() return self.prevout.short_name()
@property
def utxo(self):
return self._utxo
@utxo.setter
def utxo(self, tx: Optional['Transaction']):
if tx is None:
return
# note that tx might be a PartialTransaction
# serialize and de-serialize tx now. this might e.g. convert a complete PartialTx to a Tx
tx = tx_from_any(str(tx))
# 'utxo' field should not be a PSBT:
if not tx.is_complete():
return
self.validate_data(utxo=tx)
self._utxo = tx
def validate_data(self, *, utxo: Optional['Transaction'] = None, **kwargs) -> None:
utxo = utxo or self.utxo
if utxo:
if self.prevout.txid.hex() != utxo.txid():
raise PSBTInputConsistencyFailure(f"PSBT input validation: "
f"If a non-witness UTXO is provided, its hash must match the hash specified in the prevout")
def is_coinbase_input(self) -> bool: def is_coinbase_input(self) -> bool:
"""Whether this is the input of a coinbase tx.""" """Whether this is the input of a coinbase tx."""
return self.prevout.is_coinbase() return self.prevout.is_coinbase()
@ -275,6 +301,22 @@ class TxInput:
return self._is_coinbase_output return self._is_coinbase_output
def value_sats(self) -> Optional[int]: def value_sats(self) -> Optional[int]:
if self.utxo:
out_idx = self.prevout.out_idx
return self.utxo.outputs()[out_idx].value
return None
@property
def address(self) -> Optional[str]:
if self.scriptpubkey:
return get_address_from_output_script(self.scriptpubkey)
return None
@property
def scriptpubkey(self) -> Optional[bytes]:
if self.utxo:
out_idx = self.prevout.out_idx
return self.utxo.outputs()[out_idx].scriptpubkey
return None return None
def to_json(self): def to_json(self):
@ -314,6 +356,32 @@ class TxInput:
return True return True
return False return False
async def add_info_from_network(
self,
network: Optional['Network'],
*,
ignore_network_issues: bool = True,
) -> None:
from .network import NetworkException
async def fetch_from_network(txid) -> Optional[Transaction]:
tx = None
if network and network.has_internet_connection():
try:
raw_tx = await network.get_transaction(txid, timeout=10)
except NetworkException as e:
_logger.info(f'got network error getting input txn. err: {repr(e)}. txid: {txid}. '
f'if you are intentionally offline, consider using the --offline flag')
if not ignore_network_issues:
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
if self.utxo is None:
self.utxo = await fetch_from_network(txid=self.prevout.txid.hex())
class BCDataStream(object): class BCDataStream(object):
"""Workalike python implementation of Bitcoin's CDataStream class.""" """Workalike python implementation of Bitcoin's CDataStream class."""
@ -895,7 +963,40 @@ class Transaction:
return sha256d(bfh(ser))[::-1].hex() return sha256d(bfh(ser))[::-1].hex()
def add_info_from_wallet(self, wallet: 'Abstract_Wallet', **kwargs) -> None: def add_info_from_wallet(self, wallet: 'Abstract_Wallet', **kwargs) -> None:
return # no-op # populate prev_txs
for txin in self.inputs():
wallet.add_input_info(txin)
async def add_info_from_network(self, network: Optional['Network'], *, ignore_network_issues: bool = True) -> None:
"""note: it is recommended to call add_info_from_wallet first, as this can save some network requests"""
if not self.is_missing_info_from_network():
return
async with OldTaskGroup() as group:
for txin in self.inputs():
if txin.utxo is None:
await group.spawn(txin.add_info_from_network(network=network, ignore_network_issues=ignore_network_issues))
def is_missing_info_from_network(self) -> bool:
return any(txin.utxo is None for txin in self.inputs())
def add_info_from_wallet_and_network(
self, *, wallet: 'Abstract_Wallet', show_error: Callable[[str], None],
) -> bool:
"""Returns whether successful.
note: This is sort of a legacy hack... doing network requests in non-async code.
Relatedly, this should *not* be called from the network thread.
"""
# note side-effect: tx is being mutated
from .network import NetworkException
self.add_info_from_wallet(wallet)
try:
if self.is_missing_info_from_network():
Network.run_from_another_thread(
self.add_info_from_network(wallet.network, ignore_network_issues=False))
except NetworkException as e:
show_error(repr(e))
return False
return True
def is_final(self) -> bool: def is_final(self) -> bool:
"""Whether RBF is disabled.""" """Whether RBF is disabled."""
@ -1004,6 +1105,21 @@ class Transaction:
else: else:
raise Exception('output not found', addr) raise Exception('output not found', addr)
def input_value(self) -> int:
input_values = [txin.value_sats() for txin in self.inputs()]
if any([val is None for val in input_values]):
raise MissingTxInputAmount()
return sum(input_values)
def output_value(self) -> int:
return sum(o.value for o in self.outputs())
def get_fee(self) -> Optional[int]:
try:
return self.input_value() - self.output_value()
except MissingTxInputAmount:
return None
def get_input_idx_that_spent_prevout(self, prevout: TxOutpoint) -> Optional[int]: def get_input_idx_that_spent_prevout(self, prevout: TxOutpoint) -> Optional[int]:
# build cache if there isn't one yet # build cache if there isn't one yet
# note: can become stale and return incorrect data # note: can become stale and return incorrect data
@ -1177,7 +1293,6 @@ class PSBTSection:
class PartialTxInput(TxInput, PSBTSection): class PartialTxInput(TxInput, PSBTSection):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
TxInput.__init__(self, *args, **kwargs) TxInput.__init__(self, *args, **kwargs)
self._utxo = None # type: Optional[Transaction]
self._witness_utxo = None # type: Optional[TxOutput] self._witness_utxo = None # type: Optional[TxOutput]
self.part_sigs = {} # type: Dict[bytes, bytes] # pubkey -> sig self.part_sigs = {} # type: Dict[bytes, bytes] # pubkey -> sig
self.sighash = None # type: Optional[int] self.sighash = None # type: Optional[int]
@ -1193,23 +1308,6 @@ class PartialTxInput(TxInput, PSBTSection):
self._is_native_segwit = None # type: Optional[bool] # None means unknown self._is_native_segwit = None # type: Optional[bool] # None means unknown
self.witness_sizehint = None # type: Optional[int] # byte size of serialized complete witness, for tx size est self.witness_sizehint = None # type: Optional[int] # byte size of serialized complete witness, for tx size est
@property
def utxo(self):
return self._utxo
@utxo.setter
def utxo(self, tx: Optional[Transaction]):
if tx is None:
return
# note that tx might be a PartialTransaction
# serialize and de-serialize tx now. this might e.g. convert a complete PartialTx to a Tx
tx = tx_from_any(str(tx))
# 'utxo' field in PSBT cannot be another PSBT:
if not tx.is_complete():
return
self.validate_data(utxo=tx)
self._utxo = tx
@property @property
def witness_utxo(self): def witness_utxo(self):
return self._witness_utxo return self._witness_utxo
@ -1268,6 +1366,7 @@ class PartialTxInput(TxInput, PSBTSection):
nsequence=txin.nsequence, nsequence=txin.nsequence,
witness=None if strip_witness else txin.witness, witness=None if strip_witness else txin.witness,
is_coinbase_output=txin.is_coinbase_output()) is_coinbase_output=txin.is_coinbase_output())
res.utxo = txin.utxo
return res return res
def validate_data( def validate_data(
@ -1397,31 +1496,28 @@ class PartialTxInput(TxInput, PSBTSection):
wr(key_type, val, key=key) wr(key_type, val, key=key)
def value_sats(self) -> Optional[int]: def value_sats(self) -> Optional[int]:
if (val := super().value_sats()) is not None:
return val
if self._trusted_value_sats is not None: if self._trusted_value_sats is not None:
return self._trusted_value_sats return self._trusted_value_sats
if self.utxo:
out_idx = self.prevout.out_idx
return self.utxo.outputs()[out_idx].value
if self.witness_utxo: if self.witness_utxo:
return self.witness_utxo.value return self.witness_utxo.value
return None return None
@property @property
def address(self) -> Optional[str]: def address(self) -> Optional[str]:
if (addr := super().address) is not None:
return addr
if self._trusted_address is not None: if self._trusted_address is not None:
return self._trusted_address return self._trusted_address
scriptpubkey = self.scriptpubkey
if scriptpubkey:
return get_address_from_output_script(scriptpubkey)
return None return None
@property @property
def scriptpubkey(self) -> Optional[bytes]: def scriptpubkey(self) -> Optional[bytes]:
if (spk := super().scriptpubkey) is not None:
return spk
if self._trusted_address is not None: if self._trusted_address is not None:
return bfh(bitcoin.address_to_script(self._trusted_address)) return bfh(bitcoin.address_to_script(self._trusted_address))
if self.utxo:
out_idx = self.prevout.out_idx
return self.utxo.outputs()[out_idx].scriptpubkey
if self.witness_utxo: if self.witness_utxo:
return self.witness_utxo.scriptpubkey return self.witness_utxo.scriptpubkey
return None return None
@ -1886,21 +1982,6 @@ class PartialTransaction(Transaction):
self._outputs.sort(key = lambda o: (o.value, o.scriptpubkey)) self._outputs.sort(key = lambda o: (o.value, o.scriptpubkey))
self.invalidate_ser_cache() self.invalidate_ser_cache()
def input_value(self) -> int:
input_values = [txin.value_sats() for txin in self.inputs()]
if any([val is None for val in input_values]):
raise MissingTxInputAmount()
return sum(input_values)
def output_value(self) -> int:
return sum(o.value for o in self.outputs())
def get_fee(self) -> Optional[int]:
try:
return self.input_value() - self.output_value()
except MissingTxInputAmount:
return None
def serialize_preimage(self, txin_index: int, *, def serialize_preimage(self, txin_index: int, *,
bip143_shared_txdigest_fields: BIP143SharedTxDigestFields = None) -> str: bip143_shared_txdigest_fields: BIP143SharedTxDigestFields = None) -> str:
nVersion = int_to_hex(self.version, 4) nVersion = int_to_hex(self.version, 4)
@ -2052,7 +2133,6 @@ class PartialTransaction(Transaction):
wallet: 'Abstract_Wallet', wallet: 'Abstract_Wallet',
*, *,
include_xpubs: bool = False, include_xpubs: bool = False,
ignore_network_issues: bool = True,
) -> None: ) -> None:
if self.is_complete(): if self.is_complete():
return return
@ -2074,7 +2154,6 @@ class PartialTransaction(Transaction):
wallet.add_input_info( wallet.add_input_info(
txin, txin,
only_der_suffix=False, only_der_suffix=False,
ignore_network_issues=ignore_network_issues,
) )
for txout in self.outputs(): for txout in self.outputs():
wallet.add_output_info( wallet.add_output_info(
@ -2104,8 +2183,9 @@ class PartialTransaction(Transaction):
txout.bip32_paths.clear() txout.bip32_paths.clear()
txout._unknown.clear() txout._unknown.clear()
def prepare_for_export_for_hardware_device(self, wallet: 'Abstract_Wallet') -> None: async def prepare_for_export_for_hardware_device(self, wallet: 'Abstract_Wallet') -> None:
self.add_info_from_wallet(wallet, include_xpubs=True) self.add_info_from_wallet(wallet, include_xpubs=True)
await self.add_info_from_network(wallet.network)
# log warning if PSBT_*_BIP32_DERIVATION fields cannot be filled with full path due to missing info # log warning if PSBT_*_BIP32_DERIVATION fields cannot be filled with full path due to missing info
from .keystore import Xpub from .keystore import Xpub
def is_ks_missing_info(ks): def is_ks_missing_info(ks):

65
electrum/wallet.py

@ -1861,6 +1861,9 @@ class Abstract_Wallet(ABC, Logger, EventListener):
"""Increase the miner fee of 'tx'. """Increase the miner fee of 'tx'.
'new_fee_rate' is the target min rate in sat/vbyte '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 'coins' is a list of UTXOs we can choose from as potential new inputs to be added
note: it is the caller's responsibility to have already called tx.add_info_from_network().
Without that, all txins must be ismine.
""" """
txid = txid or tx.txid() txid = txid or tx.txid()
assert txid assert txid
@ -1872,11 +1875,9 @@ class Abstract_Wallet(ABC, Logger, EventListener):
if tx.is_final(): if tx.is_final():
raise CannotBumpFee(_('Transaction is final')) raise CannotBumpFee(_('Transaction is final'))
new_fee_rate = quantize_feerate(new_fee_rate) # strip excess precision new_fee_rate = quantize_feerate(new_fee_rate) # strip excess precision
try: tx.add_info_from_wallet(self)
# note: this might download input utxos over network if tx.is_missing_info_from_network():
tx.add_info_from_wallet(self, ignore_network_issues=False) raise Exception("tx missing info from network")
except NetworkException as e:
raise CannotBumpFee(repr(e))
old_tx_size = tx.estimated_size() old_tx_size = tx.estimated_size()
old_fee = tx.get_fee() old_fee = tx.get_fee()
assert old_fee is not None assert old_fee is not None
@ -2123,6 +2124,9 @@ class Abstract_Wallet(ABC, Logger, EventListener):
"""Double-Spend-Cancel: cancel an unconfirmed tx by double-spending """Double-Spend-Cancel: cancel an unconfirmed tx by double-spending
its inputs, paying ourselves. its inputs, paying ourselves.
'new_fee_rate' is the target min rate in sat/vbyte 'new_fee_rate' is the target min rate in sat/vbyte
note: it is the caller's responsibility to have already called tx.add_info_from_network().
Without that, all txins must be ismine.
""" """
if not isinstance(tx, PartialTransaction): if not isinstance(tx, PartialTransaction):
tx = PartialTransaction.from_tx(tx) tx = PartialTransaction.from_tx(tx)
@ -2132,11 +2136,9 @@ class Abstract_Wallet(ABC, Logger, EventListener):
if tx.is_final(): if tx.is_final():
raise CannotDoubleSpendTx(_('Transaction is final')) raise CannotDoubleSpendTx(_('Transaction is final'))
new_fee_rate = quantize_feerate(new_fee_rate) # strip excess precision new_fee_rate = quantize_feerate(new_fee_rate) # strip excess precision
try: tx.add_info_from_wallet(self)
# note: this might download input utxos over network if tx.is_missing_info_from_network():
tx.add_info_from_wallet(self, ignore_network_issues=False) raise Exception("tx missing info from network")
except NetworkException as e:
raise CannotDoubleSpendTx(repr(e))
old_tx_size = tx.estimated_size() old_tx_size = tx.estimated_size()
old_fee = tx.get_fee() old_fee = tx.get_fee()
assert old_fee is not None assert old_fee is not None
@ -2178,7 +2180,6 @@ class Abstract_Wallet(ABC, Logger, EventListener):
txin: PartialTxInput, txin: PartialTxInput,
*, *,
address: str = None, address: str = None,
ignore_network_issues: bool = True,
) -> None: ) -> None:
# - We prefer to include UTXO (full tx), even for segwit inputs (see #6198). # - We prefer to include UTXO (full tx), even for segwit inputs (see #6198).
# - For witness v0 inputs, we include *both* UTXO and WITNESS_UTXO. UTXO is a strict superset, # - For witness v0 inputs, we include *both* UTXO and WITNESS_UTXO. UTXO is a strict superset,
@ -2194,7 +2195,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
txin_value = item[2] txin_value = item[2]
txin.witness_utxo = TxOutput.from_address_and_value(address, txin_value) txin.witness_utxo = TxOutput.from_address_and_value(address, txin_value)
if txin.utxo is None: if txin.utxo is None:
txin.utxo = self.get_input_tx(txin.prevout.txid.hex(), ignore_network_issues=ignore_network_issues) txin.utxo = self.db.get_transaction(txin.prevout.txid.hex())
def _learn_derivation_path_for_address_from_txinout(self, txinout: Union[PartialTxInput, PartialTxOutput], def _learn_derivation_path_for_address_from_txinout(self, txinout: Union[PartialTxInput, PartialTxOutput],
address: str) -> bool: address: str) -> bool:
@ -2206,14 +2207,21 @@ class Abstract_Wallet(ABC, Logger, EventListener):
def add_input_info( def add_input_info(
self, self,
txin: PartialTxInput, txin: TxInput,
*, *,
only_der_suffix: bool = False, only_der_suffix: bool = False,
ignore_network_issues: bool = True,
) -> None: ) -> None:
address = self.adb.get_txin_address(txin) """Populates the txin, using info the wallet already has.
That is, network requests are *not* done to fetch missing prev txs!
For that, use txin.add_info_from_network.
"""
# note: we add input utxos regardless of is_mine # note: we add input utxos regardless of is_mine
self._add_input_utxo_info(txin, ignore_network_issues=ignore_network_issues, address=address) if txin.utxo is None:
txin.utxo = self.db.get_transaction(txin.prevout.txid.hex())
if not isinstance(txin, PartialTxInput):
return
address = self.adb.get_txin_address(txin)
self._add_input_utxo_info(txin, address=address)
is_mine = self.is_mine(address) is_mine = self.is_mine(address)
if not is_mine: if not is_mine:
is_mine = self._learn_derivation_path_for_address_from_txinout(txin, address) is_mine = self._learn_derivation_path_for_address_from_txinout(txin, address)
@ -2279,31 +2287,6 @@ class Abstract_Wallet(ABC, Logger, EventListener):
return True return True
return False return False
def _get_rawtx_from_network(self, txid: str) -> str:
"""legacy hack. do not use in new code."""
assert self.network
return self.network.run_from_another_thread(
self.network.get_transaction(txid, timeout=10))
def get_input_tx(self, tx_hash: str, *, ignore_network_issues=False) -> Optional[Transaction]:
# First look up an input transaction in the wallet where it
# will likely be. If co-signing a transaction it may not have
# all the input txs, in which case we ask the network.
tx = self.db.get_transaction(tx_hash)
if not tx and self.network and self.network.has_internet_connection():
try:
raw_tx = self._get_rawtx_from_network(tx_hash)
except NetworkException as e:
_logger.info(f'got network error getting input txn. err: {repr(e)}. txid: {tx_hash}. '
f'if you are intentionally offline, consider using the --offline flag')
if not ignore_network_issues:
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: def add_output_info(self, txout: PartialTxOutput, *, only_der_suffix: bool = False) -> None:
address = txout.address address = txout.address
if not self.is_mine(address): if not self.is_mine(address):

Loading…
Cancel
Save