Browse Source
- Output selection belongs in the Send tab. - Tx finalization is performed in a confirmation dialog (ConfirmTxDialog or PreviewTxDialog) - the fee slider is shown in the confirmation dialog - coin control works by selecting items in the coins tab - user can save invoices and pay them later - ConfirmTxDialog is used when opening channels and sweeping keysln-negative-red
ThomasV
5 years ago
7 changed files with 655 additions and 502 deletions
@ -0,0 +1,261 @@ |
|||
#!/usr/bin/env python |
|||
# |
|||
# Electrum - lightweight Bitcoin client |
|||
# Copyright (2019) The Electrum Developers |
|||
# |
|||
# Permission is hereby granted, free of charge, to any person |
|||
# obtaining a copy of this software and associated documentation files |
|||
# (the "Software"), to deal in the Software without restriction, |
|||
# including without limitation the rights to use, copy, modify, merge, |
|||
# publish, distribute, sublicense, and/or sell copies of the Software, |
|||
# and to permit persons to whom the Software is furnished to do so, |
|||
# subject to the following conditions: |
|||
# |
|||
# The above copyright notice and this permission notice shall be |
|||
# included in all copies or substantial portions of the Software. |
|||
# |
|||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
|||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
|||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
|||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
|||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
|||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
|||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|||
# SOFTWARE. |
|||
|
|||
from typing import TYPE_CHECKING |
|||
import copy |
|||
|
|||
from PyQt5.QtCore import Qt, QSize |
|||
from PyQt5.QtGui import QTextCharFormat, QBrush, QFont |
|||
from PyQt5.QtWidgets import QHBoxLayout, QVBoxLayout, QLabel, QGridLayout, QPushButton, QWidget, QTextEdit, QLineEdit, QCheckBox |
|||
|
|||
from electrum.i18n import _ |
|||
from electrum.util import quantize_feerate, NotEnoughFunds, NoDynamicFeeEstimates |
|||
from electrum.plugin import run_hook |
|||
from electrum.transaction import TxOutput |
|||
from electrum.simple_config import SimpleConfig, FEERATE_WARNING_HIGH_FEE |
|||
from electrum.wallet import InternalAddressCorruption |
|||
|
|||
from .util import WindowModalDialog, ButtonsLineEdit, ColorScheme, Buttons, CloseButton, FromList, HelpLabel, read_QIcon, char_width_in_lineedit, Buttons, CancelButton, OkButton |
|||
from .util import MONOSPACE_FONT |
|||
|
|||
from .fee_slider import FeeSlider |
|||
from .history_list import HistoryList, HistoryModel |
|||
from .qrtextedit import ShowQRTextEdit |
|||
|
|||
if TYPE_CHECKING: |
|||
from .main_window import ElectrumWindow |
|||
|
|||
|
|||
|
|||
class TxEditor: |
|||
|
|||
def __init__(self, window, inputs, outputs, external_keypairs): |
|||
self.main_window = window |
|||
self.outputs = outputs |
|||
self.get_coins = inputs |
|||
self.tx = None |
|||
self.config = window.config |
|||
self.wallet = window.wallet |
|||
self.external_keypairs = external_keypairs |
|||
self.not_enough_funds = False |
|||
self.no_dynfee_estimates = False |
|||
self.needs_update = False |
|||
self.password_required = self.wallet.has_keystore_encryption() and not external_keypairs |
|||
self.main_window.gui_object.timer.timeout.connect(self.timer_actions) |
|||
|
|||
def timer_actions(self): |
|||
if self.needs_update: |
|||
self.update_tx() |
|||
self.update() |
|||
self.needs_update = False |
|||
|
|||
def fee_slider_callback(self, dyn, pos, fee_rate): |
|||
if dyn: |
|||
if self.config.use_mempool_fees(): |
|||
self.config.set_key('depth_level', pos, False) |
|||
else: |
|||
self.config.set_key('fee_level', pos, False) |
|||
else: |
|||
self.config.set_key('fee_per_kb', fee_rate, False) |
|||
self.needs_update = True |
|||
|
|||
def get_fee_estimator(self): |
|||
return None |
|||
|
|||
def update_tx(self): |
|||
fee_estimator = self.get_fee_estimator() |
|||
is_sweep = bool(self.external_keypairs) |
|||
coins = self.get_coins() |
|||
# deepcopy outputs because '!' is converted to number |
|||
outputs = copy.deepcopy(self.outputs) |
|||
make_tx = lambda fee_est: self.wallet.make_unsigned_transaction( |
|||
coins=coins, |
|||
outputs=outputs, |
|||
fee=fee_est, |
|||
is_sweep=is_sweep) |
|||
try: |
|||
self.tx = make_tx(fee_estimator) |
|||
self.not_enough_funds = False |
|||
self.no_dynfee_estimates = False |
|||
except NotEnoughFunds: |
|||
self.not_enough_funds = True |
|||
self.tx = None |
|||
return |
|||
except NoDynamicFeeEstimates: |
|||
self.no_dynfee_estimates = True |
|||
self.tx = None |
|||
try: |
|||
self.tx = make_tx(0) |
|||
except BaseException: |
|||
return |
|||
except InternalAddressCorruption as e: |
|||
self.tx = None |
|||
self.main_window.show_error(str(e)) |
|||
raise |
|||
except BaseException as e: |
|||
self.tx = None |
|||
self.main_window.logger.exception('') |
|||
self.show_message(str(e)) |
|||
return |
|||
use_rbf = bool(self.config.get('use_rbf', True)) |
|||
if use_rbf: |
|||
self.tx.set_rbf(True) |
|||
|
|||
|
|||
|
|||
|
|||
|
|||
|
|||
class ConfirmTxDialog(TxEditor, WindowModalDialog): |
|||
# set fee and return password (after pw check) |
|||
|
|||
def __init__(self, window: 'ElectrumWindow', inputs, outputs, external_keypairs): |
|||
|
|||
TxEditor.__init__(self, window, inputs, outputs, external_keypairs) |
|||
WindowModalDialog.__init__(self, window, _("Confirm Transaction")) |
|||
vbox = QVBoxLayout() |
|||
self.setLayout(vbox) |
|||
grid = QGridLayout() |
|||
vbox.addLayout(grid) |
|||
self.amount_label = QLabel('') |
|||
grid.addWidget(QLabel(_("Amount to be sent") + ": "), 0, 0) |
|||
grid.addWidget(self.amount_label, 0, 1) |
|||
|
|||
msg = _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\ |
|||
+ _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\ |
|||
+ _('A suggested fee is automatically added to this field. You may override it. The suggested fee increases with the size of the transaction.') |
|||
self.fee_label = QLabel('') |
|||
grid.addWidget(HelpLabel(_("Mining fee") + ": ", msg), 1, 0) |
|||
grid.addWidget(self.fee_label, 1, 1) |
|||
|
|||
self.extra_fee_label = QLabel(_("Additional fees") + ": ") |
|||
self.extra_fee_label.setVisible(False) |
|||
self.extra_fee_value = QLabel('') |
|||
self.extra_fee_value.setVisible(False) |
|||
grid.addWidget(self.extra_fee_label, 2, 0) |
|||
grid.addWidget(self.extra_fee_value, 2, 1) |
|||
|
|||
self.fee_slider = FeeSlider(self, self.config, self.fee_slider_callback) |
|||
grid.addWidget(self.fee_slider, 5, 1) |
|||
|
|||
self.message_label = QLabel(self.default_message()) |
|||
grid.addWidget(self.message_label, 6, 0, 1, -1) |
|||
self.pw_label = QLabel(_('Password')) |
|||
self.pw_label.setVisible(self.password_required) |
|||
self.pw = QLineEdit() |
|||
self.pw.setEchoMode(2) |
|||
self.pw.setVisible(self.password_required) |
|||
grid.addWidget(self.pw_label, 8, 0) |
|||
grid.addWidget(self.pw, 8, 1, 1, -1) |
|||
vbox.addLayout(grid) |
|||
self.preview_button = QPushButton(_('Advanced')) |
|||
self.preview_button.clicked.connect(self.on_preview) |
|||
grid.addWidget(self.preview_button, 0, 2) |
|||
self.send_button = QPushButton(_('Send')) |
|||
self.send_button.clicked.connect(self.on_send) |
|||
self.send_button.setDefault(True) |
|||
vbox.addLayout(Buttons(CancelButton(self), self.send_button)) |
|||
self.update_tx() |
|||
self.update() |
|||
self.is_send = False |
|||
|
|||
def default_message(self): |
|||
return _('Enter your password to proceed') if self.password_required else _('Click Send to proceed') |
|||
|
|||
def on_preview(self): |
|||
self.accept() |
|||
|
|||
def run(self): |
|||
cancelled = not self.exec_() |
|||
password = self.pw.text() or None |
|||
return cancelled, self.is_send, password, self.tx |
|||
|
|||
def on_send(self): |
|||
password = self.pw.text() or None |
|||
if self.password_required: |
|||
if password is None: |
|||
return |
|||
try: |
|||
self.wallet.check_password(password) |
|||
except Exception as e: |
|||
self.main_window.show_error(str(e), parent=self) |
|||
return |
|||
self.is_send = True |
|||
self.accept() |
|||
|
|||
def disable(self, reason): |
|||
self.message_label.setStyleSheet(ColorScheme.RED.as_stylesheet()) |
|||
self.message_label.setText(reason) |
|||
self.pw.setEnabled(False) |
|||
self.send_button.setEnabled(False) |
|||
|
|||
def enable(self): |
|||
self.message_label.setStyleSheet(None) |
|||
self.message_label.setText(self.default_message()) |
|||
self.pw.setEnabled(True) |
|||
self.send_button.setEnabled(True) |
|||
|
|||
def update(self): |
|||
tx = self.tx |
|||
output_values = [x.value for x in self.outputs] |
|||
is_max = '!' in output_values |
|||
amount = tx.output_value() if is_max else sum(output_values) |
|||
self.amount_label.setText(self.main_window.format_amount_and_units(amount)) |
|||
|
|||
if self.not_enough_funds: |
|||
text = _("Not enough funds") |
|||
c, u, x = self.wallet.get_frozen_balance() |
|||
if c+u+x: |
|||
text += " ({} {} {})".format( |
|||
self.main_window.format_amount(c + u + x).strip(), self.main_window.base_unit(), _("are frozen") |
|||
) |
|||
self.disable(text) |
|||
return |
|||
|
|||
if not tx: |
|||
return |
|||
|
|||
fee = tx.get_fee() |
|||
self.fee_label.setText(self.main_window.format_amount_and_units(fee)) |
|||
x_fee = run_hook('get_tx_extra_fee', self.wallet, tx) |
|||
if x_fee: |
|||
x_fee_address, x_fee_amount = x_fee |
|||
self.extra_fee_label.setVisible(True) |
|||
self.extra_fee_value.setVisible(True) |
|||
self.extra_fee_value.setText(self.main_window.format_amount_and_units(x_fee_amount)) |
|||
|
|||
feerate_warning = FEERATE_WARNING_HIGH_FEE |
|||
low_fee = fee < self.wallet.relayfee() * tx.estimated_size() / 1000 |
|||
high_fee = fee > feerate_warning * tx.estimated_size() / 1000 |
|||
if low_fee: |
|||
msg = '\n'.join([ |
|||
_("This transaction requires a higher fee, or it will not be propagated by your current server"), |
|||
_("Try to raise your transaction fee, or use a server with a lower relay fee.") |
|||
]) |
|||
self.disable(msg) |
|||
elif high_fee: |
|||
self.disable(_('Warning') + ': ' + _("The fee for this transaction seems unusually high.")) |
|||
else: |
|||
self.enable() |
Loading…
Reference in new issue