Files
purple-electrumwallet/electrum/gui/qml/qetxfinalizer.py
T
f321x 30b146033d gui: detect if reserve inputs have been removed from tx
Adapts the gui(s) to detect if an existing reserve input has not been
used as input for the transaction even though the user tried to spend
max. This allows to show the lightning reserve warning not only for
reserve change outputs but also for existing reserve inputs that have
been ignored for this max spend transaction.
Still keeps the `is_utxo_reserve` flag on `PartialTxOutput` as it is
used in the qml gui.
2025-07-29 15:40:51 +02:00

1060 lines
33 KiB
Python

import copy
from enum import IntEnum
import threading
from decimal import Decimal
from typing import Optional, TYPE_CHECKING
from functools import partial
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, pyqtEnum
from electrum.logging import get_logger
from electrum.i18n import _
from electrum.bitcoin import DummyAddress
from electrum.transaction import PartialTxOutput, PartialTransaction, Transaction, TxOutpoint
from electrum.util import NotEnoughFunds, profiler, quantize_feerate, UserFacingException
from electrum.wallet import CannotBumpFee, CannotDoubleSpendTx, CannotCPFP, BumpFeeStrategy, sweep_preparations
from electrum import keystore
from electrum.plugin import run_hook
from electrum.fee_policy import FeePolicy, FeeMethod
from .qewallet import QEWallet
from .qetypes import QEAmount
from .util import QtEventListener, event_listener
if TYPE_CHECKING:
from electrum.simple_config import SimpleConfig
class FeeSlider(QObject):
@pyqtEnum
class FSMethod(IntEnum):
FEERATE = 0
ETA = 1
MEMPOOL = 2
def to_fee_method(self) -> 'FeeMethod':
return {
self.FEERATE: FeeMethod.FEERATE,
self.ETA: FeeMethod.ETA,
self.MEMPOOL: FeeMethod.MEMPOOL,
}[self]
@classmethod
def from_fee_method(cls, fm: FeeMethod) -> 'FeeSlider.FSMethod':
return {
FeeMethod.FEERATE: cls.FEERATE,
FeeMethod.ETA: cls.ETA,
FeeMethod.MEMPOOL: cls.MEMPOOL,
}[fm]
def __init__(self, parent=None):
super().__init__(parent)
self._wallet = None # type: Optional[QEWallet]
self._sliderSteps = 0
self._sliderPos = 0
self._fee_policy = None
self._target = ''
self._config = None # type: Optional[SimpleConfig]
walletChanged = pyqtSignal()
@pyqtProperty(QEWallet, notify=walletChanged)
def wallet(self):
return self._wallet
@wallet.setter
def wallet(self, wallet: QEWallet):
if self._wallet != wallet:
self._wallet = wallet
self._config = self._wallet.wallet.config
self.read_config()
self.walletChanged.emit()
sliderStepsChanged = pyqtSignal()
@pyqtProperty(int, notify=sliderStepsChanged)
def sliderSteps(self):
return self._sliderSteps
sliderPosChanged = pyqtSignal()
@pyqtProperty(int, notify=sliderPosChanged)
def sliderPos(self):
return self._sliderPos
@sliderPos.setter
def sliderPos(self, sliderPos):
if self._sliderPos != sliderPos:
self._sliderPos = sliderPos
self.save_config()
self.sliderPosChanged.emit()
methodChanged = pyqtSignal()
@pyqtProperty(int, notify=methodChanged)
def method(self) -> int:
fsmethod = self.FSMethod.from_fee_method(self._fee_policy.method)
return int(fsmethod)
@method.setter
def method(self, method: int):
fsmethod = self.FSMethod(method)
method = fsmethod.to_fee_method()
if self._fee_policy.method != method:
self._fee_policy.set_method(method)
self.update_slider()
self.methodChanged.emit()
self.save_config()
targetChanged = pyqtSignal()
@pyqtProperty(str, notify=targetChanged)
def target(self):
return self._target
@target.setter
def target(self, target):
if self._target != target:
self._target = target
self.targetChanged.emit()
def update_slider(self):
self._sliderSteps = self._fee_policy.get_slider_max()
self._sliderPos = self._fee_policy.get_slider_pos()
self.sliderStepsChanged.emit()
self.sliderPosChanged.emit()
def update_target(self):
self.target = self._fee_policy.get_target_text()
def read_config(self):
self._fee_policy = FeePolicy(self._config.FEE_POLICY)
self.update_slider()
self.methodChanged.emit()
self.update_target()
self.update()
def save_config(self):
value = int(self._sliderPos)
self._fee_policy.set_value_from_slider_pos(value)
self._config.FEE_POLICY = self._fee_policy.get_descriptor()
self.update_target()
self.update()
def update(self):
raise NotImplementedError()
class TxFeeSlider(FeeSlider):
def __init__(self, parent=None):
super().__init__(parent)
self._fee = QEAmount()
self._feeRate = ''
self._rbf = False
self._tx = None
self._inputs = []
self._outputs = []
self._finalized_txid = ''
self._valid = False
self._warning = ''
feeChanged = pyqtSignal()
@pyqtProperty(QEAmount, notify=feeChanged)
def fee(self):
return self._fee
@fee.setter
def fee(self, fee):
if self._fee != fee:
self._fee.copyFrom(fee)
self.feeChanged.emit()
feeRateChanged = pyqtSignal()
@pyqtProperty(str, notify=feeRateChanged)
def feeRate(self):
return self._feeRate
@feeRate.setter
def feeRate(self, feeRate):
if self._feeRate != feeRate:
self._feeRate = feeRate
self.feeRateChanged.emit()
rbfChanged = pyqtSignal()
@pyqtProperty(bool, notify=rbfChanged)
def rbf(self):
return self._rbf
@rbf.setter
def rbf(self, rbf):
if self._rbf != rbf:
self._rbf = rbf
self.update()
self.rbfChanged.emit()
inputsChanged = pyqtSignal()
@pyqtProperty('QVariantList', notify=inputsChanged)
def inputs(self):
return self._inputs
@inputs.setter
def inputs(self, inputs):
if self._inputs != inputs:
self._inputs = inputs
self.inputsChanged.emit()
outputsChanged = pyqtSignal()
@pyqtProperty('QVariantList', notify=outputsChanged)
def outputs(self):
return self._outputs
@outputs.setter
def outputs(self, outputs):
if self._outputs != outputs:
self._outputs = outputs
self.outputsChanged.emit()
finalizedTxidChanged = pyqtSignal()
@pyqtProperty(str, notify=finalizedTxidChanged)
def finalizedTxid(self):
return self._finalized_txid
@finalizedTxid.setter
def finalizedTxid(self, finalized_txid):
if self._finalized_txid != finalized_txid:
self._finalized_txid = finalized_txid
self.finalizedTxidChanged.emit()
warningChanged = pyqtSignal()
@pyqtProperty(str, notify=warningChanged)
def warning(self):
return self._warning
@warning.setter
def warning(self, warning):
if self._warning != warning:
self._warning = warning
self.warningChanged.emit()
validChanged = pyqtSignal()
@pyqtProperty(bool, notify=validChanged)
def valid(self):
return self._valid
@pyqtSlot()
def doUpdate(self):
self.update()
def update_from_tx(self, tx):
tx_size = tx.estimated_size()
fee = tx.get_fee()
feerate = Decimal(fee) / tx_size # sat/byte
self.fee = QEAmount(amount_sat=int(fee))
self.feeRate = f'{feerate:.1f}'
self.finalizedTxid = tx.txid()
self.update_inputs_from_tx(tx)
self.update_outputs_from_tx(tx)
def update_inputs_from_tx(self, tx):
inputs = []
for inp in tx.inputs():
# addr = self.wallet.adb.get_txin_address(txin)
addr = inp.address
address_str = '<address unknown>' if addr is None else addr
txin_value = inp.value_sats() if inp.value_sats() else 0
inputs.append({
'address': address_str,
'short_id': str(inp.short_id),
'value': QEAmount(amount_sat=txin_value),
'is_coinbase': inp.is_coinbase_input(),
'is_mine': self._wallet.wallet.is_mine(addr),
'is_change': self._wallet.wallet.is_change(addr),
'prevout_txid': inp.prevout.txid.hex(),
'is_swap': False
})
self.inputs = inputs
def update_outputs_from_tx(self, tx):
sm = self._wallet.wallet.lnworker.swap_manager if self._wallet.wallet.lnworker else None
outputs = []
for idx, o in enumerate(tx.outputs()):
outputs.append({
'address': o.get_ui_address_str(),
'value': o.value,
'short_id': str(TxOutpoint(bytes.fromhex(tx.txid()), idx).short_name()) if tx.txid() else '',
'is_mine': self._wallet.wallet.is_mine(o.get_ui_address_str()),
'is_change': self._wallet.wallet.is_change(o.get_ui_address_str()),
'is_billing': self._wallet.wallet.is_billing_address(o.get_ui_address_str()),
'is_swap': False if not sm else sm.is_lockup_address_for_a_swap(o.get_ui_address_str()) or o.get_ui_address_str() == DummyAddress.SWAP,
'is_accounting': self._wallet.wallet.is_accounting_address(o.get_ui_address_str()),
'is_reserve': o.is_utxo_reserve
})
self.outputs = outputs
def update_fee_warning_from_tx(self, *, tx: PartialTransaction, invoice_amt: Optional[int]):
if invoice_amt is None:
invoice_amt = sum([txo.value for txo in tx.outputs() if not txo.is_mine])
if invoice_amt == 0:
invoice_amt = tx.output_value()
fee_warning_tuple = self._wallet.wallet.get_tx_fee_warning(
invoice_amt=invoice_amt, tx_size=tx.estimated_size(), fee=tx.get_fee(), txid=tx.txid())
if fee_warning_tuple:
allow_send, long_warning, short_warning = fee_warning_tuple
self.warning = _('Warning') + ': ' + long_warning
else:
self.warning = ''
class QETxFinalizer(TxFeeSlider):
_logger = get_logger(__name__)
finished = pyqtSignal([bool, bool, bool], arguments=['signed', 'saved', 'complete'])
signError = pyqtSignal([str], arguments=['message'])
def __init__(self, parent=None, *, make_tx=None, accept=None):
super().__init__(parent)
self.f_make_tx = make_tx
self.f_accept = accept
self._address = ''
self._amount = QEAmount()
self._effectiveAmount = QEAmount()
self._extraFee = QEAmount()
self._canRbf = False
addressChanged = pyqtSignal()
@pyqtProperty(str, notify=addressChanged)
def address(self):
return self._address
@address.setter
def address(self, address):
if self._address != address:
self._address = address
self.addressChanged.emit()
amountChanged = pyqtSignal()
@pyqtProperty(QEAmount, notify=amountChanged)
def amount(self):
return self._amount
@amount.setter
def amount(self, amount):
if self._amount != amount:
self._logger.debug(str(amount))
self._amount.copyFrom(amount)
self.amountChanged.emit()
effectiveAmountChanged = pyqtSignal()
@pyqtProperty(QEAmount, notify=effectiveAmountChanged)
def effectiveAmount(self):
return self._effectiveAmount
extraFeeChanged = pyqtSignal()
@pyqtProperty(QEAmount, notify=extraFeeChanged)
def extraFee(self):
return self._extraFee
@extraFee.setter
def extraFee(self, extrafee):
if self._extraFee != extrafee:
self._extraFee.copyFrom(extrafee)
self.extraFeeChanged.emit()
canRbfChanged = pyqtSignal()
@pyqtProperty(bool, notify=canRbfChanged)
def canRbf(self):
return self._canRbf
@canRbf.setter
def canRbf(self, canRbf):
if self._canRbf != canRbf:
self._canRbf = canRbf
self.canRbfChanged.emit()
self.rbf = self._canRbf # if we can RbF, we do RbF
@profiler
def make_tx(self, amount):
self._logger.debug(f'make_tx amount={amount}')
if self.f_make_tx:
tx = self.f_make_tx(amount, self._fee_policy)
else:
# default impl
coins = self._wallet.wallet.get_spendable_coins(None)
outputs = [PartialTxOutput.from_address_and_value(self.address, amount)]
tx = self._wallet.wallet.make_unsigned_transaction(
coins=coins,
outputs=outputs,
fee_policy=self._fee_policy,
rbf=self._rbf)
self._logger.debug('fee: %d, inputs: %d, outputs: %d' % (tx.get_fee(), len(tx.inputs()), len(tx.outputs())))
return tx
def update(self):
if not self._wallet:
self._logger.debug('wallet not set, ignoring update()')
return
try:
# make unsigned transaction
amount = '!' if self._amount.isMax else self._amount.satsInt
tx = self.make_tx(amount=amount)
except NotEnoughFunds:
self.warning = self._wallet.wallet.get_text_not_enough_funds_mentioning_frozen(for_amount=amount)
self._valid = False
self.validChanged.emit()
return
except Exception as e:
self._logger.error(str(e))
self.warning = repr(e)
self._valid = False
self.validChanged.emit()
return
self._tx = tx
amount = self._amount.satsInt if not self._amount.isMax else tx.output_value()
self._effectiveAmount.satsInt = amount
self.effectiveAmountChanged.emit()
self.update_from_tx(tx)
x_fee = run_hook('get_tx_extra_fee', self._wallet.wallet, tx)
if x_fee:
x_fee_address, x_fee_amount = x_fee
self.extraFee = QEAmount(amount_sat=x_fee_amount)
self.update_fee_warning_from_tx(tx=tx, invoice_amt=amount)
if self._amount.isMax and not self.warning:
if reserve_sats := self._wallet.wallet.tx_keeps_ln_utxo_reserve(
tx,
gui_spend_max=self._amount.isMax
):
reserve_str = self._config.format_amount_and_units(reserve_sats)
self.warning = ' '.join([
_('Warning') + ':',
_('Could not spend max: a security reserve of {} was kept for your Lightning channels.')
.format(reserve_str)
])
self._valid = True
self.validChanged.emit()
@pyqtSlot()
def saveOrShow(self):
if not self._valid or not self._tx:
self._logger.debug('no valid tx')
return
saved = False
if self._tx.txid():
if self._wallet.save_tx(self._tx):
saved = True
self.finished.emit(False, saved, self._tx.is_complete())
@pyqtSlot()
def signAndSend(self):
if not self._valid or not self._tx:
self._logger.debug('no valid tx')
return
if self.f_accept:
self.f_accept(self._tx)
return
self._wallet.sign_and_broadcast(self._tx, on_success=partial(self.on_signed_tx, False), on_failure=self.on_sign_failed)
@pyqtSlot()
def sign(self):
if not self._valid or not self._tx:
self._logger.error('no valid tx')
return
self._wallet.sign(self._tx, on_success=partial(self.on_signed_tx, True), on_failure=self.on_sign_failed)
def on_signed_tx(self, save: bool, tx: Transaction):
self._logger.debug('on_signed_tx')
saved = False
if save and self._tx.txid():
if self._wallet.save_tx(self._tx):
saved = True
else:
self._logger.error('Could not save tx')
self.finished.emit(True, saved, tx.is_complete())
def on_sign_failed(self, msg: str = None):
self._logger.debug('on_sign_failed')
self.signError.emit(msg)
@pyqtSlot(result='QVariantList')
def getSerializedTx(self):
txqr = self._tx.to_qr_data()
label = ""
if txid := self._tx.txid():
label = self._wallet.wallet.get_label_for_txid(txid)
return [str(self._tx), txqr[0], txqr[1], label]
class TxMonMixin(QtEventListener):
""" mixin for watching an existing TX based on its txid for verified or removed event.
requires self._wallet to contain a QEWallet instance.
exposes txid qt property.
calls get_tx() once txid is set.
calls tx_verified() and emits txMined signal once tx is verified.
emits txRemoved signal if tx is removed (e.g. replace-by-fee)
"""
txMined = pyqtSignal()
txRemoved = pyqtSignal()
def __init__(self, parent=None):
self._logger.debug('TxMonMixin.__init__')
self._txid = ''
self.register_callbacks()
self.destroyed.connect(lambda: self.on_destroy())
def on_destroy(self):
self.unregister_callbacks()
@event_listener
def on_event_verified(self, wallet, txid, info):
if wallet == self._wallet.wallet and txid == self._txid:
self._logger.debug('verified event for our txid %s' % txid)
self.tx_verified()
self.txMined.emit()
@event_listener
def on_event_removed_transaction(self, wallet, tx):
if wallet == self._wallet.wallet and tx.txid() == self._txid:
self._logger.debug('remove tx for our txid %s' % self._txid)
self.tx_removed()
self.txRemoved.emit()
txidChanged = pyqtSignal()
@pyqtProperty(str, notify=txidChanged)
def txid(self):
return self._txid
@txid.setter
def txid(self, txid):
if self._txid != txid:
self._txid = txid
self.get_tx()
self.txidChanged.emit()
# override
def get_tx(self) -> None:
pass
# override
def tx_verified(self) -> None:
pass
# override
def tx_removed(self) -> None:
pass
class QETxRbfFeeBumper(TxFeeSlider, TxMonMixin):
_logger = get_logger(__name__)
def __init__(self, parent=None):
super().__init__(parent)
self._oldfee = QEAmount()
self._oldfee_rate = 0
self._orig_tx = None
self._rbf = True
self._bump_method = BumpFeeStrategy.PRESERVE_PAYMENT.name
self._bump_methods_available = []
oldfeeChanged = pyqtSignal()
@pyqtProperty(QEAmount, notify=oldfeeChanged)
def oldfee(self):
return self._oldfee
@oldfee.setter
def oldfee(self, oldfee):
if self._oldfee != oldfee:
self._oldfee.copyFrom(oldfee)
self.oldfeeChanged.emit()
oldfeeRateChanged = pyqtSignal()
@pyqtProperty(str, notify=oldfeeRateChanged)
def oldfeeRate(self):
return self._oldfee_rate
@oldfeeRate.setter
def oldfeeRate(self, oldfeerate):
if self._oldfee_rate != oldfeerate:
self._oldfee_rate = oldfeerate
self.oldfeeRateChanged.emit()
bumpMethodChanged = pyqtSignal()
@pyqtProperty(str, notify=bumpMethodChanged)
def bumpMethod(self):
return self._bump_method
@bumpMethod.setter
def bumpMethod(self, bumpmethod: str) -> None:
if self._bump_method != bumpmethod:
self._bump_method = bumpmethod
self.bumpMethodChanged.emit()
self.update()
bumpMethodsAvailableChanged = pyqtSignal()
@pyqtProperty('QVariantList', notify=bumpMethodsAvailableChanged)
def bumpMethodsAvailable(self):
return self._bump_methods_available
def get_tx(self):
assert self._txid
self._orig_tx = self._wallet.wallet.db.get_transaction(self._txid)
assert self._orig_tx
strategies, def_strat_idx = self._wallet.wallet.get_bumpfee_strategies_for_tx(tx=self._orig_tx)
self._bump_methods_available = [{'value': strat.name, 'text': strat.text()} for strat in strategies]
self.bumpMethodsAvailableChanged.emit()
self.bumpMethod = strategies[def_strat_idx].name
if not isinstance(self._orig_tx, PartialTransaction):
self._orig_tx = PartialTransaction.from_tx(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)
self.oldfee = self.fee
self.oldfeeRate = self.feeRate
self.update()
def tx_verified(self):
self._valid = False
self.validChanged.emit()
self.warning = _('Base transaction has been mined')
def tx_removed(self):
self._valid = False
self.validChanged.emit()
self.warning = _('Base transaction disappeared')
def update(self):
if not self._txid or not self._orig_tx:
# not initialized yet
return
fee_per_kb = self._fee_policy.fee_per_kb(self._wallet.wallet.network)
if fee_per_kb is None:
# dynamic method and no network
self._logger.debug('no fee_per_kb')
self.warning = _('Cannot determine dynamic fees, not connected')
return
new_fee_rate = fee_per_kb / 1000
if new_fee_rate <= float(self._oldfee_rate):
self._valid = False
self.validChanged.emit()
self.warning = _("The new fee rate needs to be higher than the old fee rate.")
return
try:
self._tx = self._wallet.wallet.bump_fee(
tx=self._orig_tx,
new_fee_rate=new_fee_rate,
strategy=BumpFeeStrategy[self._bump_method],
)
except CannotBumpFee as e:
self._valid = False
self.validChanged.emit()
self._logger.error(str(e))
self.warning = str(e)
return
else:
self.warning = ''
self._tx.set_rbf(self.rbf)
self.update_from_tx(self._tx)
self.update_fee_warning_from_tx(tx=self._tx, invoice_amt=None)
self._valid = True
self.validChanged.emit()
@pyqtSlot(result=str)
def getNewTx(self):
return str(self._tx)
class QETxCanceller(TxFeeSlider, TxMonMixin):
_logger = get_logger(__name__)
def __init__(self, parent=None):
super().__init__(parent)
self._oldfee = QEAmount()
self._oldfee_rate = 0
self._orig_tx = None
self._txid = ''
self._rbf = True
oldfeeChanged = pyqtSignal()
@pyqtProperty(QEAmount, notify=oldfeeChanged)
def oldfee(self):
return self._oldfee
@oldfee.setter
def oldfee(self, oldfee):
if self._oldfee != oldfee:
self._oldfee.copyFrom(oldfee)
self.oldfeeChanged.emit()
oldfeeRateChanged = pyqtSignal()
@pyqtProperty(str, notify=oldfeeRateChanged)
def oldfeeRate(self):
return self._oldfee_rate
@oldfeeRate.setter
def oldfeeRate(self, oldfeerate):
if self._oldfee_rate != oldfeerate:
self._oldfee_rate = oldfeerate
self.oldfeeRateChanged.emit()
def get_tx(self):
assert 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._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)
self.oldfee = self.fee
self.oldfeeRate = self.feeRate
self.update()
def tx_verified(self):
self._valid = False
self.validChanged.emit()
self.warning = _('Base transaction has been mined')
def tx_removed(self):
self._valid = False
self.validChanged.emit()
self.warning = _('Base transaction disappeared')
def update(self):
if not self._txid or not self._orig_tx:
# not initialized yet
return
fee_per_kb = self._fee_policy.fee_per_kb(self._wallet.wallet.network)
if fee_per_kb is None:
# dynamic method and no network
self._logger.debug('no fee_per_kb')
self.warning = _('Cannot determine dynamic fees, not connected')
return
new_fee_rate = fee_per_kb / 1000
if new_fee_rate <= float(self._oldfee_rate):
self._valid = False
self.validChanged.emit()
self.warning = _("The new fee rate needs to be higher than the old fee rate.")
return
try:
self._tx = self._wallet.wallet.dscancel(
tx=self._orig_tx,
new_fee_rate=new_fee_rate,
)
except CannotDoubleSpendTx as e:
self._valid = False
self.validChanged.emit()
self._logger.error(str(e))
self.warning = str(e)
return
else:
self.warning = ''
self._tx.set_rbf(self.rbf)
self.update_from_tx(self._tx)
self.update_fee_warning_from_tx(tx=self._tx, invoice_amt=None)
self._valid = True
self.validChanged.emit()
@pyqtSlot(result=str)
def getNewTx(self):
return str(self._tx)
class QETxCpfpFeeBumper(TxFeeSlider, TxMonMixin):
_logger = get_logger(__name__)
def __init__(self, parent=None):
super().__init__(parent)
self._input_amount = QEAmount()
self._output_amount = QEAmount()
self._total_fee = QEAmount()
self._total_fee_rate = 0
self._total_size = 0
self._parent_tx = None
self._new_tx = None
self._parent_tx_size = 0
self._parent_fee = 0
self._max_fee = 0
self._txid = ''
self._rbf = True
totalFeeChanged = pyqtSignal()
@pyqtProperty(QEAmount, notify=totalFeeChanged)
def totalFee(self):
return self._total_fee
@totalFee.setter
def totalFee(self, totalfee):
if self._total_fee != totalfee:
self._total_fee.copyFrom(totalfee)
self.totalFeeChanged.emit()
totalFeeRateChanged = pyqtSignal()
@pyqtProperty(str, notify=totalFeeRateChanged)
def totalFeeRate(self):
return self._total_fee_rate
@totalFeeRate.setter
def totalFeeRate(self, totalfeerate):
if self._total_fee_rate != totalfeerate:
self._total_fee_rate = totalfeerate
self.totalFeeRateChanged.emit()
inputAmountChanged = pyqtSignal()
@pyqtProperty(QEAmount, notify=inputAmountChanged)
def inputAmount(self):
return self._input_amount
outputAmountChanged = pyqtSignal()
@pyqtProperty(QEAmount, notify=outputAmountChanged)
def outputAmount(self):
return self._output_amount
totalSizeChanged = pyqtSignal()
@pyqtProperty(int, notify=totalSizeChanged)
def totalSize(self):
return self._total_size
def get_tx(self):
assert self._txid
self._parent_tx = self._wallet.wallet.db.get_transaction(self._txid)
assert self._parent_tx
if isinstance(self._parent_tx, PartialTransaction):
self._logger.error('unexpected PartialTransaction')
return
self._parent_tx_size = self._parent_tx.estimated_size()
self._parent_fee = self._wallet.wallet.get_tx_info(self._parent_tx).fee
if self._parent_fee is None:
self._logger.error(_("Can't CPFP: unknown fee for parent transaction."))
self.warning = _("Can't CPFP: unknown fee for parent transaction.")
return
self._new_tx = self._wallet.wallet.cpfp(self._parent_tx, 0)
self._total_size = self._parent_tx_size + self._new_tx.estimated_size()
self.totalSizeChanged.emit()
self._max_fee = self._new_tx.output_value()
self._input_amount.satsInt = self._max_fee
self.update()
def get_child_fee_from_total_feerate(self, fee_per_kb: Optional[int]) -> Optional[int]:
if fee_per_kb is None:
return None
fee = fee_per_kb * self._total_size / 1000 - self._parent_fee
fee = round(fee)
fee = min(self._max_fee, fee)
fee = max(self._total_size, fee) # pay at least 1 sat/byte for combined size
return fee
def tx_verified(self):
self._valid = False
self.validChanged.emit()
self.warning = _('Base transaction has been mined')
def tx_removed(self):
self._valid = False
self.validChanged.emit()
self.warning = _('Base transaction disappeared')
def update(self):
if not self._txid: # not initialized yet
return
assert self._parent_tx
self._valid = False
self.validChanged.emit()
self.warning = ''
fee_per_kb = self._fee_policy.fee_per_kb(self._wallet.wallet.network)
if fee_per_kb is None:
# dynamic method and no network
self._logger.debug('no fee_per_kb')
self.warning = _('Cannot determine dynamic fees, not connected')
return
if self._parent_fee is None:
self._logger.error(_("Can't CPFP: unknown fee for parent transaction."))
self.warning = _("Can't CPFP: unknown fee for parent transaction.")
return
fee = self.get_child_fee_from_total_feerate(fee_per_kb=fee_per_kb)
if fee is None:
self._logger.warning('no fee')
self.warning = _('No fee')
return
if fee > self._max_fee:
self._logger.warning('max fee exceeded')
self.warning = _('Max fee exceeded')
return
comb_fee = fee + self._parent_fee
comb_feerate = comb_fee / self._total_size
self._fee.satsInt = fee
self._output_amount.satsInt = self._max_fee - fee
self.outputAmountChanged.emit()
self._total_fee.satsInt = fee + self._parent_fee
self._total_fee_rate = str(quantize_feerate(comb_feerate))
try:
self._new_tx = self._wallet.wallet.cpfp(self._parent_tx, fee)
except CannotCPFP as e:
self._logger.error(str(e))
self.warning = str(e)
return
child_feerate = fee / self._new_tx.estimated_size()
self.feeRate = str(quantize_feerate(child_feerate))
self.update_inputs_from_tx(self._new_tx)
self.update_outputs_from_tx(self._new_tx)
self._valid = True
self.validChanged.emit()
@pyqtSlot(result=str)
def getNewTx(self):
return str(self._new_tx)
class QETxSweepFinalizer(QETxFinalizer):
_logger = get_logger(__name__)
txinsRetrieved = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
self._private_keys = ''
self._txins = None
self._amount = QEAmount(is_max=True)
self.txinsRetrieved.connect(self.update)
privateKeysChanged = pyqtSignal()
@pyqtProperty(str, notify=privateKeysChanged)
def privateKeys(self):
return self._private_keys
@privateKeys.setter
def privateKeys(self, private_keys):
if self._private_keys != private_keys:
self._private_keys = private_keys
self.update_privkeys()
self.privateKeysChanged.emit()
def make_sweep_tx(self):
address = self._wallet.wallet.get_receiving_address()
assert self._wallet.wallet.is_mine(address)
coins, keypairs = copy.deepcopy(self._txins)
outputs = [PartialTxOutput.from_address_and_value(address, value='!')]
tx = self._wallet.wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=None, rbf=self._rbf, is_sweep=True)
self._logger.debug('fee: %d, inputs: %d, outputs: %d' % (tx.get_fee(), len(tx.inputs()), len(tx.outputs())))
tx.sign(keypairs)
return tx
def update_privkeys(self):
privkeys = keystore.get_private_keys(self._private_keys)
def fetch_privkeys_info():
try:
self._txins = self._wallet.wallet.network.run_from_another_thread(sweep_preparations(privkeys, self._wallet.wallet.network))
self._logger.debug(f'txins {self._txins!r}')
except UserFacingException as e:
self.warning = str(e)
return
self.txinsRetrieved.emit()
threading.Thread(target=fetch_privkeys_info, daemon=True).start()
def update(self):
if not self._wallet:
self._logger.debug('wallet not set, ignoring update()')
return
if not self._private_keys:
self._logger.debug('private keys not set, ignoring update()')
return
try:
# make unsigned transaction
tx = self.make_sweep_tx()
except Exception as e:
self._logger.error(str(e))
self.warning = repr(e)
self._valid = False
self.validChanged.emit()
return
self._tx = tx
amount = tx.output_value()
self._effectiveAmount.satsInt = amount
self.effectiveAmountChanged.emit()
self.update_from_tx(tx)
self.update_fee_warning_from_tx(tx=self._tx, invoice_amt=amount)
self._valid = True
self.validChanged.emit()
self.on_signed_tx(False, tx)
@pyqtSlot()
def send(self):
self._wallet.broadcast(self._tx)
self._wallet.wallet.set_label(self._tx.txid(), _('Sweep transaction'))