You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

964 lines
29 KiB

import copy
import threading
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, 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 .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._inputs = []
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()
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()
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.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
# 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
#self.wallet.adb.get_txin_value(txin)
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()
})
self.inputs = inputs
def update_outputs_from_tx(self, tx):
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())
})
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())
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'])
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)
self.update_fee_warning_from_tx(tx=tx, invoice_amt=amount)
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)
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,
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 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.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 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.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'))