You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

415 lines
18 KiB

# 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_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(self.config.cv.WALLET_BOLT11_FALLBACK, callback=self.on_toggle_bolt11_fallback)
menu.addConfig(self.config.cv.WALLET_BIP21_LIGHTNING, 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.WALLET_PAYREQ_EXPIRY_SECONDS
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.WALLET_PAYREQ_EXPIRY_SECONDS
v = self.window.query_choice(msg, pr_expiration_values(), title=_('Expiry'), default_choice=expiry)
if v is None:
return
self.config.WALLET_PAYREQ_EXPIRY_SECONDS = 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.GUI_QT_RECEIVE_TABS_INDEX
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.GUI_QT_RECEIVE_TABS_INDEX
i = (i + 1) % (3 if self.wallet.has_lightning() else 2)
self.config.GUI_QT_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(text, 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(text, title=title)
def toggle_receive_qr(self):
b = not self.config.GUI_QT_RECEIVE_TAB_QR_VISIBLE
self.config.GUI_QT_RECEIVE_TAB_QR_VISIBLE = b
self.update_receive_widgets()
def update_receive_widgets(self):
b = self.config.GUI_QT_RECEIVE_TAB_QR_VISIBLE
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.GUI_QT_RECEIVE_TABS_INDEX
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.WALLET_PAYREQ_EXPIRY_SECONDS
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; }")