Files
pallectrum/electrum/gui/qt/receive_tab.py
SomberNight 4a626a113d qt receive_tab: fix "show_address_on_hw" functionality
follow-up b07fe970bf

I don't like this being hidden in the toolbar menu.
The other items in the toolbar menu are ~settings or generic actions
independent of the current request. This one is dependent on the
current request, and even the active "tab"... does not make sense
to show this when the lightning tab is active.
It is more difficult to discover it in the first place than previously,
and it being less visible goes against encouraging hw device users of
using it, which is what we should be doing.

Anyway, this commit just makes it functional as-is.
2023-03-31 01:14:08 +00:00

422 lines
18 KiB
Python

# Copyright (C) 2022 The Electrum developers
# Distributed under the MIT software license, see the accompanying
# file LICENCE or http://www.opensource.org/licenses/mit-license.php
from typing import Optional, TYPE_CHECKING
from PyQt5.QtGui import QFont, QCursor
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtWidgets import (QComboBox, QLabel, QVBoxLayout, QGridLayout, QLineEdit, QTextEdit,
QHBoxLayout, QPushButton, QWidget, QSizePolicy, QFrame)
from electrum.bitcoin import is_address
from electrum.i18n import _
from electrum.util import InvoiceError
from electrum.invoices import PR_DEFAULT_EXPIRATION_WHEN_CREATING
from electrum.invoices import PR_EXPIRED, pr_expiration_values
from electrum.logging import Logger
from .amountedit import AmountEdit, BTCAmountEdit, SizedFreezableLineEdit
from .qrcodewidget import QRCodeWidget
from .util import read_QIcon, ColorScheme, HelpLabel, WWLabel, MessageBoxMixin, MONOSPACE_FONT
from .util import ButtonsTextEdit, get_iconname_qrcode
if TYPE_CHECKING:
from . import ElectrumGui
from .main_window import ElectrumWindow
class ReceiveTab(QWidget, MessageBoxMixin, Logger):
# strings updated by update_current_request
addr = ''
lnaddr = ''
URI = ''
address_help = ''
URI_help = ''
ln_help = ''
def __init__(self, window: 'ElectrumWindow'):
QWidget.__init__(self, window)
Logger.__init__(self)
self.window = window
self.wallet = window.wallet
self.fx = window.fx
self.config = window.config
# A 4-column grid layout. All the stretch is in the last column.
# The exchange rate plugin adds a fiat widget in column 2
self.receive_grid = grid = QGridLayout()
grid.setSpacing(8)
grid.setColumnStretch(3, 1)
self.receive_message_e = SizedFreezableLineEdit(width=400)
grid.addWidget(QLabel(_('Description')), 0, 0)
grid.addWidget(self.receive_message_e, 0, 1, 1, 4)
self.receive_amount_e = BTCAmountEdit(self.window.get_decimal_point)
grid.addWidget(QLabel(_('Requested amount')), 1, 0)
grid.addWidget(self.receive_amount_e, 1, 1)
self.fiat_receive_e = AmountEdit(self.fx.get_currency if self.fx else '')
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.window.connect_fields(self.receive_amount_e, self.fiat_receive_e)
self.expiry_button = QPushButton('')
self.expiry_button.clicked.connect(self.expiry_dialog)
grid.addWidget(QLabel(_('Expiry')), 2, 0)
grid.addWidget(self.expiry_button, 2, 1)
self.clear_invoice_button = QPushButton(_('Clear'))
self.clear_invoice_button.clicked.connect(self.do_clear)
self.create_invoice_button = QPushButton(_('Create Request'))
self.create_invoice_button.clicked.connect(lambda: self.create_invoice())
self.receive_buttons = buttons = QHBoxLayout()
buttons.addStretch(1)
buttons.addWidget(self.clear_invoice_button)
buttons.addWidget(self.create_invoice_button)
grid.addLayout(buttons, 4, 0, 1, -1)
self.receive_e = QTextEdit()
self.receive_e.setFont(QFont(MONOSPACE_FONT))
self.receive_e.setReadOnly(True)
self.receive_e.setContextMenuPolicy(Qt.NoContextMenu)
self.receive_e.setTextInteractionFlags(Qt.NoTextInteraction)
self.receive_e.textChanged.connect(self.update_receive_widgets)
self.receive_qr = QRCodeWidget(manual_size=True)
self.receive_help_text = WWLabel('')
self.receive_rebalance_button = QPushButton('Rebalance')
self.receive_rebalance_button.suggestion = None
def on_receive_rebalance():
if self.receive_rebalance_button.suggestion:
chan1, chan2, delta = self.receive_rebalance_button.suggestion
self.window.rebalance_dialog(chan1, chan2, amount_sat=delta)
self.receive_rebalance_button.clicked.connect(on_receive_rebalance)
self.receive_swap_button = QPushButton('Swap')
self.receive_swap_button.suggestion = None
def on_receive_swap():
if self.receive_swap_button.suggestion:
chan, swap_recv_amount_sat = self.receive_swap_button.suggestion
self.window.run_swap_dialog(is_reverse=True, recv_amount_sat=swap_recv_amount_sat, channels=[chan])
self.receive_swap_button.clicked.connect(on_receive_swap)
buttons = QHBoxLayout()
buttons.addWidget(self.receive_rebalance_button)
buttons.addWidget(self.receive_swap_button)
vbox = QVBoxLayout()
vbox.addWidget(self.receive_help_text)
vbox.addLayout(buttons)
self.receive_help_widget = FramedWidget()
self.receive_help_widget.setVisible(False)
self.receive_help_widget.setLayout(vbox)
self.receive_widget = ReceiveWidget(
self, self.receive_e, self.receive_qr, self.receive_help_widget)
receive_widget_sp = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
receive_widget_sp.setRetainSizeWhenHidden(True)
self.receive_widget.setSizePolicy(receive_widget_sp)
self.receive_widget.setVisible(False)
self.receive_requests_label = QLabel(_('Requests'))
# with QDarkStyle, this label may partially cover the qrcode widget.
# setMaximumWidth prevents that
self.receive_requests_label.setMaximumWidth(400)
from .request_list import RequestList
self.request_list = RequestList(self)
# toolbar
self.toolbar, menu = self.request_list.create_toolbar_with_menu('')
self.toggle_qr_button = QPushButton('')
self.toggle_qr_button.setIcon(read_QIcon(get_iconname_qrcode()))
self.toggle_qr_button.setToolTip(_('Switch between text and QR code view'))
self.toggle_qr_button.clicked.connect(self.toggle_receive_qr)
self.toggle_qr_button.setEnabled(False)
self.toolbar.insertWidget(2, self.toggle_qr_button)
self.toggle_view_button = QPushButton('')
self.toggle_view_button.setToolTip(_('switch between view'))
self.toggle_view_button.clicked.connect(self.toggle_view)
self.toggle_view_button.setEnabled(False)
self.update_view_button()
self.toolbar.insertWidget(2, self.toggle_view_button)
# menu
menu.addConfig(
_('Add on-chain fallback to lightning requests'), 'bolt11_fallback', True,
callback=self.on_toggle_bolt11_fallback)
menu.addConfig(
_('Add lightning requests to bitcoin URIs'), 'bip21_lightning', False,
tooltip=_('This may result in large QR codes'),
callback=self.update_current_request)
self.qr_menu_action = menu.addToggle(_("Show detached QR code window"), self.window.toggle_qr_window)
menu.addAction(_("Import requests"), self.window.import_requests)
menu.addAction(_("Export requests"), self.window.export_requests)
menu.addAction(_("Delete expired requests"), self.request_list.delete_expired_requests)
self.toolbar_menu = menu
# layout
vbox_g = QVBoxLayout()
vbox_g.addLayout(grid)
vbox_g.addStretch()
hbox = QHBoxLayout()
hbox.addLayout(vbox_g)
hbox.addStretch()
hbox.addWidget(self.receive_widget)
self.searchable_list = self.request_list
vbox = QVBoxLayout(self)
vbox.addLayout(self.toolbar)
vbox.addLayout(hbox)
vbox.addStretch()
vbox.addWidget(self.receive_requests_label)
vbox.addWidget(self.request_list)
vbox.setStretchFactor(hbox, 40)
vbox.setStretchFactor(self.request_list, 60)
self.request_list.update() # after parented and put into a layout, can update without flickering
self.update_expiry_text()
def update_expiry_text(self):
expiry = self.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING)
text = pr_expiration_values[expiry]
self.expiry_button.setText(text)
def expiry_dialog(self):
msg = ''.join([
_('Expiration period of your request.'), ' ',
_('This information is seen by the recipient if you send them a signed payment request.'),
'\n\n',
_('For on-chain requests, the address gets reserved until expiration. After that, it might get reused.'), ' ',
_('The bitcoin address never expires and will always be part of this electrum wallet.'), ' ',
_('You can reuse a bitcoin address any number of times but it is not good for your privacy.'),
'\n\n',
_('For Lightning requests, payments will not be accepted after the expiration.'),
])
expiry = self.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING)
v = self.window.query_choice(msg, pr_expiration_values, title=_('Expiry'), default_choice=expiry)
if v is None:
return
self.config.set_key('request_expiry', v)
self.update_expiry_text()
def on_toggle_bolt11_fallback(self):
if not self.wallet.lnworker:
return
self.wallet.lnworker.clear_invoices_cache()
self.update_current_request()
def update_view_button(self):
i = self.config.get('receive_tabs_index', 0)
if i == 0:
icon, text = read_QIcon("link.png"), _('Bitcoin URI')
elif i == 1:
icon, text = read_QIcon("bitcoin.png"), _('Address')
elif i == 2:
icon, text = read_QIcon("lightning.png"), _('Lightning')
self.toggle_view_button.setText(text)
self.toggle_view_button.setIcon(icon)
def toggle_view(self):
i = self.config.get('receive_tabs_index', 0)
i = (i + 1) % (3 if self.wallet.has_lightning() else 2)
self.config.set_key('receive_tabs_index', i)
self.update_current_request()
self.update_view_button()
def on_tab_changed(self):
text, data, help_text, title = self.get_tab_data()
self.window.do_copy(data, title=title)
self.update_receive_qr_window()
def do_copy(self, e):
if e.button() != Qt.LeftButton:
return
text, data, help_text, title = self.get_tab_data()
self.window.do_copy(data, title=title)
def toggle_receive_qr(self):
b = not self.config.get('receive_qr_visible', False)
self.config.set_key('receive_qr_visible', b)
self.update_receive_widgets()
def update_receive_widgets(self):
b = self.config.get('receive_qr_visible', False)
self.receive_widget.update_visibility(b)
def update_current_request(self):
key = self.request_list.get_current_key()
req = self.wallet.get_request(key) if key else None
if req is None:
self.receive_e.setText('')
self.addr = self.URI = self.lnaddr = ''
self.address_help = self.URI_help = self.ln_help = ''
return
help_texts = self.wallet.get_help_texts_for_receive_request(req)
self.addr = (req.get_address() or '') if not help_texts.address_is_error else ''
self.URI = (self.wallet.get_request_URI(req) or '') if not help_texts.URI_is_error else ''
self.lnaddr = self.wallet.get_bolt11_invoice(req) if not help_texts.ln_is_error else ''
self.address_help = help_texts.address_help
self.URI_help = help_texts.URI_help
self.ln_help = help_texts.ln_help
can_rebalance = help_texts.can_rebalance()
can_swap = help_texts.can_swap()
self.receive_rebalance_button.suggestion = help_texts.ln_rebalance_suggestion
self.receive_swap_button.suggestion = help_texts.ln_swap_suggestion
self.receive_rebalance_button.setVisible(can_rebalance)
self.receive_swap_button.setVisible(can_swap)
self.receive_rebalance_button.setEnabled(can_rebalance and self.window.num_tasks() == 0)
self.receive_swap_button.setEnabled(can_swap and self.window.num_tasks() == 0)
text, data, help_text, title = self.get_tab_data()
self.receive_e.setText(text)
self.receive_qr.setData(data)
self.receive_help_text.setText(help_text)
for w in [self.receive_e, self.receive_qr]:
w.setEnabled(bool(text) and not help_text)
w.setToolTip(help_text)
# macOS hack (similar to #4777)
self.receive_e.repaint()
# always show
self.receive_widget.setVisible(True)
self.toggle_qr_button.setEnabled(True)
self.toggle_view_button.setEnabled(True)
self.update_receive_qr_window()
def get_tab_data(self):
i = self.config.get('receive_tabs_index', 0)
if i == 0:
out = self.URI, self.URI, self.URI_help, _('Bitcoin URI')
elif i == 1:
out = self.addr, self.addr, self.address_help, _('Address')
elif i == 2:
# encode lightning invoices as uppercase so QR encoding can use
# alphanumeric mode; resulting in smaller QR codes
out = self.lnaddr, self.lnaddr.upper(), self.ln_help, _('Lightning Request')
return out
def update_receive_qr_window(self):
if self.window.qr_window and self.window.qr_window.isVisible():
text, data, help_text, title = self.get_tab_data()
self.window.qr_window.qrw.setData(data)
def create_invoice(self):
amount_sat = self.receive_amount_e.get_amount()
message = self.receive_message_e.text()
expiry = self.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING)
if amount_sat and amount_sat < self.wallet.dust_threshold():
address = None
if not self.wallet.has_lightning():
self.show_error(_('Amount too small to be received onchain'))
return
else:
address = self.get_bitcoin_address_for_request(amount_sat)
if not address:
return
self.window.address_list.update()
# generate even if we cannot receive
try:
key = self.wallet.create_request(amount_sat, message, expiry, address)
except InvoiceError as e:
self.show_error(_('Error creating payment request') + ':\n' + str(e))
return
except Exception as e:
self.logger.exception('Error adding payment request')
self.show_error(_('Error adding payment request') + ':\n' + repr(e))
return
assert key is not None
self.window.address_list.refresh_all()
self.request_list.update()
self.request_list.set_current_key(key)
# clear request fields
self.receive_amount_e.setText('')
self.receive_message_e.setText('')
# copy current tab to clipboard
self.on_tab_changed()
def get_bitcoin_address_for_request(self, amount) -> Optional[str]:
addr = self.wallet.get_unused_address()
if addr is None:
if not self.wallet.is_deterministic(): # imported wallet
msg = [
_('No more addresses in your wallet.'), ' ',
_('You are using a non-deterministic wallet, which cannot create new addresses.'), ' ',
_('If you want to create new addresses, use a deterministic wallet instead.'), '\n\n',
_('Creating a new payment request will reuse one of your addresses and overwrite an existing request. Continue anyway?'),
]
if not self.question(''.join(msg)):
return
addr = self.wallet.get_receiving_address()
else: # deterministic wallet
if not self.question(_("Warning: The next address will not be recovered automatically if you restore your wallet from seed; you may need to add it manually.\n\nThis occurs because you have too many unused addresses in your wallet. To avoid this situation, use the existing addresses first.\n\nCreate anyway?")):
return
addr = self.wallet.create_new_address(False)
return addr
def do_clear(self):
self.receive_e.setText('')
self.addr = self.URI = self.lnaddr = ''
self.address_help = self.URI_help = self.ln_help = ''
self.receive_widget.setVisible(False)
self.toggle_qr_button.setEnabled(False)
self.toggle_view_button.setEnabled(False)
self.receive_message_e.setText('')
self.receive_amount_e.setAmount(None)
self.request_list.clearSelection()
class ReceiveWidget(QWidget):
min_size = QSize(200, 200)
def __init__(self, receive_tab: 'ReceiveTab', textedit: QWidget, qr: QWidget, help_widget: QWidget):
QWidget.__init__(self)
self.textedit = textedit
self.qr = qr
self.help_widget = help_widget
self.setMinimumSize(self.min_size)
for w in [textedit, qr, help_widget]:
w.setMinimumSize(self.min_size)
for w in [textedit, qr]:
w.mousePressEvent = receive_tab.do_copy
w.setCursor(QCursor(Qt.PointingHandCursor))
textedit.setFocusPolicy(Qt.NoFocus)
if isinstance(help_widget, QLabel):
help_widget.setFrameStyle(QFrame.StyledPanel)
help_widget.setStyleSheet("QLabel {border:1px solid gray; border-radius:2px; }")
hbox = QHBoxLayout()
hbox.setContentsMargins(0, 0, 0, 0)
hbox.addWidget(textedit)
hbox.addWidget(help_widget)
hbox.addWidget(qr)
self.setLayout(hbox)
def update_visibility(self, is_qr):
if str(self.textedit.toPlainText()):
self.help_widget.setVisible(False)
self.textedit.setVisible(not is_qr)
self.qr.setVisible(is_qr)
else:
self.help_widget.setVisible(True)
self.textedit.setVisible(False)
self.qr.setVisible(False)
def resizeEvent(self, e):
# keep square aspect ratio when resized
size = e.size()
w = size.height()
self.setFixedWidth(w)
return super().resizeEvent(e)
class FramedWidget(QFrame):
def __init__(self):
QFrame.__init__(self)
self.setFrameStyle(QFrame.StyledPanel)
self.setStyleSheet("FramedWidget {border:1px solid gray; border-radius:2px; }")