Browse Source

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
ln-negative-red
ThomasV 5 years ago
parent
commit
dd6cb2caf7
  1. 1
      RELEASE-NOTES
  2. 261
      electrum/gui/qt/confirm_tx_dialog.py
  3. 6
      electrum/gui/qt/invoice_list.py
  4. 530
      electrum/gui/qt/main_window.py
  5. 17
      electrum/gui/qt/settings_dialog.py
  6. 307
      electrum/gui/qt/transaction_dialog.py
  7. 35
      electrum/gui/qt/utxo_list.py

1
RELEASE-NOTES

@ -1,6 +1,7 @@
# Release 4.0 - (Not released yet; release notes are incomplete) # Release 4.0 - (Not released yet; release notes are incomplete)
* Lightning Network * Lightning Network
* Qt GUI: Separation between output selection and transaction finalization.
* Http PayServer can be configured from GUI * Http PayServer can be configured from GUI
# Release 3.3.8 - (July 11, 2019) # Release 3.3.8 - (July 11, 2019)

261
electrum/gui/qt/confirm_tx_dialog.py

@ -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()

6
electrum/gui/qt/invoice_list.py

@ -27,6 +27,7 @@ from enum import IntEnum
from PyQt5.QtCore import Qt, QItemSelectionModel from PyQt5.QtCore import Qt, QItemSelectionModel
from PyQt5.QtGui import QStandardItemModel, QStandardItem, QFont from PyQt5.QtGui import QStandardItemModel, QStandardItem, QFont
from PyQt5.QtWidgets import QAbstractItemView
from PyQt5.QtWidgets import QHeaderView, QMenu, QVBoxLayout, QGridLayout, QLabel, QTreeWidget, QTreeWidgetItem from PyQt5.QtWidgets import QHeaderView, QMenu, QVBoxLayout, QGridLayout, QLabel, QTreeWidget, QTreeWidgetItem
from electrum.i18n import _ from electrum.i18n import _
@ -70,6 +71,7 @@ class InvoiceList(MyTreeView):
editable_columns=[]) editable_columns=[])
self.setSortingEnabled(True) self.setSortingEnabled(True)
self.setModel(QStandardItemModel(self)) self.setModel(QStandardItemModel(self))
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.update() self.update()
def update_item(self, key, status): def update_item(self, key, status):
@ -143,6 +145,10 @@ class InvoiceList(MyTreeView):
export_meta_gui(self.parent, _('invoices'), self.parent.invoices.export_file) export_meta_gui(self.parent, _('invoices'), self.parent.invoices.export_file)
def create_menu(self, position): def create_menu(self, position):
items = self.selected_in_column(0)
if len(items) > 1:
print(items)
return
idx = self.indexAt(position) idx = self.indexAt(position)
item = self.model().itemFromIndex(idx) item = self.model().itemFromIndex(idx)
item_col0 = self.model().itemFromIndex(idx.sibling(idx.row(), self.Columns.DATE)) item_col0 = self.model().itemFromIndex(idx.sibling(idx.row(), self.Columns.DATE))

530
electrum/gui/qt/main_window.py

@ -95,6 +95,8 @@ from .installwizard import WIF_HELP_TEXT
from .history_list import HistoryList, HistoryModel from .history_list import HistoryList, HistoryModel
from .update_checker import UpdateCheck, UpdateCheckThread from .update_checker import UpdateCheck, UpdateCheckThread
from .channels_list import ChannelsList from .channels_list import ChannelsList
from .confirm_tx_dialog import ConfirmTxDialog
from .transaction_dialog import PreviewTxDialog
if TYPE_CHECKING: if TYPE_CHECKING:
from . import ElectrumGui from . import ElectrumGui
@ -153,11 +155,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.payto_URI = None self.payto_URI = None
self.checking_accounts = False self.checking_accounts = False
self.qr_window = None self.qr_window = None
self.not_enough_funds = False
self.pluginsdialog = None self.pluginsdialog = None
self.require_fee_update = False self.require_fee_update = False
self.tl_windows = [] self.tl_windows = []
self.tx_external_keypairs = {}
Logger.__init__(self) Logger.__init__(self)
self.tx_notification_queue = queue.Queue() self.tx_notification_queue = queue.Queue()
@ -174,8 +174,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.completions = QStringListModel() self.completions = QStringListModel()
self.send_tab_is_onchain = False
self.tabs = tabs = QTabWidget(self) self.tabs = tabs = QTabWidget(self)
self.send_tab = self.create_send_tab() self.send_tab = self.create_send_tab()
self.receive_tab = self.create_receive_tab() self.receive_tab = self.create_receive_tab()
@ -244,7 +242,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.console.showMessage(self.network.banner) self.console.showMessage(self.network.banner)
# update fee slider in case we missed the callback # update fee slider in case we missed the callback
self.fee_slider.update() #self.fee_slider.update()
self.load_wallet(wallet) self.load_wallet(wallet)
gui_object.timer.timeout.connect(self.timer_actions) gui_object.timer.timeout.connect(self.timer_actions)
self.fetch_alias() self.fetch_alias()
@ -397,11 +395,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.history_model.update_tx_mined_status(tx_hash, tx_mined_status) self.history_model.update_tx_mined_status(tx_hash, tx_mined_status)
elif event == 'fee': elif event == 'fee':
if self.config.is_dynfee(): if self.config.is_dynfee():
self.fee_slider.update() #self.fee_slider.update()
self.require_fee_update = True self.require_fee_update = True
elif event == 'fee_histogram': elif event == 'fee_histogram':
if self.config.is_dynfee(): if self.config.is_dynfee():
self.fee_slider.update() #self.fee_slider.update()
self.require_fee_update = True self.require_fee_update = True
self.history_model.on_fee_histogram() self.history_model.on_fee_histogram()
else: else:
@ -769,7 +767,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.payto_e.resolve() self.payto_e.resolve()
# update fee # update fee
if self.require_fee_update: if self.require_fee_update:
self.do_update_fee() #self.do_update_fee()
self.require_fee_update = False self.require_fee_update = False
self.notify_transactions() self.notify_transactions()
@ -946,7 +944,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
if not self.fx or not self.fx.is_enabled(): if not self.fx or not self.fx.is_enabled():
self.fiat_receive_e.setVisible(False) self.fiat_receive_e.setVisible(False)
grid.addWidget(self.fiat_receive_e, 1, 2, Qt.AlignLeft) 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.receive_amount_e, self.fiat_receive_e, None)
self.connect_fields(self, self.amount_e, self.fiat_send_e, None)
self.expires_combo = QComboBox() self.expires_combo = QComboBox()
evl = sorted(pr_expiration_values.items()) evl = sorted(pr_expiration_values.items())
@ -1179,10 +1179,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.receive_address_e.setStyleSheet("") self.receive_address_e.setStyleSheet("")
self.receive_address_e.setToolTip("") 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): def create_send_tab(self):
# A 4-column grid layout. All the stretch is in the last column. # A 4-column grid layout. All the stretch is in the last column.
# The exchange rate plugin adds a fiat widget in column 2 # 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) self.max_button.setCheckable(True)
grid.addWidget(self.max_button, 3, 3) 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.save_button = EnterButton(_("Save"), self.do_save_invoice)
self.preview_button = EnterButton(_("Preview"), self.do_preview) self.send_button = EnterButton(_("Pay"), self.do_pay)
self.preview_button.setToolTip(_('Display the details of your transaction before signing it.'))
self.send_button = EnterButton(_("Send"), self.do_pay)
self.clear_button = EnterButton(_("Clear"), self.do_clear) self.clear_button = EnterButton(_("Clear"), self.do_clear)
buttons = QHBoxLayout() buttons = QHBoxLayout()
buttons.addStretch(1) buttons.addStretch(1)
buttons.addWidget(self.clear_button) buttons.addWidget(self.clear_button)
buttons.addWidget(self.save_button) buttons.addWidget(self.save_button)
buttons.addWidget(self.preview_button)
buttons.addWidget(self.send_button) buttons.addWidget(self.send_button)
grid.addLayout(buttons, 6, 1, 1, 4) grid.addLayout(buttons, 6, 1, 1, 4)
self.amount_e.shortcut.connect(self.spend_max) 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): def reset_max(text):
self.max_button.setChecked(False) self.max_button.setChecked(False)
@ -1365,45 +1248,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.amount_e.textEdited.connect(reset_max) self.amount_e.textEdited.connect(reset_max)
self.fiat_send_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.set_onchain(False)
self.invoices_label = QLabel(_('Outgoing payments')) self.invoices_label = QLabel(_('Outgoing payments'))
@ -1430,144 +1274,12 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
if run_hook('abort_send', self): if run_hook('abort_send', self):
return return
self.max_button.setChecked(True) self.max_button.setChecked(True)
self.do_update_fee() amount = sum(x.value_sats() for x in self.get_coins())
self.amount_e.setAmount(amount)
def update_fee(self): ## substract extra fee
self.require_fee_update = True #__, x_fee_amount = run_hook('get_tx_extra_fee', self.wallet, tx) or (None, 0)
#amount_after_all_fees = amount - x_fee_amount
def get_payto_or_dummy(self) -> bytes: #self.amount_e.setAmount(amount_after_all_fees)
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)
def get_contact_payto(self, key): def get_contact_payto(self, key):
_type, label = self.contacts.get(key) _type, label = self.contacts.get(key)
@ -1605,26 +1317,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
def protect(self, func, args, password): def protect(self, func, args, password):
return 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]: def read_outputs(self) -> List[PartialTxOutput]:
if self.payment_request: if self.payment_request:
outputs = self.payment_request.get_outputs() outputs = self.payment_request.get_outputs()
@ -1734,115 +1426,69 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.do_clear() self.do_clear()
self.invoice_list.update() self.invoice_list.update()
def do_preview(self): def do_pay(self):
self.do_pay(preview=True)
def do_pay(self, preview=False):
invoice = self.read_invoice() invoice = self.read_invoice()
if not invoice: if not invoice:
return return
self.wallet.save_invoice(invoice) self.wallet.save_invoice(invoice)
self.invoice_list.update() 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: if invoice['type'] == PR_TYPE_LN:
self.pay_lightning_invoice(invoice['invoice']) self.pay_lightning_invoice(invoice['invoice'])
return
elif invoice['type'] == PR_TYPE_ONCHAIN: elif invoice['type'] == PR_TYPE_ONCHAIN:
message = invoice['message'] outputs = invoice['outputs']
outputs = invoice['outputs'] # type: List[PartialTxOutput] self.pay_onchain_dialog(self.get_coins, outputs, invoice=invoice)
else: else:
raise Exception('unknown invoice type') 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): if run_hook('abort_send', self):
return return
if self.config.get('advanced_preview'):
for txout in outputs: self.preview_tx_dialog(inputs, outputs, invoice=invoice)
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))
return return
except InternalAddressCorruption as e: d = ConfirmTxDialog(self, inputs, outputs, external_keypairs)
self.show_error(str(e)) d.update_tx()
raise if d.not_enough_funds:
except BaseException as e: self.show_message(_('Not Enough Funds'))
self.logger.exception('')
self.show_message(str(e))
return return
cancelled, is_send, password, tx = d.run()
amount = tx.output_value() if self.max_button.isChecked() else sum(map(lambda x: x.value, outputs)) if cancelled:
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.")
]))
return return
if is_send:
def sign_done(success):
if success:
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)
if preview: def preview_tx_dialog(self, inputs, outputs, external_keypairs=None, invoice=None):
self.show_transaction(tx, invoice=invoice) d = PreviewTxDialog(inputs, outputs, external_keypairs, window=self, invoice=invoice)
return d.show()
def broadcast_or_show(self, tx, invoice=None):
if not self.network: if not self.network:
self.show_error(_("You can't broadcast a transaction without a live network connection.")) self.show_error(_("You can't broadcast a transaction without a live network connection."))
return self.show_transaction(tx, invoice=invoice)
elif not tx.is_complete():
# confirmation dialog self.show_transaction(tx, invoice=invoice)
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: else:
msg.append(_('Proceed?')) self.broadcast_transaction(tx, invoice=invoice)
password = None
if not self.question('\n'.join(msg)):
return
def sign_done(success):
if success:
self.do_clear()
if 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 @protected
def sign_tx(self, tx, callback, password): def sign_tx(self, tx, callback, external_keypairs, password):
self.sign_tx_with_password(tx, callback, 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 '''Sign the transaction in a separate thread. When done, calls
the callback with a success code of True or False. the callback with a success code of True or False.
''' '''
@ -1852,9 +1498,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.on_error(exc_info) self.on_error(exc_info)
callback(False) callback(False)
on_success = run_hook('tc_sign_wrapper', self.wallet, tx, on_success, on_failure) or on_success 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 # can sign directly
task = partial(tx.sign, self.tx_external_keypairs) task = partial(tx.sign, external_keypairs)
else: else:
task = partial(self.wallet.sign_transaction, tx, password) task = partial(self.wallet.sign_transaction, tx, password)
msg = _('Signing transaction...') msg = _('Signing transaction...')
@ -1908,10 +1554,21 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
WaitingDialog(self, _('Broadcasting transaction...'), WaitingDialog(self, _('Broadcasting transaction...'),
broadcast_thread, broadcast_done, self.on_error) broadcast_thread, broadcast_done, self.on_error)
@protected def open_channel(self, connect_str, local_amt, push_amt):
def open_channel(self, *args, **kwargs): # 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(): 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): def on_success(chan):
n = chan.constraints.funding_txn_minimum_depth n = chan.constraints.funding_txn_minimum_depth
message = '\n'.join([ message = '\n'.join([
@ -2014,13 +1671,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
def set_onchain(self, b): def set_onchain(self, b):
self.is_onchain = b self.is_onchain = b
self.preview_button.setEnabled(b)
self.max_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): def pay_to_URI(self, URI):
if not URI: if not URI:
@ -2056,36 +1707,25 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
def do_clear(self): def do_clear(self):
self.max_button.setChecked(False) self.max_button.setChecked(False)
self.not_enough_funds = False
self.payment_request = None self.payment_request = None
self.payto_URI = None self.payto_URI = None
self.payto_e.is_pr = False self.payto_e.is_pr = False
self.is_onchain = False self.is_onchain = False
self.set_onchain(False) self.set_onchain(False)
for e in [self.payto_e, self.message_e, self.amount_e, self.fiat_send_e, for e in [self.payto_e, self.message_e, self.amount_e]:
self.fee_e, self.feerate_e]:
e.setText('') e.setText('')
e.setFrozen(False) 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() self.update_status()
run_hook('do_clear', self) run_hook('do_clear', self)
def set_frozen_state_of_addresses(self, addrs, freeze: bool): def set_frozen_state_of_addresses(self, addrs, freeze: bool):
self.wallet.set_frozen_state_of_addresses(addrs, freeze) self.wallet.set_frozen_state_of_addresses(addrs, freeze)
self.address_list.update() self.address_list.update()
self.utxo_list.update() self.utxo_list.update()
self.update_fee()
def set_frozen_state_of_coins(self, utxos: Sequence[PartialTxInput], freeze: bool): def set_frozen_state_of_coins(self, utxos: Sequence[PartialTxInput], freeze: bool):
self.wallet.set_frozen_state_of_coins(utxos, freeze) self.wallet.set_frozen_state_of_coins(utxos, freeze)
self.utxo_list.update() self.utxo_list.update()
self.update_fee()
def create_list_tab(self, l, toolbar=None): def create_list_tab(self, l, toolbar=None):
w = QWidget() w = QWidget()
@ -2109,8 +1749,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
def create_utxo_tab(self): def create_utxo_tab(self):
from .utxo_list import UTXOList from .utxo_list import UTXOList
self.utxo_list = l = UTXOList(self) self.utxo_list = UTXOList(self)
return self.create_list_tab(l) t = self.utxo_list.get_toolbar()
return self.create_list_tab(self.utxo_list, t)
def create_contacts_tab(self): def create_contacts_tab(self):
from .contact_list import ContactList from .contact_list import ContactList
@ -2123,18 +1764,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.need_update.set() # history, addresses, coins self.need_update.set() # history, addresses, coins
self.clear_receive_tab() 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): def paytomany(self):
self.show_send_tab() self.show_send_tab()
self.payto_e.paytomany() self.payto_e.paytomany()
@ -2915,14 +2544,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
def sweep_key_dialog(self): def sweep_key_dialog(self):
d = WindowModalDialog(self, title=_('Sweep private keys')) d = WindowModalDialog(self, title=_('Sweep private keys'))
d.setMinimumSize(600, 300) d.setMinimumSize(600, 300)
vbox = QVBoxLayout(d) vbox = QVBoxLayout(d)
hbox_top = QHBoxLayout() hbox_top = QHBoxLayout()
hbox_top.addWidget(QLabel(_("Enter private keys:"))) hbox_top.addWidget(QLabel(_("Enter private keys:")))
hbox_top.addWidget(InfoButton(WIF_HELP_TEXT), alignment=Qt.AlignRight) hbox_top.addWidget(InfoButton(WIF_HELP_TEXT), alignment=Qt.AlignRight)
vbox.addLayout(hbox_top) vbox.addLayout(hbox_top)
keys_e = ScanQRTextEdit(allow_multi=True) keys_e = ScanQRTextEdit(allow_multi=True)
keys_e.setTabChangesFocus(True) keys_e.setTabChangesFocus(True)
vbox.addWidget(keys_e) vbox.addWidget(keys_e)
@ -2978,14 +2604,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
except Exception as e: # FIXME too broad... except Exception as e: # FIXME too broad...
self.show_message(repr(e)) self.show_message(repr(e))
return return
self.do_clear() scriptpubkey = bfh(bitcoin.address_to_script(addr))
self.tx_external_keypairs = keypairs outputs = [PartialTxOutput(scriptpubkey=scriptpubkey, value='!')]
self.spend_coins(coins)
self.payto_e.setText(addr)
self.spend_max()
self.payto_e.setFrozen(True)
self.amount_e.setFrozen(True)
self.warn_if_watching_only() 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): def _do_import(self, title, header_layout, func):
text = text_dialog(self, title, header_layout, _('Import'), allow_multi=True) text = text_dialog(self, title, header_layout, _('Import'), allow_multi=True)

17
electrum/gui/qt/settings_dialog.py

@ -120,15 +120,6 @@ class SettingsDialog(WindowModalDialog):
fee_type_combo.currentIndexChanged.connect(on_fee_type) fee_type_combo.currentIndexChanged.connect(on_fee_type)
fee_widgets.append((fee_type_label, fee_type_combo)) 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 = bool(self.config.get('use_rbf', True))
use_rbf_cb = QCheckBox(_('Use Replace-By-Fee')) use_rbf_cb = QCheckBox(_('Use Replace-By-Fee'))
use_rbf_cb.setChecked(use_rbf) 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.')) filelogging_cb.setToolTip(_('Debug logs can be persisted to disk. These are useful for troubleshooting.'))
gui_widgets.append((filelogging_cb, None)) 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 = QCheckBox(_('Use change addresses'))
usechange_cb.setChecked(self.window.wallet.use_change) usechange_cb.setChecked(self.window.wallet.use_change)
if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False) if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)

307
electrum/gui/qt/transaction_dialog.py

@ -29,14 +29,18 @@ import datetime
import traceback import traceback
import time import time
from typing import TYPE_CHECKING, Callable from typing import TYPE_CHECKING, Callable
from functools import partial
from decimal import Decimal
from PyQt5.QtCore import QSize, Qt from PyQt5.QtCore import QSize, Qt
from PyQt5.QtGui import QTextCharFormat, QBrush, QFont, QPixmap from PyQt5.QtGui import QTextCharFormat, QBrush, QFont, QPixmap
from PyQt5.QtWidgets import (QDialog, QLabel, QPushButton, QHBoxLayout, QVBoxLayout, from PyQt5.QtWidgets import (QDialog, QLabel, QPushButton, QHBoxLayout, QVBoxLayout, QWidget,
QTextEdit, QFrame, QAction, QToolButton, QMenu) QTextEdit, QFrame, QAction, QToolButton, QMenu, QCheckBox)
import qrcode import qrcode
from qrcode import exceptions from qrcode import exceptions
from electrum.simple_config import SimpleConfig
from electrum.util import quantize_feerate
from electrum.bitcoin import base_encode from electrum.bitcoin import base_encode
from electrum.i18n import _ from electrum.i18n import _
from electrum.plugin import run_hook 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, MONOSPACE_FONT, ColorScheme, ButtonsLineEdit, text_dialog,
char_width_in_lineedit, TRANSACTION_FILE_EXTENSION_FILTER) 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: if TYPE_CHECKING:
from .main_window import ElectrumWindow 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_ENABLED_TOOLTIP = _("Save transaction offline")
SAVE_BUTTON_DISABLED_TOOLTIP = _("Please sign this transaction in order to save it") 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() 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. '''Transactions in the wallet will show their description.
Pass desc to give a description for txs not yet in the wallet. Pass desc to give a description for txs not yet in the wallet.
''' '''
# We want to be a top-level window # We want to be a top-level window
QDialog.__init__(self, parent=None) QDialog.__init__(self, parent=None)
# Take a copy; it might get updated in the main window by self.finalized = finalized
# 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.main_window = parent self.main_window = parent
self.config = parent.config
self.wallet = parent.wallet self.wallet = parent.wallet
self.prompt_if_unsaved = prompt_if_unsaved self.prompt_if_unsaved = prompt_if_unsaved
self.saved = False self.saved = False
self.desc = desc self.desc = desc
self.invoice = invoice 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.setMinimumWidth(950)
self.setWindowTitle(_("Transaction")) self.set_title()
vbox = QVBoxLayout() vbox = QVBoxLayout()
self.setLayout(vbox) self.setLayout(vbox)
@ -115,6 +120,7 @@ class TxDialog(QDialog, MessageBoxMixin):
vbox.addWidget(self.tx_hash_e) vbox.addWidget(self.tx_hash_e)
self.add_tx_stats(vbox) self.add_tx_stats(vbox)
vbox.addSpacing(10) vbox.addSpacing(10)
self.inputs_header = QLabel() self.inputs_header = QLabel()
@ -125,7 +131,6 @@ class TxDialog(QDialog, MessageBoxMixin):
vbox.addWidget(self.outputs_header) vbox.addWidget(self.outputs_header)
self.outputs_textedit = QTextEditWithDefaultSize() self.outputs_textedit = QTextEditWithDefaultSize()
vbox.addWidget(self.outputs_textedit) vbox.addWidget(self.outputs_textedit)
self.sign_button = b = QPushButton(_("Sign")) self.sign_button = b = QPushButton(_("Sign"))
b.clicked.connect(self.sign) b.clicked.connect(self.sign)
@ -133,7 +138,7 @@ class TxDialog(QDialog, MessageBoxMixin):
b.clicked.connect(self.do_broadcast) b.clicked.connect(self.do_broadcast)
self.save_button = b = QPushButton(_("Save")) 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) b.setDisabled(save_button_disabled)
if save_button_disabled: if save_button_disabled:
b.setToolTip(SAVE_BUTTON_DISABLED_TOOLTIP) b.setToolTip(SAVE_BUTTON_DISABLED_TOOLTIP)
@ -148,15 +153,18 @@ class TxDialog(QDialog, MessageBoxMixin):
self.export_actions_menu = export_actions_menu = QMenu() self.export_actions_menu = export_actions_menu = QMenu()
self.add_export_actions_to_menu(export_actions_menu) self.add_export_actions_to_menu(export_actions_menu)
export_actions_menu.addSeparator() export_actions_menu.addSeparator()
if isinstance(tx, PartialTransaction): #if isinstance(tx, PartialTransaction):
export_for_coinjoin_submenu = export_actions_menu.addMenu(_("For CoinJoin; strip privates")) 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) self.add_export_actions_to_menu(export_for_coinjoin_submenu, gettx=self._gettx_for_coinjoin)
self.export_actions_button = QToolButton() self.export_actions_button = QToolButton()
self.export_actions_button.setText(_("Export")) self.export_actions_button.setText(_("Export"))
self.export_actions_button.setMenu(export_actions_menu) self.export_actions_button.setMenu(export_actions_menu)
self.export_actions_button.setPopupMode(QToolButton.InstantPopup) 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() partial_tx_actions_menu = QMenu()
ptx_merge_sigs_action = QAction(_("Merge signatures from"), self) ptx_merge_sigs_action = QAction(_("Merge signatures from"), self)
ptx_merge_sigs_action.triggered.connect(self.merge_sigs) ptx_merge_sigs_action.triggered.connect(self.merge_sigs)
@ -171,20 +179,41 @@ class TxDialog(QDialog, MessageBoxMixin):
# Action buttons # Action buttons
self.buttons = [] self.buttons = []
if isinstance(tx, PartialTransaction): #if isinstance(tx, PartialTransaction):
self.buttons.append(self.partial_tx_actions_button) self.buttons.append(self.partial_tx_actions_button)
self.buttons += [self.sign_button, self.broadcast_button, self.cancel_button] self.buttons += [self.sign_button, self.broadcast_button, self.cancel_button]
# Transaction sharing buttons # 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) run_hook('transaction_dialog', self)
if not self.finalized:
hbox = QHBoxLayout() self.create_fee_controls()
vbox.addWidget(self.feecontrol_fields)
self.hbox = hbox = QHBoxLayout()
hbox.addLayout(Buttons(*self.sharing_buttons)) hbox.addLayout(Buttons(*self.sharing_buttons))
hbox.addStretch(1) hbox.addStretch(1)
hbox.addLayout(Buttons(*self.buttons)) hbox.addLayout(Buttons(*self.buttons))
vbox.addLayout(hbox) 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): def do_broadcast(self):
self.main_window.push_top_level_window(self) self.main_window.push_top_level_window(self)
@ -269,7 +298,7 @@ class TxDialog(QDialog, MessageBoxMixin):
self.sign_button.setDisabled(True) self.sign_button.setDisabled(True)
self.main_window.push_top_level_window(self) 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): def save(self):
self.main_window.push_top_level_window(self) self.main_window.push_top_level_window(self)
@ -341,6 +370,10 @@ class TxDialog(QDialog, MessageBoxMixin):
self.update() self.update()
def update(self): def update(self):
if not self.finalized:
self.update_fee_fields()
if self.tx is None:
return
self.update_io() self.update_io()
desc = self.desc desc = self.desc
base_unit = self.main_window.base_unit() base_unit = self.main_window.base_unit()
@ -373,7 +406,8 @@ class TxDialog(QDialog, MessageBoxMixin):
else: else:
self.date_label.hide() self.date_label.hide()
self.locktime_label.setText(f"LockTime: {self.tx.locktime}") 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: if tx_mined_status.header_hash:
self.block_hash_label.setText(_("Included in block: {}") self.block_hash_label.setText(_("Included in block: {}")
.format(tx_mined_status.header_hash)) .format(tx_mined_status.header_hash))
@ -443,7 +477,7 @@ class TxDialog(QDialog, MessageBoxMixin):
addr = self.wallet.get_txin_address(txin) addr = self.wallet.get_txin_address(txin)
if addr is None: if addr is None:
addr = '' addr = ''
cursor.insertText(addr, text_format(addr)) #cursor.insertText(addr, text_format(addr))
if isinstance(txin, PartialTxInput) and txin.value_sats() is not None: if isinstance(txin, PartialTxInput) and txin.value_sats() is not None:
cursor.insertText(format_amount(txin.value_sats()), ext) cursor.insertText(format_amount(txin.value_sats()), ext)
cursor.insertBlock() cursor.insertBlock()
@ -509,6 +543,11 @@ class TxDialog(QDialog, MessageBoxMixin):
vbox_right.addWidget(self.size_label) vbox_right.addWidget(self.size_label)
self.rbf_label = TxDetailLabel() self.rbf_label = TxDetailLabel()
vbox_right.addWidget(self.rbf_label) 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() self.locktime_label = TxDetailLabel()
vbox_right.addWidget(self.locktime_label) vbox_right.addWidget(self.locktime_label)
self.block_hash_label = TxDetailLabel(word_wrap=True) self.block_hash_label = TxDetailLabel(word_wrap=True)
@ -520,6 +559,19 @@ class TxDialog(QDialog, MessageBoxMixin):
vbox.addLayout(hbox_stats) 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): class QTextEditWithDefaultSize(QTextEdit):
def sizeHint(self): def sizeHint(self):
@ -532,3 +584,190 @@ class TxDetailLabel(QLabel):
self.setTextInteractionFlags(Qt.TextSelectableByMouse) self.setTextInteractionFlags(Qt.TextSelectableByMouse)
if word_wrap is not None: if word_wrap is not None:
self.setWordWrap(word_wrap) 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)

35
electrum/gui/qt/utxo_list.py

@ -28,12 +28,12 @@ from enum import IntEnum
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.QtGui import QStandardItemModel, QStandardItem, QFont 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.i18n import _
from electrum.transaction import PartialTxInput from electrum.transaction import PartialTxInput
from .util import MyTreeView, ColorScheme, MONOSPACE_FONT from .util import MyTreeView, ColorScheme, MONOSPACE_FONT, EnterButton
class UTXOList(MyTreeView): class UTXOList(MyTreeView):
@ -58,6 +58,9 @@ class UTXOList(MyTreeView):
super().__init__(parent, self.create_menu, super().__init__(parent, self.create_menu,
stretch_column=self.Columns.LABEL, stretch_column=self.Columns.LABEL,
editable_columns=[]) 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.setModel(QStandardItemModel(self))
self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.setSortingEnabled(True) self.setSortingEnabled(True)
@ -72,6 +75,11 @@ class UTXOList(MyTreeView):
for idx, utxo in enumerate(utxos): for idx, utxo in enumerate(utxos):
self.insert_utxo(idx, utxo) self.insert_utxo(idx, utxo)
self.filter() 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): def insert_utxo(self, idx, utxo: PartialTxInput):
address = utxo.address address = utxo.address
@ -88,10 +96,13 @@ class UTXOList(MyTreeView):
utxo_item[self.Columns.AMOUNT].setFont(QFont(MONOSPACE_FONT)) utxo_item[self.Columns.AMOUNT].setFont(QFont(MONOSPACE_FONT))
utxo_item[self.Columns.OUTPOINT].setFont(QFont(MONOSPACE_FONT)) utxo_item[self.Columns.OUTPOINT].setFont(QFont(MONOSPACE_FONT))
utxo_item[self.Columns.ADDRESS].setData(name, Qt.UserRole) 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].setBackground(ColorScheme.BLUE.as_color(True))
utxo_item[self.Columns.ADDRESS].setToolTip(_('Address is frozen')) 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].setBackground(ColorScheme.BLUE.as_color(True))
utxo_item[self.Columns.OUTPOINT].setToolTip(f"{name}\n{_('Coin is frozen')}") utxo_item[self.Columns.OUTPOINT].setToolTip(f"{name}\n{_('Coin is frozen')}")
else: else:
@ -106,6 +117,20 @@ class UTXOList(MyTreeView):
return None return None
return [x.data(Qt.UserRole) for x in items] 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): def create_menu(self, position):
selected = self.get_selected_outpoints() selected = self.get_selected_outpoints()
if not selected: if not selected:
@ -113,7 +138,7 @@ class UTXOList(MyTreeView):
menu = QMenu() menu = QMenu()
menu.setSeparatorsCollapsible(True) # consecutive separators are merged together menu.setSeparatorsCollapsible(True) # consecutive separators are merged together
coins = [self.utxo_dict[name] for name in selected] 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) assert len(coins) >= 1, len(coins)
if len(coins) == 1: if len(coins) == 1:
utxo = coins[0] utxo = coins[0]

Loading…
Cancel
Save