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