mirror of
https://github.com/LBRYFoundation/LBRY-Vault.git
synced 2025-08-23 09:37:31 +00:00
GUI: Separate output selection and transaction finalization.
- 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 keys
This commit is contained in:
parent
f8c84fbb1e
commit
dd6cb2caf7
7 changed files with 655 additions and 502 deletions
|
@ -1,6 +1,7 @@
|
|||
# Release 4.0 - (Not released yet; release notes are incomplete)
|
||||
|
||||
* Lightning Network
|
||||
* Qt GUI: Separation between output selection and transaction finalization.
|
||||
* Http PayServer can be configured from GUI
|
||||
|
||||
# Release 3.3.8 - (July 11, 2019)
|
||||
|
|
261
electrum/gui/qt/confirm_tx_dialog.py
Normal file
261
electrum/gui/qt/confirm_tx_dialog.py
Normal file
|
@ -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()
|
|
@ -27,6 +27,7 @@ from enum import IntEnum
|
|||
|
||||
from PyQt5.QtCore import Qt, QItemSelectionModel
|
||||
from PyQt5.QtGui import QStandardItemModel, QStandardItem, QFont
|
||||
from PyQt5.QtWidgets import QAbstractItemView
|
||||
from PyQt5.QtWidgets import QHeaderView, QMenu, QVBoxLayout, QGridLayout, QLabel, QTreeWidget, QTreeWidgetItem
|
||||
|
||||
from electrum.i18n import _
|
||||
|
@ -70,6 +71,7 @@ class InvoiceList(MyTreeView):
|
|||
editable_columns=[])
|
||||
self.setSortingEnabled(True)
|
||||
self.setModel(QStandardItemModel(self))
|
||||
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
||||
self.update()
|
||||
|
||||
def update_item(self, key, status):
|
||||
|
@ -143,6 +145,10 @@ class InvoiceList(MyTreeView):
|
|||
export_meta_gui(self.parent, _('invoices'), self.parent.invoices.export_file)
|
||||
|
||||
def create_menu(self, position):
|
||||
items = self.selected_in_column(0)
|
||||
if len(items) > 1:
|
||||
print(items)
|
||||
return
|
||||
idx = self.indexAt(position)
|
||||
item = self.model().itemFromIndex(idx)
|
||||
item_col0 = self.model().itemFromIndex(idx.sibling(idx.row(), self.Columns.DATE))
|
||||
|
|
|
@ -95,6 +95,8 @@ from .installwizard import WIF_HELP_TEXT
|
|||
from .history_list import HistoryList, HistoryModel
|
||||
from .update_checker import UpdateCheck, UpdateCheckThread
|
||||
from .channels_list import ChannelsList
|
||||
from .confirm_tx_dialog import ConfirmTxDialog
|
||||
from .transaction_dialog import PreviewTxDialog
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import ElectrumGui
|
||||
|
@ -153,11 +155,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
self.payto_URI = None
|
||||
self.checking_accounts = False
|
||||
self.qr_window = None
|
||||
self.not_enough_funds = False
|
||||
self.pluginsdialog = None
|
||||
self.require_fee_update = False
|
||||
self.tl_windows = []
|
||||
self.tx_external_keypairs = {}
|
||||
Logger.__init__(self)
|
||||
|
||||
self.tx_notification_queue = queue.Queue()
|
||||
|
@ -174,8 +174,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
|
||||
self.completions = QStringListModel()
|
||||
|
||||
self.send_tab_is_onchain = False
|
||||
|
||||
self.tabs = tabs = QTabWidget(self)
|
||||
self.send_tab = self.create_send_tab()
|
||||
self.receive_tab = self.create_receive_tab()
|
||||
|
@ -244,7 +242,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
self.console.showMessage(self.network.banner)
|
||||
|
||||
# update fee slider in case we missed the callback
|
||||
self.fee_slider.update()
|
||||
#self.fee_slider.update()
|
||||
self.load_wallet(wallet)
|
||||
gui_object.timer.timeout.connect(self.timer_actions)
|
||||
self.fetch_alias()
|
||||
|
@ -397,11 +395,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
self.history_model.update_tx_mined_status(tx_hash, tx_mined_status)
|
||||
elif event == 'fee':
|
||||
if self.config.is_dynfee():
|
||||
self.fee_slider.update()
|
||||
#self.fee_slider.update()
|
||||
self.require_fee_update = True
|
||||
elif event == 'fee_histogram':
|
||||
if self.config.is_dynfee():
|
||||
self.fee_slider.update()
|
||||
#self.fee_slider.update()
|
||||
self.require_fee_update = True
|
||||
self.history_model.on_fee_histogram()
|
||||
else:
|
||||
|
@ -769,7 +767,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
self.payto_e.resolve()
|
||||
# update fee
|
||||
if self.require_fee_update:
|
||||
self.do_update_fee()
|
||||
#self.do_update_fee()
|
||||
self.require_fee_update = False
|
||||
self.notify_transactions()
|
||||
|
||||
|
@ -946,7 +944,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
if not self.fx or not self.fx.is_enabled():
|
||||
self.fiat_receive_e.setVisible(False)
|
||||
grid.addWidget(self.fiat_receive_e, 1, 2, Qt.AlignLeft)
|
||||
|
||||
self.connect_fields(self, self.receive_amount_e, self.fiat_receive_e, None)
|
||||
self.connect_fields(self, self.amount_e, self.fiat_send_e, None)
|
||||
|
||||
self.expires_combo = QComboBox()
|
||||
evl = sorted(pr_expiration_values.items())
|
||||
|
@ -1179,10 +1179,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
self.receive_address_e.setStyleSheet("")
|
||||
self.receive_address_e.setToolTip("")
|
||||
|
||||
def set_feerounding_text(self, num_satoshis_added):
|
||||
self.feerounding_text = (_('Additional {} satoshis are going to be added.')
|
||||
.format(num_satoshis_added))
|
||||
|
||||
def create_send_tab(self):
|
||||
# A 4-column grid layout. All the stretch is in the last column.
|
||||
# The exchange rate plugin adds a fiat widget in column 2
|
||||
|
@ -1232,131 +1228,18 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
self.max_button.setCheckable(True)
|
||||
grid.addWidget(self.max_button, 3, 3)
|
||||
|
||||
self.from_label = QLabel(_('From'))
|
||||
grid.addWidget(self.from_label, 4, 0)
|
||||
self.from_list = FromList(self, self.from_list_menu)
|
||||
grid.addWidget(self.from_list, 4, 1, 1, -1)
|
||||
self.set_pay_from([])
|
||||
|
||||
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_e_label = HelpLabel(_('Fee'), msg)
|
||||
|
||||
def fee_cb(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)
|
||||
|
||||
if fee_rate:
|
||||
fee_rate = Decimal(fee_rate)
|
||||
self.feerate_e.setAmount(quantize_feerate(fee_rate / 1000))
|
||||
else:
|
||||
self.feerate_e.setAmount(None)
|
||||
self.fee_e.setModified(False)
|
||||
|
||||
self.fee_slider.activate()
|
||||
self.spend_max() if self.max_button.isChecked() else self.update_fee()
|
||||
|
||||
self.fee_slider = FeeSlider(self, self.config, fee_cb)
|
||||
self.fee_slider.setFixedWidth(self.amount_e.width())
|
||||
|
||||
def on_fee_or_feerate(edit_changed, editing_finished):
|
||||
edit_other = self.feerate_e if edit_changed == self.fee_e else self.fee_e
|
||||
if editing_finished:
|
||||
if edit_changed.get_amount() is None:
|
||||
# This is so that when the user blanks the fee and moves on,
|
||||
# we go back to auto-calculate mode and put a fee back.
|
||||
edit_changed.setModified(False)
|
||||
else:
|
||||
# edit_changed was edited just now, so make sure we will
|
||||
# freeze the correct fee setting (this)
|
||||
edit_other.setModified(False)
|
||||
self.fee_slider.deactivate()
|
||||
self.update_fee()
|
||||
|
||||
class TxSizeLabel(QLabel):
|
||||
def setAmount(self, byte_size):
|
||||
self.setText(('x %s bytes =' % byte_size) if byte_size else '')
|
||||
|
||||
self.size_e = TxSizeLabel()
|
||||
self.size_e.setAlignment(Qt.AlignCenter)
|
||||
self.size_e.setAmount(0)
|
||||
self.size_e.setFixedWidth(self.amount_e.width())
|
||||
self.size_e.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet())
|
||||
|
||||
self.feerate_e = FeerateEdit(lambda: 0)
|
||||
self.feerate_e.setAmount(self.config.fee_per_byte())
|
||||
self.feerate_e.textEdited.connect(partial(on_fee_or_feerate, self.feerate_e, False))
|
||||
self.feerate_e.editingFinished.connect(partial(on_fee_or_feerate, self.feerate_e, True))
|
||||
|
||||
self.fee_e = BTCAmountEdit(self.get_decimal_point)
|
||||
self.fee_e.textEdited.connect(partial(on_fee_or_feerate, self.fee_e, False))
|
||||
self.fee_e.editingFinished.connect(partial(on_fee_or_feerate, self.fee_e, True))
|
||||
|
||||
def feerounding_onclick():
|
||||
text = (self.feerounding_text + '\n\n' +
|
||||
_('To somewhat protect your privacy, Electrum tries to create change with similar precision to other outputs.') + ' ' +
|
||||
_('At most 100 satoshis might be lost due to this rounding.') + ' ' +
|
||||
_("You can disable this setting in '{}'.").format(_('Preferences')) + '\n' +
|
||||
_('Also, dust is not kept as change, but added to the fee.') + '\n' +
|
||||
_('Also, when batching RBF transactions, BIP 125 imposes a lower bound on the fee.'))
|
||||
self.show_message(title=_('Fee rounding'), msg=text)
|
||||
|
||||
self.feerounding_icon = QPushButton(read_QIcon('info.png'), '')
|
||||
self.feerounding_icon.setFixedWidth(round(2.2 * char_width_in_lineedit()))
|
||||
self.feerounding_icon.setFlat(True)
|
||||
self.feerounding_icon.clicked.connect(feerounding_onclick)
|
||||
self.feerounding_icon.setVisible(False)
|
||||
|
||||
self.connect_fields(self, self.amount_e, self.fiat_send_e, self.fee_e)
|
||||
|
||||
vbox_feelabel = QVBoxLayout()
|
||||
vbox_feelabel.addWidget(self.fee_e_label)
|
||||
vbox_feelabel.addStretch(1)
|
||||
grid.addLayout(vbox_feelabel, 5, 0)
|
||||
|
||||
self.fee_adv_controls = QWidget()
|
||||
hbox = QHBoxLayout(self.fee_adv_controls)
|
||||
hbox.setContentsMargins(0, 0, 0, 0)
|
||||
hbox.addWidget(self.feerate_e)
|
||||
hbox.addWidget(self.size_e)
|
||||
hbox.addWidget(self.fee_e)
|
||||
hbox.addWidget(self.feerounding_icon, Qt.AlignLeft)
|
||||
hbox.addStretch(1)
|
||||
|
||||
self.feecontrol_fields = QWidget()
|
||||
vbox_feecontrol = QVBoxLayout(self.feecontrol_fields)
|
||||
vbox_feecontrol.setContentsMargins(0, 0, 0, 0)
|
||||
vbox_feecontrol.addWidget(self.fee_adv_controls)
|
||||
vbox_feecontrol.addWidget(self.fee_slider)
|
||||
|
||||
grid.addWidget(self.feecontrol_fields, 5, 1, 1, -1)
|
||||
|
||||
if not self.config.get('show_fee', False):
|
||||
self.fee_adv_controls.setVisible(False)
|
||||
|
||||
self.save_button = EnterButton(_("Save"), self.do_save_invoice)
|
||||
self.preview_button = EnterButton(_("Preview"), self.do_preview)
|
||||
self.preview_button.setToolTip(_('Display the details of your transaction before signing it.'))
|
||||
self.send_button = EnterButton(_("Send"), self.do_pay)
|
||||
self.send_button = EnterButton(_("Pay"), self.do_pay)
|
||||
self.clear_button = EnterButton(_("Clear"), self.do_clear)
|
||||
|
||||
buttons = QHBoxLayout()
|
||||
buttons.addStretch(1)
|
||||
buttons.addWidget(self.clear_button)
|
||||
buttons.addWidget(self.save_button)
|
||||
buttons.addWidget(self.preview_button)
|
||||
buttons.addWidget(self.send_button)
|
||||
grid.addLayout(buttons, 6, 1, 1, 4)
|
||||
|
||||
self.amount_e.shortcut.connect(self.spend_max)
|
||||
self.payto_e.textChanged.connect(self.update_fee)
|
||||
self.amount_e.textEdited.connect(self.update_fee)
|
||||
|
||||
def reset_max(text):
|
||||
self.max_button.setChecked(False)
|
||||
|
@ -1365,45 +1248,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
self.amount_e.textEdited.connect(reset_max)
|
||||
self.fiat_send_e.textEdited.connect(reset_max)
|
||||
|
||||
def entry_changed():
|
||||
text = ""
|
||||
|
||||
amt_color = ColorScheme.DEFAULT
|
||||
fee_color = ColorScheme.DEFAULT
|
||||
feerate_color = ColorScheme.DEFAULT
|
||||
|
||||
if self.not_enough_funds:
|
||||
amt_color, fee_color = ColorScheme.RED, ColorScheme.RED
|
||||
feerate_color = ColorScheme.RED
|
||||
text = _("Not enough funds")
|
||||
c, u, x = self.wallet.get_frozen_balance()
|
||||
if c+u+x:
|
||||
text += " ({} {} {})".format(
|
||||
self.format_amount(c + u + x).strip(), self.base_unit(), _("are frozen")
|
||||
)
|
||||
|
||||
# blue color denotes auto-filled values
|
||||
elif self.fee_e.isModified():
|
||||
feerate_color = ColorScheme.BLUE
|
||||
elif self.feerate_e.isModified():
|
||||
fee_color = ColorScheme.BLUE
|
||||
elif self.amount_e.isModified():
|
||||
fee_color = ColorScheme.BLUE
|
||||
feerate_color = ColorScheme.BLUE
|
||||
else:
|
||||
amt_color = ColorScheme.BLUE
|
||||
fee_color = ColorScheme.BLUE
|
||||
feerate_color = ColorScheme.BLUE
|
||||
|
||||
self.statusBar().showMessage(text)
|
||||
self.amount_e.setStyleSheet(amt_color.as_stylesheet())
|
||||
self.fee_e.setStyleSheet(fee_color.as_stylesheet())
|
||||
self.feerate_e.setStyleSheet(feerate_color.as_stylesheet())
|
||||
|
||||
self.amount_e.textChanged.connect(entry_changed)
|
||||
self.fee_e.textChanged.connect(entry_changed)
|
||||
self.feerate_e.textChanged.connect(entry_changed)
|
||||
|
||||
self.set_onchain(False)
|
||||
|
||||
self.invoices_label = QLabel(_('Outgoing payments'))
|
||||
|
@ -1430,144 +1274,12 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
if run_hook('abort_send', self):
|
||||
return
|
||||
self.max_button.setChecked(True)
|
||||
self.do_update_fee()
|
||||
|
||||
def update_fee(self):
|
||||
self.require_fee_update = True
|
||||
|
||||
def get_payto_or_dummy(self) -> bytes:
|
||||
r = self.payto_e.get_destination_scriptpubkey()
|
||||
if r:
|
||||
return r
|
||||
return bfh(bitcoin.address_to_script(self.wallet.dummy_address()))
|
||||
|
||||
def do_update_fee(self):
|
||||
'''Recalculate the fee. If the fee was manually input, retain it, but
|
||||
still build the TX to see if there are enough funds.
|
||||
'''
|
||||
if not self.is_onchain:
|
||||
return
|
||||
freeze_fee = self.is_send_fee_frozen()
|
||||
freeze_feerate = self.is_send_feerate_frozen()
|
||||
amount = '!' if self.max_button.isChecked() else self.amount_e.get_amount()
|
||||
if amount is None:
|
||||
if not freeze_fee:
|
||||
self.fee_e.setAmount(None)
|
||||
self.not_enough_funds = False
|
||||
self.statusBar().showMessage('')
|
||||
return
|
||||
|
||||
outputs = self.read_outputs()
|
||||
fee_estimator = self.get_send_fee_estimator()
|
||||
coins = self.get_coins()
|
||||
|
||||
if not outputs:
|
||||
scriptpubkey = self.get_payto_or_dummy()
|
||||
outputs = [PartialTxOutput(scriptpubkey=scriptpubkey, value=amount)]
|
||||
is_sweep = bool(self.tx_external_keypairs)
|
||||
make_tx = lambda fee_est: \
|
||||
self.wallet.make_unsigned_transaction(
|
||||
coins=coins,
|
||||
outputs=outputs,
|
||||
fee=fee_est,
|
||||
is_sweep=is_sweep)
|
||||
try:
|
||||
tx = make_tx(fee_estimator)
|
||||
self.not_enough_funds = False
|
||||
except (NotEnoughFunds, NoDynamicFeeEstimates) as e:
|
||||
if not freeze_fee:
|
||||
self.fee_e.setAmount(None)
|
||||
if not freeze_feerate:
|
||||
self.feerate_e.setAmount(None)
|
||||
self.feerounding_icon.setVisible(False)
|
||||
|
||||
if isinstance(e, NotEnoughFunds):
|
||||
self.not_enough_funds = True
|
||||
elif isinstance(e, NoDynamicFeeEstimates):
|
||||
try:
|
||||
tx = make_tx(0)
|
||||
size = tx.estimated_size()
|
||||
self.size_e.setAmount(size)
|
||||
except BaseException:
|
||||
pass
|
||||
return
|
||||
except BaseException:
|
||||
self.logger.exception('')
|
||||
return
|
||||
|
||||
size = tx.estimated_size()
|
||||
self.size_e.setAmount(size)
|
||||
|
||||
fee = tx.get_fee()
|
||||
fee = None if self.not_enough_funds else fee
|
||||
|
||||
# Displayed fee/fee_rate values are set according to user input.
|
||||
# Due to rounding or dropping dust in CoinChooser,
|
||||
# actual fees often differ somewhat.
|
||||
if freeze_feerate or self.fee_slider.is_active():
|
||||
displayed_feerate = self.feerate_e.get_amount()
|
||||
if displayed_feerate is not None:
|
||||
displayed_feerate = quantize_feerate(displayed_feerate)
|
||||
else:
|
||||
# fallback to actual fee
|
||||
displayed_feerate = quantize_feerate(fee / size) if fee is not None else None
|
||||
self.feerate_e.setAmount(displayed_feerate)
|
||||
displayed_fee = round(displayed_feerate * size) if displayed_feerate is not None else None
|
||||
self.fee_e.setAmount(displayed_fee)
|
||||
else:
|
||||
if freeze_fee:
|
||||
displayed_fee = self.fee_e.get_amount()
|
||||
else:
|
||||
# fallback to actual fee if nothing is frozen
|
||||
displayed_fee = fee
|
||||
self.fee_e.setAmount(displayed_fee)
|
||||
displayed_fee = displayed_fee if displayed_fee else 0
|
||||
displayed_feerate = quantize_feerate(displayed_fee / size) if displayed_fee is not None else None
|
||||
self.feerate_e.setAmount(displayed_feerate)
|
||||
|
||||
# show/hide fee rounding icon
|
||||
feerounding = (fee - displayed_fee) if fee else 0
|
||||
self.set_feerounding_text(int(feerounding))
|
||||
self.feerounding_icon.setToolTip(self.feerounding_text)
|
||||
self.feerounding_icon.setVisible(abs(feerounding) >= 1)
|
||||
|
||||
if self.max_button.isChecked():
|
||||
amount = tx.output_value()
|
||||
__, x_fee_amount = run_hook('get_tx_extra_fee', self.wallet, tx) or (None, 0)
|
||||
amount_after_all_fees = amount - x_fee_amount
|
||||
self.amount_e.setAmount(amount_after_all_fees)
|
||||
|
||||
def from_list_delete(self, item):
|
||||
i = self.from_list.indexOfTopLevelItem(item)
|
||||
self.pay_from.pop(i)
|
||||
self.redraw_from_list()
|
||||
self.update_fee()
|
||||
|
||||
def from_list_menu(self, position):
|
||||
item = self.from_list.itemAt(position)
|
||||
menu = QMenu()
|
||||
menu.addAction(_("Remove"), lambda: self.from_list_delete(item))
|
||||
menu.exec_(self.from_list.viewport().mapToGlobal(position))
|
||||
|
||||
def set_pay_from(self, coins: Sequence[PartialTxInput]):
|
||||
self.pay_from = list(coins)
|
||||
self.redraw_from_list()
|
||||
|
||||
def redraw_from_list(self):
|
||||
self.from_list.clear()
|
||||
self.from_label.setHidden(len(self.pay_from) == 0)
|
||||
self.from_list.setHidden(len(self.pay_from) == 0)
|
||||
|
||||
def format(txin: PartialTxInput):
|
||||
h = txin.prevout.txid.hex()
|
||||
out_idx = txin.prevout.out_idx
|
||||
addr = txin.address
|
||||
return h[0:10] + '...' + h[-10:] + ":%d"%out_idx + '\t' + addr + '\t'
|
||||
|
||||
for coin in self.pay_from:
|
||||
item = QTreeWidgetItem([format(coin), self.format_amount(coin.value_sats())])
|
||||
item.setFont(0, QFont(MONOSPACE_FONT))
|
||||
self.from_list.addTopLevelItem(item)
|
||||
amount = sum(x.value_sats() for x in self.get_coins())
|
||||
self.amount_e.setAmount(amount)
|
||||
## substract extra fee
|
||||
#__, x_fee_amount = run_hook('get_tx_extra_fee', self.wallet, tx) or (None, 0)
|
||||
#amount_after_all_fees = amount - x_fee_amount
|
||||
#self.amount_e.setAmount(amount_after_all_fees)
|
||||
|
||||
def get_contact_payto(self, key):
|
||||
_type, label = self.contacts.get(key)
|
||||
|
@ -1605,26 +1317,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
def protect(self, func, args, password):
|
||||
return func(*args, password)
|
||||
|
||||
def is_send_fee_frozen(self):
|
||||
return self.fee_e.isVisible() and self.fee_e.isModified() \
|
||||
and (self.fee_e.text() or self.fee_e.hasFocus())
|
||||
|
||||
def is_send_feerate_frozen(self):
|
||||
return self.feerate_e.isVisible() and self.feerate_e.isModified() \
|
||||
and (self.feerate_e.text() or self.feerate_e.hasFocus())
|
||||
|
||||
def get_send_fee_estimator(self):
|
||||
if self.is_send_fee_frozen():
|
||||
fee_estimator = self.fee_e.get_amount()
|
||||
elif self.is_send_feerate_frozen():
|
||||
amount = self.feerate_e.get_amount() # sat/byte feerate
|
||||
amount = 0 if amount is None else amount * 1000 # sat/kilobyte feerate
|
||||
fee_estimator = partial(
|
||||
simple_config.SimpleConfig.estimate_fee_for_feerate, amount)
|
||||
else:
|
||||
fee_estimator = None
|
||||
return fee_estimator
|
||||
|
||||
def read_outputs(self) -> List[PartialTxOutput]:
|
||||
if self.payment_request:
|
||||
outputs = self.payment_request.get_outputs()
|
||||
|
@ -1734,115 +1426,69 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
self.do_clear()
|
||||
self.invoice_list.update()
|
||||
|
||||
def do_preview(self):
|
||||
self.do_pay(preview=True)
|
||||
|
||||
def do_pay(self, preview=False):
|
||||
def do_pay(self):
|
||||
invoice = self.read_invoice()
|
||||
if not invoice:
|
||||
return
|
||||
self.wallet.save_invoice(invoice)
|
||||
self.invoice_list.update()
|
||||
self.do_pay_invoice(invoice, preview)
|
||||
self.do_clear()
|
||||
self.do_pay_invoice(invoice)
|
||||
|
||||
def do_pay_invoice(self, invoice, preview=False):
|
||||
def do_pay_invoice(self, invoice):
|
||||
if invoice['type'] == PR_TYPE_LN:
|
||||
self.pay_lightning_invoice(invoice['invoice'])
|
||||
return
|
||||
elif invoice['type'] == PR_TYPE_ONCHAIN:
|
||||
message = invoice['message']
|
||||
outputs = invoice['outputs'] # type: List[PartialTxOutput]
|
||||
outputs = invoice['outputs']
|
||||
self.pay_onchain_dialog(self.get_coins, outputs, invoice=invoice)
|
||||
else:
|
||||
raise Exception('unknown invoice type')
|
||||
|
||||
def get_coins(self):
|
||||
coins = self.utxo_list.get_spend_list()
|
||||
return coins or self.wallet.get_spendable_coins(None)
|
||||
|
||||
def pay_onchain_dialog(self, inputs, outputs, invoice=None, external_keypairs=None):
|
||||
# trustedcoin requires this
|
||||
if run_hook('abort_send', self):
|
||||
return
|
||||
|
||||
for txout in outputs:
|
||||
assert isinstance(txout, PartialTxOutput)
|
||||
fee_estimator = self.get_send_fee_estimator()
|
||||
coins = self.get_coins()
|
||||
try:
|
||||
is_sweep = bool(self.tx_external_keypairs)
|
||||
tx = self.wallet.make_unsigned_transaction(
|
||||
coins=coins,
|
||||
outputs=outputs,
|
||||
fee=fee_estimator,
|
||||
is_sweep=is_sweep)
|
||||
except (NotEnoughFunds, NoDynamicFeeEstimates) as e:
|
||||
self.show_message(str(e))
|
||||
if self.config.get('advanced_preview'):
|
||||
self.preview_tx_dialog(inputs, outputs, invoice=invoice)
|
||||
return
|
||||
except InternalAddressCorruption as e:
|
||||
self.show_error(str(e))
|
||||
raise
|
||||
except BaseException as e:
|
||||
self.logger.exception('')
|
||||
self.show_message(str(e))
|
||||
d = ConfirmTxDialog(self, inputs, outputs, external_keypairs)
|
||||
d.update_tx()
|
||||
if d.not_enough_funds:
|
||||
self.show_message(_('Not Enough Funds'))
|
||||
return
|
||||
|
||||
amount = tx.output_value() if self.max_button.isChecked() else sum(map(lambda x: x.value, outputs))
|
||||
fee = tx.get_fee()
|
||||
|
||||
use_rbf = bool(self.config.get('use_rbf', True))
|
||||
if use_rbf:
|
||||
tx.set_rbf(True)
|
||||
|
||||
if fee < self.wallet.relayfee() * tx.estimated_size() / 1000:
|
||||
self.show_error('\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.")
|
||||
]))
|
||||
cancelled, is_send, password, tx = d.run()
|
||||
if cancelled:
|
||||
return
|
||||
|
||||
if preview:
|
||||
self.show_transaction(tx, invoice=invoice)
|
||||
return
|
||||
|
||||
if not self.network:
|
||||
self.show_error(_("You can't broadcast a transaction without a live network connection."))
|
||||
return
|
||||
|
||||
# confirmation dialog
|
||||
msg = [
|
||||
_("Amount to be sent") + ": " + self.format_amount_and_units(amount),
|
||||
_("Mining fee") + ": " + self.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
|
||||
msg.append( _("Additional fees") + ": " + self.format_amount_and_units(x_fee_amount) )
|
||||
|
||||
feerate_warning = simple_config.FEERATE_WARNING_HIGH_FEE
|
||||
if fee > feerate_warning * tx.estimated_size() / 1000:
|
||||
msg.append(_('Warning') + ': ' + _("The fee for this transaction seems unusually high."))
|
||||
|
||||
if self.wallet.has_keystore_encryption():
|
||||
msg.append("")
|
||||
msg.append(_("Enter your password to proceed"))
|
||||
password = self.password_dialog('\n'.join(msg))
|
||||
if not password:
|
||||
return
|
||||
else:
|
||||
msg.append(_('Proceed?'))
|
||||
password = None
|
||||
if not self.question('\n'.join(msg)):
|
||||
return
|
||||
|
||||
if is_send:
|
||||
def sign_done(success):
|
||||
if success:
|
||||
self.do_clear()
|
||||
if not tx.is_complete():
|
||||
self.broadcast_or_show(tx, invoice=invoice)
|
||||
self.sign_tx_with_password(tx, sign_done, password, external_keypairs)
|
||||
else:
|
||||
self.preview_tx_dialog(inputs, outputs, external_keypairs=external_keypairs, invoice=invoice)
|
||||
|
||||
def preview_tx_dialog(self, inputs, outputs, external_keypairs=None, invoice=None):
|
||||
d = PreviewTxDialog(inputs, outputs, external_keypairs, window=self, invoice=invoice)
|
||||
d.show()
|
||||
|
||||
def broadcast_or_show(self, tx, invoice=None):
|
||||
if not self.network:
|
||||
self.show_error(_("You can't broadcast a transaction without a live network connection."))
|
||||
self.show_transaction(tx, invoice=invoice)
|
||||
elif not tx.is_complete():
|
||||
self.show_transaction(tx, invoice=invoice)
|
||||
else:
|
||||
self.broadcast_transaction(tx, invoice=invoice)
|
||||
self.sign_tx_with_password(tx, sign_done, password)
|
||||
|
||||
@protected
|
||||
def sign_tx(self, tx, callback, password):
|
||||
self.sign_tx_with_password(tx, callback, password)
|
||||
def sign_tx(self, tx, callback, external_keypairs, password):
|
||||
self.sign_tx_with_password(tx, callback, password, external_keypairs=external_keypairs)
|
||||
|
||||
def sign_tx_with_password(self, tx: PartialTransaction, callback, password):
|
||||
def sign_tx_with_password(self, tx: PartialTransaction, callback, password, external_keypairs=None):
|
||||
'''Sign the transaction in a separate thread. When done, calls
|
||||
the callback with a success code of True or False.
|
||||
'''
|
||||
|
@ -1852,9 +1498,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
self.on_error(exc_info)
|
||||
callback(False)
|
||||
on_success = run_hook('tc_sign_wrapper', self.wallet, tx, on_success, on_failure) or on_success
|
||||
if self.tx_external_keypairs:
|
||||
if external_keypairs:
|
||||
# can sign directly
|
||||
task = partial(tx.sign, self.tx_external_keypairs)
|
||||
task = partial(tx.sign, external_keypairs)
|
||||
else:
|
||||
task = partial(self.wallet.sign_transaction, tx, password)
|
||||
msg = _('Signing transaction...')
|
||||
|
@ -1908,10 +1554,21 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
WaitingDialog(self, _('Broadcasting transaction...'),
|
||||
broadcast_thread, broadcast_done, self.on_error)
|
||||
|
||||
@protected
|
||||
def open_channel(self, *args, **kwargs):
|
||||
def open_channel(self, connect_str, local_amt, push_amt):
|
||||
# use ConfirmTxDialog
|
||||
# we need to know the fee before we broadcast, because the txid is required
|
||||
# however, the user must be allowed to broadcast early
|
||||
funding_sat = local_amt + push_amt
|
||||
inputs = self.get_coins
|
||||
outputs = [PartialTxOutput.from_address_and_value(self.wallet.dummy_address(), funding_sat)]
|
||||
d = ConfirmTxDialog(self, inputs, outputs, None)
|
||||
cancelled, is_send, password, tx = d.run()
|
||||
if not is_send:
|
||||
return
|
||||
if cancelled:
|
||||
return
|
||||
def task():
|
||||
return self.wallet.lnworker.open_channel(*args, **kwargs)
|
||||
return self.wallet.lnworker.open_channel(connect_str, local_amt, push_amt, password)
|
||||
def on_success(chan):
|
||||
n = chan.constraints.funding_txn_minimum_depth
|
||||
message = '\n'.join([
|
||||
|
@ -2014,13 +1671,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
|
||||
def set_onchain(self, b):
|
||||
self.is_onchain = b
|
||||
self.preview_button.setEnabled(b)
|
||||
self.max_button.setEnabled(b)
|
||||
self.show_send_tab_onchain_fees(b)
|
||||
|
||||
def show_send_tab_onchain_fees(self, b: bool):
|
||||
self.feecontrol_fields.setEnabled(b)
|
||||
#self.fee_e_label.setVisible(b)
|
||||
|
||||
def pay_to_URI(self, URI):
|
||||
if not URI:
|
||||
|
@ -2056,36 +1707,25 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
|
||||
def do_clear(self):
|
||||
self.max_button.setChecked(False)
|
||||
self.not_enough_funds = False
|
||||
self.payment_request = None
|
||||
self.payto_URI = None
|
||||
self.payto_e.is_pr = False
|
||||
self.is_onchain = False
|
||||
self.set_onchain(False)
|
||||
for e in [self.payto_e, self.message_e, self.amount_e, self.fiat_send_e,
|
||||
self.fee_e, self.feerate_e]:
|
||||
for e in [self.payto_e, self.message_e, self.amount_e]:
|
||||
e.setText('')
|
||||
e.setFrozen(False)
|
||||
self.fee_slider.activate()
|
||||
self.feerate_e.setAmount(self.config.fee_per_byte())
|
||||
self.size_e.setAmount(0)
|
||||
self.feerounding_icon.setVisible(False)
|
||||
self.set_pay_from([])
|
||||
self.tx_external_keypairs = {}
|
||||
self.update_status()
|
||||
run_hook('do_clear', self)
|
||||
|
||||
|
||||
def set_frozen_state_of_addresses(self, addrs, freeze: bool):
|
||||
self.wallet.set_frozen_state_of_addresses(addrs, freeze)
|
||||
self.address_list.update()
|
||||
self.utxo_list.update()
|
||||
self.update_fee()
|
||||
|
||||
def set_frozen_state_of_coins(self, utxos: Sequence[PartialTxInput], freeze: bool):
|
||||
self.wallet.set_frozen_state_of_coins(utxos, freeze)
|
||||
self.utxo_list.update()
|
||||
self.update_fee()
|
||||
|
||||
def create_list_tab(self, l, toolbar=None):
|
||||
w = QWidget()
|
||||
|
@ -2109,8 +1749,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
|
||||
def create_utxo_tab(self):
|
||||
from .utxo_list import UTXOList
|
||||
self.utxo_list = l = UTXOList(self)
|
||||
return self.create_list_tab(l)
|
||||
self.utxo_list = UTXOList(self)
|
||||
t = self.utxo_list.get_toolbar()
|
||||
return self.create_list_tab(self.utxo_list, t)
|
||||
|
||||
def create_contacts_tab(self):
|
||||
from .contact_list import ContactList
|
||||
|
@ -2123,18 +1764,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
self.need_update.set() # history, addresses, coins
|
||||
self.clear_receive_tab()
|
||||
|
||||
def get_coins(self):
|
||||
if self.pay_from:
|
||||
return self.pay_from
|
||||
else:
|
||||
return self.wallet.get_spendable_coins(None)
|
||||
|
||||
def spend_coins(self, coins: Sequence[PartialTxInput]):
|
||||
self.set_pay_from(coins)
|
||||
self.set_onchain(len(coins) > 0)
|
||||
self.show_send_tab()
|
||||
self.update_fee()
|
||||
|
||||
def paytomany(self):
|
||||
self.show_send_tab()
|
||||
self.payto_e.paytomany()
|
||||
|
@ -2915,14 +2544,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
def sweep_key_dialog(self):
|
||||
d = WindowModalDialog(self, title=_('Sweep private keys'))
|
||||
d.setMinimumSize(600, 300)
|
||||
|
||||
vbox = QVBoxLayout(d)
|
||||
|
||||
hbox_top = QHBoxLayout()
|
||||
hbox_top.addWidget(QLabel(_("Enter private keys:")))
|
||||
hbox_top.addWidget(InfoButton(WIF_HELP_TEXT), alignment=Qt.AlignRight)
|
||||
vbox.addLayout(hbox_top)
|
||||
|
||||
keys_e = ScanQRTextEdit(allow_multi=True)
|
||||
keys_e.setTabChangesFocus(True)
|
||||
vbox.addWidget(keys_e)
|
||||
|
@ -2978,14 +2604,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
except Exception as e: # FIXME too broad...
|
||||
self.show_message(repr(e))
|
||||
return
|
||||
self.do_clear()
|
||||
self.tx_external_keypairs = keypairs
|
||||
self.spend_coins(coins)
|
||||
self.payto_e.setText(addr)
|
||||
self.spend_max()
|
||||
self.payto_e.setFrozen(True)
|
||||
self.amount_e.setFrozen(True)
|
||||
scriptpubkey = bfh(bitcoin.address_to_script(addr))
|
||||
outputs = [PartialTxOutput(scriptpubkey=scriptpubkey, value='!')]
|
||||
self.warn_if_watching_only()
|
||||
self.pay_onchain_dialog(lambda: coins, outputs, invoice=None, external_keypairs=keypairs)
|
||||
|
||||
def _do_import(self, title, header_layout, func):
|
||||
text = text_dialog(self, title, header_layout, _('Import'), allow_multi=True)
|
||||
|
|
|
@ -120,15 +120,6 @@ class SettingsDialog(WindowModalDialog):
|
|||
fee_type_combo.currentIndexChanged.connect(on_fee_type)
|
||||
fee_widgets.append((fee_type_label, fee_type_combo))
|
||||
|
||||
feebox_cb = QCheckBox(_('Edit fees manually'))
|
||||
feebox_cb.setChecked(bool(self.config.get('show_fee', False)))
|
||||
feebox_cb.setToolTip(_("Show fee edit box in send tab."))
|
||||
def on_feebox(x):
|
||||
self.config.set_key('show_fee', x == Qt.Checked)
|
||||
self.window.fee_adv_controls.setVisible(bool(x))
|
||||
feebox_cb.stateChanged.connect(on_feebox)
|
||||
fee_widgets.append((feebox_cb, None))
|
||||
|
||||
use_rbf = bool(self.config.get('use_rbf', True))
|
||||
use_rbf_cb = QCheckBox(_('Use Replace-By-Fee'))
|
||||
use_rbf_cb.setChecked(use_rbf)
|
||||
|
@ -321,6 +312,14 @@ that is always connected to the internet. Configure a port if you want it to be
|
|||
filelogging_cb.setToolTip(_('Debug logs can be persisted to disk. These are useful for troubleshooting.'))
|
||||
gui_widgets.append((filelogging_cb, None))
|
||||
|
||||
preview_cb = QCheckBox(_('Advanced preview'))
|
||||
preview_cb.setChecked(bool(self.config.get('advanced_preview', False)))
|
||||
preview_cb.setToolTip(_("Open advanced transaction preview dialog when 'Pay' is clicked."))
|
||||
def on_preview(x):
|
||||
self.config.set_key('advanced_preview', x == Qt.Checked)
|
||||
preview_cb.stateChanged.connect(on_preview)
|
||||
tx_widgets.append((preview_cb, None))
|
||||
|
||||
usechange_cb = QCheckBox(_('Use change addresses'))
|
||||
usechange_cb.setChecked(self.window.wallet.use_change)
|
||||
if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
|
||||
|
|
|
@ -29,14 +29,18 @@ import datetime
|
|||
import traceback
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
from functools import partial
|
||||
from decimal import Decimal
|
||||
|
||||
from PyQt5.QtCore import QSize, Qt
|
||||
from PyQt5.QtGui import QTextCharFormat, QBrush, QFont, QPixmap
|
||||
from PyQt5.QtWidgets import (QDialog, QLabel, QPushButton, QHBoxLayout, QVBoxLayout,
|
||||
QTextEdit, QFrame, QAction, QToolButton, QMenu)
|
||||
from PyQt5.QtWidgets import (QDialog, QLabel, QPushButton, QHBoxLayout, QVBoxLayout, QWidget,
|
||||
QTextEdit, QFrame, QAction, QToolButton, QMenu, QCheckBox)
|
||||
import qrcode
|
||||
from qrcode import exceptions
|
||||
|
||||
from electrum.simple_config import SimpleConfig
|
||||
from electrum.util import quantize_feerate
|
||||
from electrum.bitcoin import base_encode
|
||||
from electrum.i18n import _
|
||||
from electrum.plugin import run_hook
|
||||
|
@ -49,9 +53,21 @@ from .util import (MessageBoxMixin, read_QIcon, Buttons, CopyButton, icon_path,
|
|||
MONOSPACE_FONT, ColorScheme, ButtonsLineEdit, text_dialog,
|
||||
char_width_in_lineedit, TRANSACTION_FILE_EXTENSION_FILTER)
|
||||
|
||||
from .fee_slider import FeeSlider
|
||||
from .confirm_tx_dialog import TxEditor
|
||||
from .amountedit import FeerateEdit, BTCAmountEdit
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .main_window import ElectrumWindow
|
||||
|
||||
class TxSizeLabel(QLabel):
|
||||
def setAmount(self, byte_size):
|
||||
self.setText(('x %s bytes =' % byte_size) if byte_size else '')
|
||||
|
||||
class QTextEditWithDefaultSize(QTextEdit):
|
||||
def sizeHint(self):
|
||||
return QSize(0, 100)
|
||||
|
||||
|
||||
SAVE_BUTTON_ENABLED_TOOLTIP = _("Save transaction offline")
|
||||
SAVE_BUTTON_DISABLED_TOOLTIP = _("Please sign this transaction in order to save it")
|
||||
|
@ -72,36 +88,25 @@ def show_transaction(tx: Transaction, *, parent: 'ElectrumWindow', invoice=None,
|
|||
d.show()
|
||||
|
||||
|
||||
class TxDialog(QDialog, MessageBoxMixin):
|
||||
|
||||
def __init__(self, tx: Transaction, *, parent: 'ElectrumWindow', invoice, desc, prompt_if_unsaved):
|
||||
class BaseTxDialog(QDialog, MessageBoxMixin):
|
||||
|
||||
def __init__(self, *, parent: 'ElectrumWindow', invoice, desc, prompt_if_unsaved, finalized):
|
||||
'''Transactions in the wallet will show their description.
|
||||
Pass desc to give a description for txs not yet in the wallet.
|
||||
'''
|
||||
# We want to be a top-level window
|
||||
QDialog.__init__(self, parent=None)
|
||||
# Take a copy; it might get updated in the main window by
|
||||
# e.g. the FX plugin. If this happens during or after a long
|
||||
# sign operation the signatures are lost.
|
||||
self.tx = tx = copy.deepcopy(tx)
|
||||
try:
|
||||
self.tx.deserialize()
|
||||
except BaseException as e:
|
||||
raise SerializationError(e)
|
||||
self.finalized = finalized
|
||||
self.main_window = parent
|
||||
self.config = parent.config
|
||||
self.wallet = parent.wallet
|
||||
self.prompt_if_unsaved = prompt_if_unsaved
|
||||
self.saved = False
|
||||
self.desc = desc
|
||||
self.invoice = invoice
|
||||
|
||||
# if the wallet can populate the inputs with more info, do it now.
|
||||
# as a result, e.g. we might learn an imported address tx is segwit,
|
||||
# or that a beyond-gap-limit address is is_mine
|
||||
tx.add_info_from_wallet(self.wallet)
|
||||
|
||||
self.setMinimumWidth(950)
|
||||
self.setWindowTitle(_("Transaction"))
|
||||
self.set_title()
|
||||
|
||||
vbox = QVBoxLayout()
|
||||
self.setLayout(vbox)
|
||||
|
@ -115,6 +120,7 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||
vbox.addWidget(self.tx_hash_e)
|
||||
|
||||
self.add_tx_stats(vbox)
|
||||
|
||||
vbox.addSpacing(10)
|
||||
|
||||
self.inputs_header = QLabel()
|
||||
|
@ -125,7 +131,6 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||
vbox.addWidget(self.outputs_header)
|
||||
self.outputs_textedit = QTextEditWithDefaultSize()
|
||||
vbox.addWidget(self.outputs_textedit)
|
||||
|
||||
self.sign_button = b = QPushButton(_("Sign"))
|
||||
b.clicked.connect(self.sign)
|
||||
|
||||
|
@ -133,7 +138,7 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||
b.clicked.connect(self.do_broadcast)
|
||||
|
||||
self.save_button = b = QPushButton(_("Save"))
|
||||
save_button_disabled = not tx.is_complete()
|
||||
save_button_disabled = False #not tx.is_complete()
|
||||
b.setDisabled(save_button_disabled)
|
||||
if save_button_disabled:
|
||||
b.setToolTip(SAVE_BUTTON_DISABLED_TOOLTIP)
|
||||
|
@ -148,7 +153,7 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||
self.export_actions_menu = export_actions_menu = QMenu()
|
||||
self.add_export_actions_to_menu(export_actions_menu)
|
||||
export_actions_menu.addSeparator()
|
||||
if isinstance(tx, PartialTransaction):
|
||||
#if isinstance(tx, PartialTransaction):
|
||||
export_for_coinjoin_submenu = export_actions_menu.addMenu(_("For CoinJoin; strip privates"))
|
||||
self.add_export_actions_to_menu(export_for_coinjoin_submenu, gettx=self._gettx_for_coinjoin)
|
||||
|
||||
|
@ -157,6 +162,9 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||
self.export_actions_button.setMenu(export_actions_menu)
|
||||
self.export_actions_button.setPopupMode(QToolButton.InstantPopup)
|
||||
|
||||
self.finalize_button = QPushButton(_('Finalize'))
|
||||
self.finalize_button.clicked.connect(self.on_finalize)
|
||||
|
||||
partial_tx_actions_menu = QMenu()
|
||||
ptx_merge_sigs_action = QAction(_("Merge signatures from"), self)
|
||||
ptx_merge_sigs_action.triggered.connect(self.merge_sigs)
|
||||
|
@ -171,20 +179,41 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||
|
||||
# Action buttons
|
||||
self.buttons = []
|
||||
if isinstance(tx, PartialTransaction):
|
||||
#if isinstance(tx, PartialTransaction):
|
||||
self.buttons.append(self.partial_tx_actions_button)
|
||||
self.buttons += [self.sign_button, self.broadcast_button, self.cancel_button]
|
||||
# Transaction sharing buttons
|
||||
self.sharing_buttons = [self.export_actions_button, self.save_button]
|
||||
|
||||
self.sharing_buttons = [self.finalize_button, self.export_actions_button, self.save_button]
|
||||
run_hook('transaction_dialog', self)
|
||||
|
||||
hbox = QHBoxLayout()
|
||||
if not self.finalized:
|
||||
self.create_fee_controls()
|
||||
vbox.addWidget(self.feecontrol_fields)
|
||||
self.hbox = hbox = QHBoxLayout()
|
||||
hbox.addLayout(Buttons(*self.sharing_buttons))
|
||||
hbox.addStretch(1)
|
||||
hbox.addLayout(Buttons(*self.buttons))
|
||||
vbox.addLayout(hbox)
|
||||
self.update()
|
||||
self.set_buttons_visibility()
|
||||
|
||||
def set_buttons_visibility(self):
|
||||
for b in [self.export_actions_button, self.save_button, self.sign_button, self.broadcast_button, self.partial_tx_actions_button]:
|
||||
b.setVisible(self.finalized)
|
||||
for b in [self.finalize_button]:
|
||||
b.setVisible(not self.finalized)
|
||||
|
||||
def set_tx(self, tx):
|
||||
# Take a copy; it might get updated in the main window by
|
||||
# e.g. the FX plugin. If this happens during or after a long
|
||||
# sign operation the signatures are lost.
|
||||
self.tx = tx = copy.deepcopy(tx)
|
||||
try:
|
||||
self.tx.deserialize()
|
||||
except BaseException as e:
|
||||
raise SerializationError(e)
|
||||
# if the wallet can populate the inputs with more info, do it now.
|
||||
# as a result, e.g. we might learn an imported address tx is segwit,
|
||||
# or that a beyond-gap-limit address is is_mine
|
||||
tx.add_info_from_wallet(self.wallet)
|
||||
|
||||
def do_broadcast(self):
|
||||
self.main_window.push_top_level_window(self)
|
||||
|
@ -269,7 +298,7 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||
|
||||
self.sign_button.setDisabled(True)
|
||||
self.main_window.push_top_level_window(self)
|
||||
self.main_window.sign_tx(self.tx, sign_done)
|
||||
self.main_window.sign_tx(self.tx, sign_done, self.external_keypairs)
|
||||
|
||||
def save(self):
|
||||
self.main_window.push_top_level_window(self)
|
||||
|
@ -341,6 +370,10 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||
self.update()
|
||||
|
||||
def update(self):
|
||||
if not self.finalized:
|
||||
self.update_fee_fields()
|
||||
if self.tx is None:
|
||||
return
|
||||
self.update_io()
|
||||
desc = self.desc
|
||||
base_unit = self.main_window.base_unit()
|
||||
|
@ -373,7 +406,8 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||
else:
|
||||
self.date_label.hide()
|
||||
self.locktime_label.setText(f"LockTime: {self.tx.locktime}")
|
||||
self.rbf_label.setText(f"RBF: {not self.tx.is_final()}")
|
||||
self.rbf_label.setText(f"Replace by Fee: {not self.tx.is_final()}")
|
||||
|
||||
if tx_mined_status.header_hash:
|
||||
self.block_hash_label.setText(_("Included in block: {}")
|
||||
.format(tx_mined_status.header_hash))
|
||||
|
@ -443,7 +477,7 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||
addr = self.wallet.get_txin_address(txin)
|
||||
if addr is None:
|
||||
addr = ''
|
||||
cursor.insertText(addr, text_format(addr))
|
||||
#cursor.insertText(addr, text_format(addr))
|
||||
if isinstance(txin, PartialTxInput) and txin.value_sats() is not None:
|
||||
cursor.insertText(format_amount(txin.value_sats()), ext)
|
||||
cursor.insertBlock()
|
||||
|
@ -509,6 +543,11 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||
vbox_right.addWidget(self.size_label)
|
||||
self.rbf_label = TxDetailLabel()
|
||||
vbox_right.addWidget(self.rbf_label)
|
||||
self.rbf_cb = QCheckBox(_('Replace by fee'))
|
||||
vbox_right.addWidget(self.rbf_cb)
|
||||
self.rbf_label.setVisible(self.finalized)
|
||||
self.rbf_cb.setVisible(not self.finalized)
|
||||
|
||||
self.locktime_label = TxDetailLabel()
|
||||
vbox_right.addWidget(self.locktime_label)
|
||||
self.block_hash_label = TxDetailLabel(word_wrap=True)
|
||||
|
@ -520,6 +559,19 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||
|
||||
vbox.addLayout(hbox_stats)
|
||||
|
||||
def set_title(self):
|
||||
self.setWindowTitle(_("Create transaction") if not self.finalized else _("Transaction"))
|
||||
|
||||
def on_finalize(self):
|
||||
self.finalized = True
|
||||
for widget in [self.fee_slider, self.feecontrol_fields, self.rbf_cb]:
|
||||
widget.setEnabled(False)
|
||||
widget.setVisible(False)
|
||||
for widget in [self.rbf_label]:
|
||||
widget.setVisible(True)
|
||||
self.set_title()
|
||||
self.set_buttons_visibility()
|
||||
|
||||
|
||||
class QTextEditWithDefaultSize(QTextEdit):
|
||||
def sizeHint(self):
|
||||
|
@ -532,3 +584,190 @@ class TxDetailLabel(QLabel):
|
|||
self.setTextInteractionFlags(Qt.TextSelectableByMouse)
|
||||
if word_wrap is not None:
|
||||
self.setWordWrap(word_wrap)
|
||||
|
||||
|
||||
class TxDialog(BaseTxDialog):
|
||||
def __init__(self, tx: Transaction, *, parent: 'ElectrumWindow', invoice, desc, prompt_if_unsaved):
|
||||
BaseTxDialog.__init__(self, parent=parent, invoice=invoice, desc=desc, prompt_if_unsaved=prompt_if_unsaved, finalized=True)
|
||||
self.set_tx(tx)
|
||||
self.update()
|
||||
|
||||
|
||||
|
||||
class PreviewTxDialog(BaseTxDialog, TxEditor):
|
||||
|
||||
def __init__(self, inputs, outputs, external_keypairs, *, window: 'ElectrumWindow', invoice):
|
||||
TxEditor.__init__(self, window, inputs, outputs, external_keypairs)
|
||||
BaseTxDialog.__init__(self, parent=window, invoice=invoice, desc='', prompt_if_unsaved=False, finalized=False)
|
||||
self.update_tx()
|
||||
self.update()
|
||||
|
||||
def create_fee_controls(self):
|
||||
|
||||
self.size_e = TxSizeLabel()
|
||||
self.size_e.setAlignment(Qt.AlignCenter)
|
||||
self.size_e.setAmount(0)
|
||||
self.size_e.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet())
|
||||
|
||||
self.feerate_e = FeerateEdit(lambda: 0)
|
||||
self.feerate_e.setAmount(self.config.fee_per_byte())
|
||||
self.feerate_e.textEdited.connect(partial(self.on_fee_or_feerate, self.feerate_e, False))
|
||||
self.feerate_e.editingFinished.connect(partial(self.on_fee_or_feerate, self.feerate_e, True))
|
||||
|
||||
self.fee_e = BTCAmountEdit(self.main_window.get_decimal_point)
|
||||
self.fee_e.textEdited.connect(partial(self.on_fee_or_feerate, self.fee_e, False))
|
||||
self.fee_e.editingFinished.connect(partial(self.on_fee_or_feerate, self.fee_e, True))
|
||||
|
||||
self.fee_e.textChanged.connect(self.entry_changed)
|
||||
self.feerate_e.textChanged.connect(self.entry_changed)
|
||||
|
||||
self.fee_slider = FeeSlider(self, self.config, self.fee_slider_callback)
|
||||
self.fee_slider.setFixedWidth(self.fee_e.width())
|
||||
|
||||
def feerounding_onclick():
|
||||
text = (self.feerounding_text + '\n\n' +
|
||||
_('To somewhat protect your privacy, Electrum tries to create change with similar precision to other outputs.') + ' ' +
|
||||
_('At most 100 satoshis might be lost due to this rounding.') + ' ' +
|
||||
_("You can disable this setting in '{}'.").format(_('Preferences')) + '\n' +
|
||||
_('Also, dust is not kept as change, but added to the fee.') + '\n' +
|
||||
_('Also, when batching RBF transactions, BIP 125 imposes a lower bound on the fee.'))
|
||||
self.show_message(title=_('Fee rounding'), msg=text)
|
||||
|
||||
self.feerounding_icon = QPushButton(read_QIcon('info.png'), '')
|
||||
self.feerounding_icon.setFixedWidth(round(2.2 * char_width_in_lineedit()))
|
||||
self.feerounding_icon.setFlat(True)
|
||||
self.feerounding_icon.clicked.connect(feerounding_onclick)
|
||||
self.feerounding_icon.setVisible(False)
|
||||
|
||||
self.fee_adv_controls = QWidget()
|
||||
hbox = QHBoxLayout(self.fee_adv_controls)
|
||||
hbox.setContentsMargins(0, 0, 0, 0)
|
||||
hbox.addWidget(self.feerate_e)
|
||||
hbox.addWidget(self.size_e)
|
||||
hbox.addWidget(self.fee_e)
|
||||
hbox.addWidget(self.feerounding_icon, Qt.AlignLeft)
|
||||
hbox.addStretch(1)
|
||||
|
||||
self.feecontrol_fields = QWidget()
|
||||
vbox_feecontrol = QVBoxLayout(self.feecontrol_fields)
|
||||
vbox_feecontrol.setContentsMargins(0, 0, 0, 0)
|
||||
vbox_feecontrol.addWidget(self.fee_adv_controls)
|
||||
vbox_feecontrol.addWidget(self.fee_slider)
|
||||
|
||||
def fee_slider_callback(self, dyn, pos, fee_rate):
|
||||
super().fee_slider_callback(dyn, pos, fee_rate)
|
||||
self.fee_slider.activate()
|
||||
if fee_rate:
|
||||
fee_rate = Decimal(fee_rate)
|
||||
self.feerate_e.setAmount(quantize_feerate(fee_rate / 1000))
|
||||
else:
|
||||
self.feerate_e.setAmount(None)
|
||||
self.fee_e.setModified(False)
|
||||
|
||||
def on_fee_or_feerate(self, edit_changed, editing_finished):
|
||||
edit_other = self.feerate_e if edit_changed == self.fee_e else self.fee_e
|
||||
if editing_finished:
|
||||
if edit_changed.get_amount() is None:
|
||||
# This is so that when the user blanks the fee and moves on,
|
||||
# we go back to auto-calculate mode and put a fee back.
|
||||
edit_changed.setModified(False)
|
||||
else:
|
||||
# edit_changed was edited just now, so make sure we will
|
||||
# freeze the correct fee setting (this)
|
||||
edit_other.setModified(False)
|
||||
self.fee_slider.deactivate()
|
||||
self.update()
|
||||
|
||||
def is_send_fee_frozen(self):
|
||||
return self.fee_e.isVisible() and self.fee_e.isModified() \
|
||||
and (self.fee_e.text() or self.fee_e.hasFocus())
|
||||
|
||||
def is_send_feerate_frozen(self):
|
||||
return self.feerate_e.isVisible() and self.feerate_e.isModified() \
|
||||
and (self.feerate_e.text() or self.feerate_e.hasFocus())
|
||||
|
||||
def set_feerounding_text(self, num_satoshis_added):
|
||||
self.feerounding_text = (_('Additional {} satoshis are going to be added.')
|
||||
.format(num_satoshis_added))
|
||||
|
||||
def get_fee_estimator(self):
|
||||
if self.is_send_fee_frozen():
|
||||
fee_estimator = self.fee_e.get_amount()
|
||||
elif self.is_send_feerate_frozen():
|
||||
amount = self.feerate_e.get_amount() # sat/byte feerate
|
||||
amount = 0 if amount is None else amount * 1000 # sat/kilobyte feerate
|
||||
fee_estimator = partial(
|
||||
SimpleConfig.estimate_fee_for_feerate, amount)
|
||||
else:
|
||||
fee_estimator = None
|
||||
return fee_estimator
|
||||
|
||||
def entry_changed(self):
|
||||
# blue color denotes auto-filled values
|
||||
text = ""
|
||||
fee_color = ColorScheme.DEFAULT
|
||||
feerate_color = ColorScheme.DEFAULT
|
||||
if self.not_enough_funds:
|
||||
fee_color = ColorScheme.RED
|
||||
feerate_color = ColorScheme.RED
|
||||
elif self.fee_e.isModified():
|
||||
feerate_color = ColorScheme.BLUE
|
||||
elif self.feerate_e.isModified():
|
||||
fee_color = ColorScheme.BLUE
|
||||
else:
|
||||
fee_color = ColorScheme.BLUE
|
||||
feerate_color = ColorScheme.BLUE
|
||||
self.fee_e.setStyleSheet(fee_color.as_stylesheet())
|
||||
self.feerate_e.setStyleSheet(feerate_color.as_stylesheet())
|
||||
#
|
||||
self.needs_update = True
|
||||
|
||||
def update_fee_fields(self):
|
||||
freeze_fee = self.is_send_fee_frozen()
|
||||
freeze_feerate = self.is_send_feerate_frozen()
|
||||
if self.no_dynfee_estimates:
|
||||
size = self.tx.estimated_size()
|
||||
self.size_e.setAmount(size)
|
||||
if self.not_enough_funds or self.no_dynfee_estimates:
|
||||
if not freeze_fee:
|
||||
self.fee_e.setAmount(None)
|
||||
if not freeze_feerate:
|
||||
self.feerate_e.setAmount(None)
|
||||
self.feerounding_icon.setVisible(False)
|
||||
return
|
||||
|
||||
tx = self.tx
|
||||
size = tx.estimated_size()
|
||||
fee = tx.get_fee()
|
||||
|
||||
self.size_e.setAmount(size)
|
||||
|
||||
# Displayed fee/fee_rate values are set according to user input.
|
||||
# Due to rounding or dropping dust in CoinChooser,
|
||||
# actual fees often differ somewhat.
|
||||
if freeze_feerate or self.fee_slider.is_active():
|
||||
displayed_feerate = self.feerate_e.get_amount()
|
||||
if displayed_feerate is not None:
|
||||
displayed_feerate = quantize_feerate(displayed_feerate)
|
||||
else:
|
||||
# fallback to actual fee
|
||||
displayed_feerate = quantize_feerate(fee / size) if fee is not None else None
|
||||
self.feerate_e.setAmount(displayed_feerate)
|
||||
displayed_fee = round(displayed_feerate * size) if displayed_feerate is not None else None
|
||||
self.fee_e.setAmount(displayed_fee)
|
||||
else:
|
||||
if freeze_fee:
|
||||
displayed_fee = self.fee_e.get_amount()
|
||||
else:
|
||||
# fallback to actual fee if nothing is frozen
|
||||
displayed_fee = fee
|
||||
self.fee_e.setAmount(displayed_fee)
|
||||
displayed_fee = displayed_fee if displayed_fee else 0
|
||||
displayed_feerate = quantize_feerate(displayed_fee / size) if displayed_fee is not None else None
|
||||
self.feerate_e.setAmount(displayed_feerate)
|
||||
|
||||
# show/hide fee rounding icon
|
||||
feerounding = (fee - displayed_fee) if fee else 0
|
||||
self.set_feerounding_text(int(feerounding))
|
||||
self.feerounding_icon.setToolTip(self.feerounding_text)
|
||||
self.feerounding_icon.setVisible(abs(feerounding) >= 1)
|
||||
|
|
|
@ -28,12 +28,12 @@ from enum import IntEnum
|
|||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QStandardItemModel, QStandardItem, QFont
|
||||
from PyQt5.QtWidgets import QAbstractItemView, QMenu
|
||||
from PyQt5.QtWidgets import QAbstractItemView, QMenu, QLabel, QHBoxLayout
|
||||
|
||||
from electrum.i18n import _
|
||||
from electrum.transaction import PartialTxInput
|
||||
|
||||
from .util import MyTreeView, ColorScheme, MONOSPACE_FONT
|
||||
from .util import MyTreeView, ColorScheme, MONOSPACE_FONT, EnterButton
|
||||
|
||||
|
||||
class UTXOList(MyTreeView):
|
||||
|
@ -58,6 +58,9 @@ class UTXOList(MyTreeView):
|
|||
super().__init__(parent, self.create_menu,
|
||||
stretch_column=self.Columns.LABEL,
|
||||
editable_columns=[])
|
||||
self.cc_label = QLabel('')
|
||||
self.clear_cc_button = EnterButton(_('Reset'), lambda: self.set_spend_list([]))
|
||||
self.spend_list = []
|
||||
self.setModel(QStandardItemModel(self))
|
||||
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
||||
self.setSortingEnabled(True)
|
||||
|
@ -72,6 +75,11 @@ class UTXOList(MyTreeView):
|
|||
for idx, utxo in enumerate(utxos):
|
||||
self.insert_utxo(idx, utxo)
|
||||
self.filter()
|
||||
self.clear_cc_button.setEnabled(bool(self.spend_list))
|
||||
coins = [self.utxo_dict[x] for x in self.spend_list] or utxos
|
||||
amount = sum(x.value_sats() for x in coins)
|
||||
amount_str = self.parent.format_amount_and_units(amount)
|
||||
self.cc_label.setText('%d outputs, %s'%(len(coins), amount_str))
|
||||
|
||||
def insert_utxo(self, idx, utxo: PartialTxInput):
|
||||
address = utxo.address
|
||||
|
@ -88,10 +96,13 @@ class UTXOList(MyTreeView):
|
|||
utxo_item[self.Columns.AMOUNT].setFont(QFont(MONOSPACE_FONT))
|
||||
utxo_item[self.Columns.OUTPOINT].setFont(QFont(MONOSPACE_FONT))
|
||||
utxo_item[self.Columns.ADDRESS].setData(name, Qt.UserRole)
|
||||
if self.wallet.is_frozen_address(address):
|
||||
if name in self.spend_list:
|
||||
for i in range(5):
|
||||
utxo_item[i].setBackground(ColorScheme.GREEN.as_color(True))
|
||||
elif self.wallet.is_frozen_address(address):
|
||||
utxo_item[self.Columns.ADDRESS].setBackground(ColorScheme.BLUE.as_color(True))
|
||||
utxo_item[self.Columns.ADDRESS].setToolTip(_('Address is frozen'))
|
||||
if self.wallet.is_frozen_coin(utxo):
|
||||
elif self.wallet.is_frozen_coin(utxo):
|
||||
utxo_item[self.Columns.OUTPOINT].setBackground(ColorScheme.BLUE.as_color(True))
|
||||
utxo_item[self.Columns.OUTPOINT].setToolTip(f"{name}\n{_('Coin is frozen')}")
|
||||
else:
|
||||
|
@ -106,6 +117,20 @@ class UTXOList(MyTreeView):
|
|||
return None
|
||||
return [x.data(Qt.UserRole) for x in items]
|
||||
|
||||
def set_spend_list(self, coins):
|
||||
self.spend_list = [utxo.prevout.to_str() for utxo in coins]
|
||||
self.update()
|
||||
|
||||
def get_spend_list(self):
|
||||
return [self.utxo_dict[x] for x in self.spend_list]
|
||||
|
||||
def get_toolbar(self):
|
||||
h = QHBoxLayout()
|
||||
h.addWidget(self.cc_label)
|
||||
h.addStretch()
|
||||
h.addWidget(self.clear_cc_button)
|
||||
return h
|
||||
|
||||
def create_menu(self, position):
|
||||
selected = self.get_selected_outpoints()
|
||||
if not selected:
|
||||
|
@ -113,7 +138,7 @@ class UTXOList(MyTreeView):
|
|||
menu = QMenu()
|
||||
menu.setSeparatorsCollapsible(True) # consecutive separators are merged together
|
||||
coins = [self.utxo_dict[name] for name in selected]
|
||||
menu.addAction(_("Spend"), lambda: self.parent.spend_coins(coins))
|
||||
menu.addAction(_("Spend"), lambda: self.set_spend_list(coins))
|
||||
assert len(coins) >= 1, len(coins)
|
||||
if len(coins) == 1:
|
||||
utxo = coins[0]
|
||||
|
|
Loading…
Add table
Reference in a new issue