gui/qt/rbf_dialog.py (old) lines 57-64 were implementing logic that should not be part of GUI code. Case in point, gui/qml/qetxfinalizer.py (old) lines 511-513 duplicated half of that logic but not the other half. That logic is now moved to wallet.get_bumpfee_strategies_for_tx(). More context: a user on irc got confused when using the qml gui. They were sending "max" and wanted to bump_fee. The qml gui selected the "preserve_payment" strategy by default, using which there was no solution, and the user did not notice that the strategy can be altered (via the "method" dropdown). The qt gui had logic to select "decrease_payment" by default in such a case (which does find a solution to bump) but this logic was not duplicated in the qml gui. Instead of duplicating the logic, this commit moves it to shared lib code.
840 lines
25 KiB
Python
840 lines
25 KiB
Python
from decimal import Decimal
|
|
from typing import Optional, TYPE_CHECKING
|
|
from functools import partial
|
|
|
|
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
|
|
|
|
from electrum.logging import get_logger
|
|
from electrum.i18n import _
|
|
from electrum.transaction import PartialTxOutput, PartialTransaction, Transaction
|
|
from electrum.util import NotEnoughFunds, profiler
|
|
from electrum.wallet import CannotBumpFee, CannotDoubleSpendTx, CannotCPFP, BumpFeeStrategy
|
|
from electrum.plugin import run_hook
|
|
|
|
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):
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
|
|
self._wallet = None # type: Optional[QEWallet]
|
|
self._sliderSteps = 0
|
|
self._sliderPos = 0
|
|
self._method = -1
|
|
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):
|
|
return self._method
|
|
|
|
@method.setter
|
|
def method(self, method):
|
|
if self._method != method:
|
|
self._method = method
|
|
self.update_slider()
|
|
self.methodChanged.emit()
|
|
self.save_config()
|
|
|
|
def get_method(self):
|
|
dynfees = self._method > 0
|
|
mempool = self._method == 2
|
|
return dynfees, mempool
|
|
|
|
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):
|
|
dynfees, mempool = self.get_method()
|
|
maxp, pos, fee_rate = self._config.get_fee_slider(dynfees, mempool)
|
|
self._sliderSteps = maxp
|
|
self._sliderPos = pos
|
|
self.sliderStepsChanged.emit()
|
|
self.sliderPosChanged.emit()
|
|
|
|
def update_target(self):
|
|
target, tooltip, dyn = self._config.get_fee_target()
|
|
self.target = target
|
|
|
|
def read_config(self):
|
|
mempool = self._config.use_mempool_fees()
|
|
dynfees = self._config.is_dynfee()
|
|
self._method = (2 if mempool else 1) if dynfees else 0
|
|
self.update_slider()
|
|
self.methodChanged.emit()
|
|
self.update_target()
|
|
self.update()
|
|
|
|
def save_config(self):
|
|
value = int(self._sliderPos)
|
|
dynfees, mempool = self.get_method()
|
|
self._config.FEE_EST_DYNAMIC = dynfees
|
|
self._config.FEE_EST_USE_MEMPOOL = mempool
|
|
if dynfees:
|
|
if mempool:
|
|
self._config.FEE_EST_DYNAMIC_MEMPOOL_SLIDERPOS = value
|
|
else:
|
|
self._config.FEE_EST_DYNAMIC_ETA_SLIDERPOS = value
|
|
else:
|
|
self._config.FEE_EST_STATIC_FEERATE = self._config.static_fee(value)
|
|
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._outputs = []
|
|
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()
|
|
|
|
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()
|
|
|
|
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
|
|
|
|
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.update_outputs_from_tx(tx)
|
|
|
|
def update_outputs_from_tx(self, tx):
|
|
outputs = []
|
|
for o in tx.outputs():
|
|
outputs.append({
|
|
'address': o.get_ui_address_str(),
|
|
'value': o.value,
|
|
'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())
|
|
})
|
|
self.outputs = outputs
|
|
|
|
|
|
class QETxFinalizer(TxFeeSlider):
|
|
_logger = get_logger(__name__)
|
|
|
|
finished = pyqtSignal([bool, bool, bool], arguments=['signed', 'saved', 'complete'])
|
|
|
|
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)
|
|
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=None, 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
|
|
tx = self.make_tx(amount='!' if self._amount.isMax else self._amount.satsInt)
|
|
except NotEnoughFunds:
|
|
self.warning = _("Not enough funds")
|
|
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)
|
|
|
|
fee_warning_tuple = self._wallet.wallet.get_tx_fee_warning(
|
|
invoice_amt=amount, tx_size=tx.estimated_size(), fee=tx.get_fee())
|
|
if fee_warning_tuple:
|
|
allow_send, long_warning, short_warning = fee_warning_tuple
|
|
self.warning = _('Warning') + ': ' + long_warning
|
|
else:
|
|
self.warning = ''
|
|
|
|
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(self._tx, broadcast=True, on_success=partial(self.on_signed_tx, False))
|
|
|
|
@pyqtSlot()
|
|
def sign(self):
|
|
if not self._valid or not self._tx:
|
|
self._logger.error('no valid tx')
|
|
return
|
|
|
|
self._wallet.sign(self._tx, broadcast=False, on_success=partial(self.on_signed_tx, True))
|
|
|
|
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())
|
|
|
|
@pyqtSlot(result='QVariantList')
|
|
def getSerializedTx(self):
|
|
txqr = self._tx.to_qr_data()
|
|
return [str(self._tx), txqr[0], txqr[1]]
|
|
|
|
|
|
class TxMonMixin(QtEventListener):
|
|
""" mixin for watching an existing TX based on its txid for verified 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.
|
|
"""
|
|
txMined = 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()
|
|
|
|
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
|
|
|
|
|
|
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, txid=self._txid)
|
|
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 update(self):
|
|
if not self._txid or not self._orig_tx:
|
|
# not initialized yet
|
|
return
|
|
|
|
fee_per_kb = self._config.fee_per_kb()
|
|
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,
|
|
txid=self._txid,
|
|
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)
|
|
|
|
# TODO: deduce amount sent?
|
|
# TODO: we don't handle send-max txs correctly yet
|
|
# fee_warning_tuple = self._wallet.wallet.get_tx_fee_warning(
|
|
# invoice_amt=amount, tx_size=tx.estimated_size(), fee=tx.get_fee())
|
|
# if fee_warning_tuple:
|
|
# allow_send, long_warning, short_warning = fee_warning_tuple
|
|
# self.warning = long_warning
|
|
# else:
|
|
# self.warning = ''
|
|
|
|
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 update(self):
|
|
if not self._txid or not self._orig_tx:
|
|
# not initialized yet
|
|
return
|
|
|
|
fee_per_kb = self._config.fee_per_kb()
|
|
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._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._fee_for_child = 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()
|
|
|
|
feeForChildChanged = pyqtSignal()
|
|
@pyqtProperty(QEAmount, notify=feeForChildChanged)
|
|
def feeForChild(self):
|
|
return self._fee_for_child
|
|
|
|
@feeForChild.setter
|
|
def feeForChild(self, feeforchild):
|
|
if self._fee_for_child != feeforchild:
|
|
self._fee_for_child.copyFrom(feeforchild)
|
|
self.feeForChildChanged.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.adb.get_tx_fee(self._txid)
|
|
|
|
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 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._config.fee_per_kb()
|
|
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_for_child.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 = f'{comb_feerate:.1f}'
|
|
|
|
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
|
|
|
|
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)
|