From 8fe7d750f7d6706e252c27dab62000f8e18fc752 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 15 Feb 2021 06:59:08 +0100 Subject: [PATCH] qt: move RBF dialog out of main_window.py into its own file --- electrum/gui/qt/main_window.py | 94 +++---------------- electrum/gui/qt/rbf_dialog.py | 161 +++++++++++++++++++++++++++++++++ 2 files changed, 172 insertions(+), 83 deletions(-) create mode 100644 electrum/gui/qt/rbf_dialog.py diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 50ebbb8df..f626cc4a2 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -100,6 +100,7 @@ from .update_checker import UpdateCheck, UpdateCheckThread from .channels_list import ChannelsList from .confirm_tx_dialog import ConfirmTxDialog from .transaction_dialog import PreviewTxDialog +from .rbf_dialog import BumpFeeDialog, DSCancelDialog if TYPE_CHECKING: from . import ElectrumGui @@ -3264,96 +3265,23 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): return False return True - def _rbf_dialog(self, tx: Transaction, func, title, help_text): + def bump_fee_dialog(self, tx: Transaction): txid = tx.txid() - assert txid if not isinstance(tx, PartialTransaction): tx = PartialTransaction.from_tx(tx) if not self._add_info_to_tx_from_wallet_and_network(tx): return - fee = tx.get_fee() - assert fee is not None - tx_label = self.wallet.get_label_for_txid(txid) - tx_size = tx.estimated_size() - old_fee_rate = fee / tx_size # sat/vbyte - d = WindowModalDialog(self, title) - vbox = QVBoxLayout(d) - vbox.addWidget(WWLabel(help_text)) - - ok_button = OkButton(d) - warning_label = WWLabel('\n') - warning_label.setStyleSheet(ColorScheme.RED.as_stylesheet()) - feerate_e = FeerateEdit(lambda: 0) - feerate_e.setAmount(max(old_fee_rate * 1.5, old_fee_rate + 1)) - def on_feerate(): - fee_rate = feerate_e.get_amount() - warning_text = '\n' - if fee_rate is not None: - try: - new_tx = func(fee_rate) - except Exception as e: - new_tx = None - warning_text = str(e).replace('\n',' ') - else: - new_tx = None - ok_button.setEnabled(new_tx is not None) - warning_label.setText(warning_text) - - feerate_e.textChanged.connect(on_feerate) - def on_slider(dyn, pos, fee_rate): - fee_slider.activate() - if fee_rate is not None: - feerate_e.setAmount(fee_rate / 1000) - fee_slider = FeeSlider(self, self.config, on_slider) - fee_combo = FeeComboBox(fee_slider) - fee_slider.deactivate() - feerate_e.textEdited.connect(fee_slider.deactivate) - - grid = QGridLayout() - grid.addWidget(QLabel(_('Current Fee') + ':'), 0, 0) - grid.addWidget(QLabel(self.format_amount(fee) + ' ' + self.base_unit()), 0, 1) - grid.addWidget(QLabel(_('Current Fee rate') + ':'), 1, 0) - grid.addWidget(QLabel(self.format_fee_rate(1000 * old_fee_rate)), 1, 1) - grid.addWidget(QLabel(_('New Fee rate') + ':'), 2, 0) - grid.addWidget(feerate_e, 2, 1) - grid.addWidget(fee_slider, 3, 1) - grid.addWidget(fee_combo, 3, 2) - vbox.addLayout(grid) - cb = QCheckBox(_('Final')) - vbox.addWidget(cb) - vbox.addWidget(warning_label) - vbox.addLayout(Buttons(CancelButton(d), ok_button)) - if not d.exec_(): - return - is_final = cb.isChecked() - new_fee_rate = feerate_e.get_amount() - try: - new_tx = func(new_fee_rate) - except Exception as e: - self.show_error(str(e)) - return - new_tx.set_rbf(not is_final) - self.show_transaction(new_tx, tx_desc=tx_label) - - def bump_fee_dialog(self, tx: Transaction): - title = _('Bump Fee') - help_text = _("Increase your transaction's fee to improve its position in mempool.") - def func(new_fee_rate): - return self.wallet.bump_fee( - tx=tx, - txid=tx.txid(), - new_fee_rate=new_fee_rate, - coins=self.get_coins()) - self._rbf_dialog(tx, func, title, help_text) + d = BumpFeeDialog(main_window=self, tx=tx, txid=txid) + d.run() def dscancel_dialog(self, tx: Transaction): - title = _('Cancel transaction') - help_text = _( - "Cancel an unconfirmed RBF transaction by double-spending " - "its inputs back to your wallet with a higher fee.") - def func(new_fee_rate): - return self.wallet.dscancel(tx=tx, new_fee_rate=new_fee_rate) - self._rbf_dialog(tx, func, title, help_text) + 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): + return + d = DSCancelDialog(main_window=self, tx=tx, txid=txid) + d.run() def save_transaction_into_wallet(self, tx: Transaction): win = self.top_level_window() diff --git a/electrum/gui/qt/rbf_dialog.py b/electrum/gui/qt/rbf_dialog.py new file mode 100644 index 000000000..3c8c7a0ad --- /dev/null +++ b/electrum/gui/qt/rbf_dialog.py @@ -0,0 +1,161 @@ +# Copyright (C) 2021 The Electrum developers +# Distributed under the MIT software license, see the accompanying +# file LICENCE or http://www.opensource.org/licenses/mit-license.php + +from typing import TYPE_CHECKING + +from PyQt5.QtWidgets import QCheckBox, QLabel, QVBoxLayout, QGridLayout + +from electrum.i18n import _ +from electrum.transaction import PartialTransaction +from .amountedit import FeerateEdit +from .fee_slider import FeeSlider, FeeComboBox +from .util import (ColorScheme, WindowModalDialog, Buttons, + OkButton, WWLabel, CancelButton) + +if TYPE_CHECKING: + from .main_window import ElectrumWindow + + +class _BaseRBFDialog(WindowModalDialog): + + def __init__( + self, + *, + main_window: 'ElectrumWindow', + tx: PartialTransaction, + txid: str, + title: str, + help_text: str, + ): + WindowModalDialog.__init__(self, main_window, title=title) + self.window = main_window + self.wallet = main_window.wallet + self.tx = tx + assert txid + self.txid = txid + + fee = tx.get_fee() + assert fee is not None + tx_size = tx.estimated_size() + old_fee_rate = fee / tx_size # sat/vbyte + vbox = QVBoxLayout(self) + vbox.addWidget(WWLabel(help_text)) + + ok_button = OkButton(self) + warning_label = WWLabel('\n') + warning_label.setStyleSheet(ColorScheme.RED.as_stylesheet()) + self.feerate_e = FeerateEdit(lambda: 0) + self.feerate_e.setAmount(max(old_fee_rate * 1.5, old_fee_rate + 1)) + + def on_feerate(): + fee_rate = self.feerate_e.get_amount() + warning_text = '\n' + if fee_rate is not None: + try: + new_tx = self.rbf_func(fee_rate) + except Exception as e: + new_tx = None + warning_text = str(e).replace('\n', ' ') + else: + new_tx = None + ok_button.setEnabled(new_tx is not None) + warning_label.setText(warning_text) + + self.feerate_e.textChanged.connect(on_feerate) + + def on_slider(dyn, pos, fee_rate): + fee_slider.activate() + if fee_rate is not None: + self.feerate_e.setAmount(fee_rate / 1000) + + fee_slider = FeeSlider(self.window, self.window.config, on_slider) + fee_combo = FeeComboBox(fee_slider) + fee_slider.deactivate() + self.feerate_e.textEdited.connect(fee_slider.deactivate) + + grid = QGridLayout() + grid.addWidget(QLabel(_('Current Fee') + ':'), 0, 0) + grid.addWidget(QLabel(self.window.format_amount(fee) + ' ' + self.window.base_unit()), 0, 1) + grid.addWidget(QLabel(_('Current Fee rate') + ':'), 1, 0) + grid.addWidget(QLabel(self.window.format_fee_rate(1000 * old_fee_rate)), 1, 1) + grid.addWidget(QLabel(_('New Fee rate') + ':'), 2, 0) + grid.addWidget(self.feerate_e, 2, 1) + grid.addWidget(fee_slider, 3, 1) + grid.addWidget(fee_combo, 3, 2) + vbox.addLayout(grid) + self.cb_is_final = QCheckBox(_('Final')) + vbox.addWidget(self.cb_is_final) + vbox.addWidget(warning_label) + vbox.addLayout(Buttons(CancelButton(self), ok_button)) + + def rbf_func(self, fee_rate) -> PartialTransaction: + raise NotImplementedError() # implemented by subclasses + + def run(self) -> None: + if not self.exec_(): + return + is_final = self.cb_is_final.isChecked() + new_fee_rate = self.feerate_e.get_amount() + try: + new_tx = self.rbf_func(new_fee_rate) + except Exception as e: + self.window.show_error(str(e)) + return + new_tx.set_rbf(not is_final) + tx_label = self.wallet.get_label_for_txid(self.txid) + self.window.show_transaction(new_tx, tx_desc=tx_label) + # TODO maybe save tx_label as label for new tx?? + + +class BumpFeeDialog(_BaseRBFDialog): + + def __init__( + self, + *, + main_window: 'ElectrumWindow', + tx: PartialTransaction, + txid: str, + ): + help_text = _("Increase your transaction's fee to improve its position in mempool.") + _BaseRBFDialog.__init__( + self, + main_window=main_window, + tx=tx, + txid=txid, + title=_('Bump Fee'), + help_text=help_text, + ) + + def rbf_func(self, fee_rate): + return self.wallet.bump_fee( + tx=self.tx, + txid=self.txid, + new_fee_rate=fee_rate, + coins=self.window.get_coins(), + ) + + +class DSCancelDialog(_BaseRBFDialog): + + def __init__( + self, + *, + main_window: 'ElectrumWindow', + tx: PartialTransaction, + txid: str, + ): + help_text = _( + "Cancel an unconfirmed RBF transaction by double-spending " + "its inputs back to your wallet with a higher fee.") + _BaseRBFDialog.__init__( + self, + main_window=main_window, + tx=tx, + txid=txid, + title=_('Cancel transaction'), + help_text=help_text, + ) + + def rbf_func(self, fee_rate): + return self.wallet.dscancel(tx=self.tx, new_fee_rate=fee_rate)