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. 5
      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. 17
      electrum/gui/qt/transaction_dialog.py
  9. 93
      electrum/tests/test_wallet_vertical.py
  10. 174
      electrum/transaction.py
  11. 65
      electrum/wallet.py

5
electrum/address_synchronizer.py

@ -138,9 +138,8 @@ class AddressSynchronizer(Logger, EventListener):
return len(self._history_local.get(addr, ()))
def get_txin_address(self, txin: TxInput) -> Optional[str]:
if isinstance(txin, PartialTxInput):
if txin.address:
return txin.address
if txin.address:
return txin.address
prevout_hash = txin.prevout.txid.hex()
prevout_n = txin.prevout.out_idx
for addr in self.db.get_txo_addresses(prevout_hash):

2
electrum/commands.py

@ -762,6 +762,8 @@ class Commands:
coins = wallet.get_spendable_coins(None)
if domain_coins is not None:
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(
tx=tx,
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.wallet import CannotBumpFee, CannotCPFP, CannotDoubleSpendTx
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.util import address_colors
@ -120,19 +120,21 @@ Builder.load_string('''
class TxDialog(Factory.Popup):
def __init__(self, app, tx):
def __init__(self, app, tx: Transaction):
Factory.Popup.__init__(self)
self.app = app # type: ElectrumWindow
self.wallet = self.app.wallet
self.tx = tx # type: Transaction
self.tx = tx
self.config = self.app.electrum_config
# 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,
# 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)
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):
self.update()
@ -201,19 +203,6 @@ class TxDialog(Factory.Popup):
)
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):
from .bump_fee_dialog import BumpFeeDialog
tx = self.tx
@ -221,7 +210,7 @@ class TxDialog(Factory.Popup):
assert txid
if not isinstance(tx, PartialTransaction):
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
fee = tx.get_fee()
assert fee is not None
@ -295,7 +284,7 @@ class TxDialog(Factory.Popup):
assert txid
if not isinstance(tx, PartialTransaction):
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
fee = tx.get_fee()
assert fee is not None

10
electrum/gui/qml/qetransactionlistmodel.py

@ -1,4 +1,5 @@
from datetime import datetime, timedelta
from typing import TYPE_CHECKING
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
from PyQt5.QtCore import Qt, QAbstractListModel, QModelIndex
@ -9,6 +10,10 @@ from electrum.util import Satoshis, TxMinedInfo
from .qetypes import QEAmount
from .util import QtEventListener, qt_event_listener
if TYPE_CHECKING:
from electrum.wallet import Abstract_Wallet
class QETransactionListModel(QAbstractListModel, QtEventListener):
_logger = get_logger(__name__)
@ -22,7 +27,7 @@ class QETransactionListModel(QAbstractListModel, QtEventListener):
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)
self.wallet = wallet
self.onchain_domain = onchain_domain
@ -101,7 +106,8 @@ class QETransactionListModel(QAbstractListModel, QtEventListener):
item['balance'] = QEAmount(amount_sat=item['balance'].value)
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()
# 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 electrum.i18n import _
from electrum.logging import get_logger
from electrum.util import format_time, AddTransactionException
from electrum.transaction import tx_from_any
from electrum.network import Network
from .qewallet import QEWallet
from .qetypes import QEAmount
@ -23,7 +26,7 @@ class QETxDetails(QObject, QtEventListener):
self.register_callbacks()
self.destroyed.connect(lambda: self.on_destroy())
self._wallet = None
self._wallet = None # type: Optional[QEWallet]
self._txid = ''
self._rawtx = ''
self._label = ''
@ -229,13 +232,16 @@ class QETxDetails(QObject, QtEventListener):
return
if not self._rawtx:
# abusing get_input_tx to get tx from txid
self._tx = self._wallet.wallet.get_input_tx(self._txid)
self._tx = self._wallet.wallet.db.get_transaction(self._txid)
assert self._tx is not None
#self._logger.debug(repr(self._tx.to_json()))
self._logger.debug('adding info from 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._outputs = list(map(lambda x: {

40
electrum/gui/qml/qetxfinalizer.py

@ -494,7 +494,7 @@ class QETxRbfFeeBumper(TxFeeSlider, TxMonMixin):
def get_tx(self):
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
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):
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
self.update_from_tx(self._orig_tx)
@ -513,21 +513,6 @@ class QETxRbfFeeBumper(TxFeeSlider, TxMonMixin):
self.oldfeeRate = self.feeRate
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):
if not self._txid:
# not initialized yet
@ -616,13 +601,13 @@ class QETxCanceller(TxFeeSlider, TxMonMixin):
def get_tx(self):
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
if not isinstance(self._orig_tx, PartialTransaction):
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
self.update_from_tx(self._orig_tx)
@ -631,21 +616,6 @@ class QETxCanceller(TxFeeSlider, TxMonMixin):
self.oldfeeRate = self.feeRate
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):
if not self._txid:
# not initialized yet
@ -757,7 +727,7 @@ class QETxCpfpFeeBumper(TxFeeSlider, TxMonMixin):
def get_tx(self):
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
if isinstance(self._parent_tx, PartialTransaction):

20
electrum/gui/qt/main_window.py

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

17
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.logging import get_logger
from electrum.util import ShortID
from electrum.network import Network
from .util import (MessageBoxMixin, read_QIcon, Buttons, icon_path,
MONOSPACE_FONT, ColorScheme, ButtonsLineEdit, ShowQRLineEdit, text_dialog,
@ -477,11 +478,14 @@ class TxDialog(QDialog, MessageBoxMixin):
# 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.
BlockingWaitingDialog(
self,
_("Adding info to tx, from wallet and network..."),
lambda: tx.add_info_from_wallet(self.wallet),
)
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(
self,
_("Adding info to tx, from network..."),
lambda: Network.run_from_another_thread(tx.add_info_from_network(self.wallet.network)),
)
def do_broadcast(self):
self.main_window.push_top_level_window(self)
@ -535,7 +539,8 @@ class TxDialog(QDialog, MessageBoxMixin):
if not isinstance(self.tx, PartialTransaction):
raise Exception("Can only export partial transactions for hardware device.")
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
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):
with TmpConfig() as config:
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,
config=config)
with TmpConfig() as config:
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,
config=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):
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,
config=config)
with TmpConfig() as config:
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,
config=config)
with TmpConfig() as config:
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,
config=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):
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,
config=config)
with TmpConfig() as config:
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,
config=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):
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,
config=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):
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,
config=config)
with TmpConfig() as config:
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,
config=config)
with TmpConfig() as config:
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,
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',
config=config)
@ -1165,7 +1165,7 @@ class TestWalletSending(ElectrumTestCase):
wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
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
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)
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',
config=config)
@ -1249,7 +1249,7 @@ class TestWalletSending(ElectrumTestCase):
wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
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',
config=config)
@ -1324,7 +1324,7 @@ class TestWalletSending(ElectrumTestCase):
wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
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',
config=config)
@ -1388,13 +1388,10 @@ class TestWalletSending(ElectrumTestCase):
wallet.adb.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):
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:
relay_fee = 1000
async def get_transaction(self, txid, timeout=None):
return self._gettx(txid)
@staticmethod
def _gettx(txid):
if txid == "597098f9077cd2a7bf5bb2a03c9ae5fcd9d1f07c0891cb42cbb129cf9eaf57fd":
return "02000000000102a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540000000000fdffffffbdeb0175b1c51c96843d1952f7e1c49c1703717d7d020048d4de0a8eed94dad50000000000fdffffff03b2a00700000000001600140cd6c9f8ce0aa73d77fcf7f156c74f5cbec6906bb2a00700000000001600146435504ddc95e6019a90bb7dfc7ca81a88a8633106d790000000000016001444bd3017ee214370abf683abaa7f6204c9f40210024730440220652a04a2a301d9a031a034f3ae48174e204e17acf7bfc27f0dcab14243f73e2202207b29e964c434dfb2c515232d36566a40dccd4dd93ccb7fd15260ecbda10f0d9801210231994e564a0530068d17a9b0f85bec58d1352517a2861ea99e5b3070d2c5dbda02473044022072186473874919019da0e3d92b6e0aa4f88cba448ed5434615e5a3c8e2b7c42a02203ec05cef66960d5bc45d0f3d25675190cf8035b11a05ed4b719fd9c3a894899b012102f5fdca8c4e30ba0a1babf9cf9ebe62519b08aead351c349ed1ffc8316c24f542d7f61c00"
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',
config=config)
wallet.network = NetworkMock()
wallet._get_rawtx_from_network = NetworkMock._gettx
# bootstrap wallet
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)
# 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.version = 2
if simulate_moving_txs:
@ -1445,13 +1444,10 @@ class TestWalletSending(ElectrumTestCase):
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):
async 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):
return self._gettx(txid)
@staticmethod
def _gettx(txid):
if txid == "08557327673db61cc921e1a30826608599b86457836be3021105c13940d9a9a3":
return "02000000000101a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540100000000fdffffff0220a1070000000000160014db44724ac632ae47ee5765954d64796dd5fec72708de3c000000000016001424b32aadb42a89016c4de8f11741c3b29b15f21c02473044022045cc6c1cc875cbb0c0d8fe323dc1de9716e49ed5659741b0fb3dd9a196894066022077c242640071d12ec5763c5870f482a4823d8713e4bd14353dd621ed29a7f96d012102aea8d439a0f79d8b58e8d7bda83009f587e1f3da350adaa484329bf47cd03465fef61c00"
else:
@ -1473,7 +1469,6 @@ class TestWalletSending(ElectrumTestCase):
gap_limit=4,
)
wallet.network = NetworkMock()
wallet._get_rawtx_from_network = NetworkMock._gettx
# bootstrap wallet
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)
# 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.version = 2
if simulate_moving_txs:
@ -1506,7 +1504,7 @@ class TestWalletSending(ElectrumTestCase):
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',
config=config)
@ -1568,7 +1566,7 @@ class TestWalletSending(ElectrumTestCase):
wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
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',
config=config)
@ -1631,7 +1629,7 @@ class TestWalletSending(ElectrumTestCase):
wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
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',
config=config)
@ -1703,7 +1701,7 @@ class TestWalletSending(ElectrumTestCase):
wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
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',
config=config)
wallet.config.set_key('batch_rbf', True)
@ -2177,23 +2175,23 @@ class TestWalletSending(ElectrumTestCase):
for simulate_moving_txs in (False, True):
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,
config=config)
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,
config=config)
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,
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(
await 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):
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',
config=config)
@ -2238,7 +2236,7 @@ class TestWalletSending(ElectrumTestCase):
tx_details = wallet.get_tx_info(tx_from_any(tx.serialize()))
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',
config=config)
@ -2304,7 +2302,7 @@ class TestWalletSending(ElectrumTestCase):
wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
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',
config=config)
@ -2369,13 +2367,10 @@ class TestWalletSending(ElectrumTestCase):
wallet.adb.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):
async 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):
return self._gettx(txid)
@staticmethod
def _gettx(txid):
if txid == "597098f9077cd2a7bf5bb2a03c9ae5fcd9d1f07c0891cb42cbb129cf9eaf57fd":
return "02000000000102a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540000000000fdffffffbdeb0175b1c51c96843d1952f7e1c49c1703717d7d020048d4de0a8eed94dad50000000000fdffffff03b2a00700000000001600140cd6c9f8ce0aa73d77fcf7f156c74f5cbec6906bb2a00700000000001600146435504ddc95e6019a90bb7dfc7ca81a88a8633106d790000000000016001444bd3017ee214370abf683abaa7f6204c9f40210024730440220652a04a2a301d9a031a034f3ae48174e204e17acf7bfc27f0dcab14243f73e2202207b29e964c434dfb2c515232d36566a40dccd4dd93ccb7fd15260ecbda10f0d9801210231994e564a0530068d17a9b0f85bec58d1352517a2861ea99e5b3070d2c5dbda02473044022072186473874919019da0e3d92b6e0aa4f88cba448ed5434615e5a3c8e2b7c42a02203ec05cef66960d5bc45d0f3d25675190cf8035b11a05ed4b719fd9c3a894899b012102f5fdca8c4e30ba0a1babf9cf9ebe62519b08aead351c349ed1ffc8316c24f542d7f61c00"
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',
config=config)
wallet.network = NetworkMock()
wallet._get_rawtx_from_network = NetworkMock._gettx
# bootstrap wallet
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)
# 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.version = 2
if simulate_moving_txs:
@ -2686,7 +2683,7 @@ class TestWalletSending(ElectrumTestCase):
tx.inputs()[0].to_json()['bip32_paths'])
self.assertEqual("70736274ff01007d020000000122c3730eb6314cf59e11988c41bfdd73f70cb55b294ec6f2eda828b5c939c0980100000000fdffffff0240e20100000000001600147e45d43294b0ff2b08a5f45232649815e516cff058ab05000000000022002014d2823afee4d75f0f83b91a9d625972df41be222c1373d28e068c3eaae9e00a7b4a24000001012b20a10700000000002200207f50b9d6eb4d899c710d8c48903de33d966ff52445d5a57b5210d02a5dd7e3bf0100fd7e0102000000000102deab5844de4aadc177d992696fda2aa6e4692403633d31a4b4073710594d2fca0000000000fdffffffdeab5844de4aadc177d992696fda2aa6e4692403633d31a4b4073710594d2fca0100000000fdffffff02f49f070000000000160014473b34b7da0aa9f7add803019f649e0729fd39d220a10700000000002200207f50b9d6eb4d899c710d8c48903de33d966ff52445d5a57b5210d02a5dd7e3bf0247304402202a4ec3df7bf2b82505bcd4833eeb32875784b4e93d09ac3cf4a8981dc89a049b02205239bad290877fb810a12538a275d5467f3f6afc88d1e0be3d8f6dc4876e6793012103e48cae7f140e15440f4ad6b3d96cb0deb471bbb45daf527e6eb4d5f6c5e26ec802473044022031028192a8307e52829ad1428941000629de73726306ca71d18c5bcfcb98a4a602205ad0240f7dd6c83686ea257f3146ba595b787d7f68b514569962fd5d3692b07c0121033c8af340bd9abf4a56c7cf7554f52e84a1128e5206ffe5da166ca18a57a260077b4a24000105475221022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc482750492103cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca52ae2206022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc48275049109559fbd10f2700800000000000000000220603cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca0c015148ee000000000000000000000101475221027f7f2eaf9a44316c2cd98b67584d1e71ccaced29a347673f3364efe16f5919e221028d9b8ff374e0f60fbc698c5a494c12d9a31a3ce364b1f81ae4a46f48ae45acdd52ae2202027f7f2eaf9a44316c2cd98b67584d1e71ccaced29a347673f3364efe16f5919e2109559fbd10f27008001000000000000002202028d9b8ff374e0f60fbc698c5a494c12d9a31a3ce364b1f81ae4a46f48ae45acdd0c015148ee010000000000000000",
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
# (derivation prefix and root fingerprint).
# 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'])
self.assertEqual("70736274ff01007d020000000122c3730eb6314cf59e11988c41bfdd73f70cb55b294ec6f2eda828b5c939c0980100000000fdffffff0240e20100000000001600147e45d43294b0ff2b08a5f45232649815e516cff058ab05000000000022002014d2823afee4d75f0f83b91a9d625972df41be222c1373d28e068c3eaae9e00a7b4a24000001012b20a10700000000002200207f50b9d6eb4d899c710d8c48903de33d966ff52445d5a57b5210d02a5dd7e3bf0100fd7e0102000000000102deab5844de4aadc177d992696fda2aa6e4692403633d31a4b4073710594d2fca0000000000fdffffffdeab5844de4aadc177d992696fda2aa6e4692403633d31a4b4073710594d2fca0100000000fdffffff02f49f070000000000160014473b34b7da0aa9f7add803019f649e0729fd39d220a10700000000002200207f50b9d6eb4d899c710d8c48903de33d966ff52445d5a57b5210d02a5dd7e3bf0247304402202a4ec3df7bf2b82505bcd4833eeb32875784b4e93d09ac3cf4a8981dc89a049b02205239bad290877fb810a12538a275d5467f3f6afc88d1e0be3d8f6dc4876e6793012103e48cae7f140e15440f4ad6b3d96cb0deb471bbb45daf527e6eb4d5f6c5e26ec802473044022031028192a8307e52829ad1428941000629de73726306ca71d18c5bcfcb98a4a602205ad0240f7dd6c83686ea257f3146ba595b787d7f68b514569962fd5d3692b07c0121033c8af340bd9abf4a56c7cf7554f52e84a1128e5206ffe5da166ca18a57a260077b4a24000105475221022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc482750492103cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca52ae2206022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc48275049109559fbd10f2700800000000000000000220603cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca1c30cf1be530000080010000800000008002000080000000000000000000000101475221027f7f2eaf9a44316c2cd98b67584d1e71ccaced29a347673f3364efe16f5919e221028d9b8ff374e0f60fbc698c5a494c12d9a31a3ce364b1f81ae4a46f48ae45acdd52ae2202027f7f2eaf9a44316c2cd98b67584d1e71ccaced29a347673f3364efe16f5919e2109559fbd10f27008001000000000000002202028d9b8ff374e0f60fbc698c5a494c12d9a31a3ce364b1f81ae4a46f48ae45acdd1c30cf1be530000080010000800000008002000080010000000000000000",
tx.serialize_as_bytes().hex())
tx.prepare_for_export_for_hardware_device(wallet)
await tx.prepare_for_export_for_hardware_device(wallet)
self.assertEqual(
{'tpubDFF7YPCSGHZy55HkQj6HJkXCR8DWbKKXpTYBH38fSHf6VuoEzNmZQZdAoKEVy36S8zXkbGeV4XQU6vaRXGsQfgptFYPR4HSpAenqkY7J7Lg': ('30cf1be5', "m/48h/1h/0h/2h"),
'tpubD9MoDeHnEQnU5EMgt9mc4yKU6SURbfq2ooMToY5GH95B8Li1CEsuo9dBKXM2sdjuDGq4KCXLuigss3y22fZULzVrfVuZDxEN55Sp6CcU9DK': ('9559fbd1', "m/9999h")},
@ -2754,7 +2751,7 @@ class TestWalletSending(ElectrumTestCase):
self.assertEqual("70736274ff0100710200000001916fa04d7080ae0cb19bd08671d37dbe3dc925be383737bb34b3097d82830dc70000000000fdffffff0240e20100000000001600147e45d43294b0ff2b08a5f45232649815e516cff0ceaa05000000000016001456ec9cad206160ab578fa1dfbe13311b3be4a3107f4a24000001011f96a007000000000016001413ce91db66299806c4f35b2b4f8426b0bd4f2cd70100fd2e010200000000010122c3730eb6314cf59e11988c41bfdd73f70cb55b294ec6f2eda828b5c939c0980100000000fdffffff0196a007000000000016001413ce91db66299806c4f35b2b4f8426b0bd4f2cd704004730440220112840ce5486c6b2d15bc3b12e45c2a4518828e1b34f9bb0b3a78220c0cec52f02205b146a1f683289909ecbd3f53932d5acc321444101d8002e435b38a54adbf47201473044022058dfb4c75de119595119f35dcd7b1b2c28c40d7e2e746baeae83f09396c6bb9e02201c3c40fb684253638f12392af3934a90a6c6a512441aac861022f927473c952001475221022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc482750492103cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca52ae4a4a24002206029e65093d22877cbfcc27cb754c58d144ec96635af1fcc63e5a7b90b23bb6acb81830cf1be5540000800100008000000080000000000000000000002202031503b2e74b21d4583b7f0d9e65b2c0ef19fd6e8aae7d0524fc770a1d2b2127501830cf1be5540000800100008000000080010000000000000000",
tx.serialize_as_bytes().hex())
# 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("70736274ff0100710200000001916fa04d7080ae0cb19bd08671d37dbe3dc925be383737bb34b3097d82830dc70000000000fdffffff0240e20100000000001600147e45d43294b0ff2b08a5f45232649815e516cff0ceaa05000000000016001456ec9cad206160ab578fa1dfbe13311b3be4a3107f4a24000001011f96a007000000000016001413ce91db66299806c4f35b2b4f8426b0bd4f2cd70100fd2e010200000000010122c3730eb6314cf59e11988c41bfdd73f70cb55b294ec6f2eda828b5c939c0980100000000fdffffff0196a007000000000016001413ce91db66299806c4f35b2b4f8426b0bd4f2cd704004730440220112840ce5486c6b2d15bc3b12e45c2a4518828e1b34f9bb0b3a78220c0cec52f02205b146a1f683289909ecbd3f53932d5acc321444101d8002e435b38a54adbf47201473044022058dfb4c75de119595119f35dcd7b1b2c28c40d7e2e746baeae83f09396c6bb9e02201c3c40fb684253638f12392af3934a90a6c6a512441aac861022f927473c952001475221022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc482750492103cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca52ae4a4a24002206029e65093d22877cbfcc27cb754c58d144ec96635af1fcc63e5a7b90b23bb6acb81830cf1be5540000800100008000000080000000000000000000002202031503b2e74b21d4583b7f0d9e65b2c0ef19fd6e8aae7d0524fc770a1d2b2127501830cf1be5540000800100008000000080010000000000000000",
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)
from .crypto import sha256d
from .logging import get_logger
from .util import ShortID
from .util import ShortID, OldTaskGroup
from .descriptor import Descriptor, MissingSolutionPiece, create_dummy_descriptor_from_address
if TYPE_CHECKING:
from .wallet import Abstract_Wallet
from .network import Network
_logger = get_logger(__name__)
@ -256,6 +257,7 @@ class TxInput:
self.block_txpos = None
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._utxo = None # type: Optional[Transaction]
@property
def short_id(self):
@ -264,6 +266,30 @@ class TxInput:
else:
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:
"""Whether this is the input of a coinbase tx."""
return self.prevout.is_coinbase()
@ -275,6 +301,22 @@ class TxInput:
return self._is_coinbase_output
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
def to_json(self):
@ -314,6 +356,32 @@ class TxInput:
return True
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):
"""Workalike python implementation of Bitcoin's CDataStream class."""
@ -895,7 +963,40 @@ class Transaction:
return sha256d(bfh(ser))[::-1].hex()
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:
"""Whether RBF is disabled."""
@ -1004,6 +1105,21 @@ class Transaction:
else:
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]:
# build cache if there isn't one yet
# note: can become stale and return incorrect data
@ -1177,7 +1293,6 @@ class PSBTSection:
class PartialTxInput(TxInput, PSBTSection):
def __init__(self, *args, **kwargs):
TxInput.__init__(self, *args, **kwargs)
self._utxo = None # type: Optional[Transaction]
self._witness_utxo = None # type: Optional[TxOutput]
self.part_sigs = {} # type: Dict[bytes, bytes] # pubkey -> sig
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.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
def witness_utxo(self):
return self._witness_utxo
@ -1268,6 +1366,7 @@ class PartialTxInput(TxInput, PSBTSection):
nsequence=txin.nsequence,
witness=None if strip_witness else txin.witness,
is_coinbase_output=txin.is_coinbase_output())
res.utxo = txin.utxo
return res
def validate_data(
@ -1397,31 +1496,28 @@ class PartialTxInput(TxInput, PSBTSection):
wr(key_type, val, key=key)
def value_sats(self) -> Optional[int]:
if (val := super().value_sats()) is not None:
return val
if self._trusted_value_sats is not None:
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:
return self.witness_utxo.value
return None
@property
def address(self) -> Optional[str]:
if (addr := super().address) is not None:
return addr
if self._trusted_address is not None:
return self._trusted_address
scriptpubkey = self.scriptpubkey
if scriptpubkey:
return get_address_from_output_script(scriptpubkey)
return None
@property
def scriptpubkey(self) -> Optional[bytes]:
if (spk := super().scriptpubkey) is not None:
return spk
if self._trusted_address is not None:
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:
return self.witness_utxo.scriptpubkey
return None
@ -1886,21 +1982,6 @@ class PartialTransaction(Transaction):
self._outputs.sort(key = lambda o: (o.value, o.scriptpubkey))
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, *,
bip143_shared_txdigest_fields: BIP143SharedTxDigestFields = None) -> str:
nVersion = int_to_hex(self.version, 4)
@ -2052,7 +2133,6 @@ class PartialTransaction(Transaction):
wallet: 'Abstract_Wallet',
*,
include_xpubs: bool = False,
ignore_network_issues: bool = True,
) -> None:
if self.is_complete():
return
@ -2074,7 +2154,6 @@ class PartialTransaction(Transaction):
wallet.add_input_info(
txin,
only_der_suffix=False,
ignore_network_issues=ignore_network_issues,
)
for txout in self.outputs():
wallet.add_output_info(
@ -2104,8 +2183,9 @@ class PartialTransaction(Transaction):
txout.bip32_paths.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)
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
from .keystore import Xpub
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'.
'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
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()
assert txid
@ -1872,11 +1875,9 @@ class Abstract_Wallet(ABC, Logger, EventListener):
if tx.is_final():
raise CannotBumpFee(_('Transaction is final'))
new_fee_rate = quantize_feerate(new_fee_rate) # strip excess precision
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(repr(e))
tx.add_info_from_wallet(self)
if tx.is_missing_info_from_network():
raise Exception("tx missing info from network")
old_tx_size = tx.estimated_size()
old_fee = tx.get_fee()
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
its inputs, paying ourselves.
'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):
tx = PartialTransaction.from_tx(tx)
@ -2132,11 +2136,9 @@ class Abstract_Wallet(ABC, Logger, EventListener):
if tx.is_final():
raise CannotDoubleSpendTx(_('Transaction is final'))
new_fee_rate = quantize_feerate(new_fee_rate) # strip excess precision
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(repr(e))
tx.add_info_from_wallet(self)
if tx.is_missing_info_from_network():
raise Exception("tx missing info from network")
old_tx_size = tx.estimated_size()
old_fee = tx.get_fee()
assert old_fee is not None
@ -2178,7 +2180,6 @@ class Abstract_Wallet(ABC, Logger, EventListener):
txin: PartialTxInput,
*,
address: str = None,
ignore_network_issues: bool = True,
) -> None:
# - 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,
@ -2194,7 +2195,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
txin_value = item[2]
txin.witness_utxo = TxOutput.from_address_and_value(address, txin_value)
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],
address: str) -> bool:
@ -2206,14 +2207,21 @@ class Abstract_Wallet(ABC, Logger, EventListener):
def add_input_info(
self,
txin: PartialTxInput,
txin: TxInput,
*,
only_der_suffix: bool = False,
ignore_network_issues: bool = True,
) -> 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
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)
if not is_mine:
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 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:
address = txout.address
if not self.is_mine(address):

Loading…
Cancel
Save