diff --git a/electrum/commands.py b/electrum/commands.py index 38d856156..bf0ebaa0c 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -1074,17 +1074,20 @@ class Commands(Logger): return wallet.get_unused_address() @command('w') - async def add_request(self, amount, memo='', expiry=3600, force=False, wallet: Abstract_Wallet = None): + async def add_request(self, amount, memo='', expiry=3600, lightning=False, force=False, wallet: Abstract_Wallet = None): """Create a payment request, using the first unused address of the wallet. The address will be considered as used after this operation. If no payment is received, the address will be considered as unused if the payment request is deleted from the wallet.""" - addr = wallet.get_unused_address() - if addr is None: - if force: - addr = wallet.create_new_address(False) - else: - return False amount = satoshis(amount) + if not lightning: + addr = wallet.get_unused_address() + if addr is None: + if force: + addr = wallet.create_new_address(False) + else: + return False + else: + addr = None expiry = int(expiry) if expiry else None key = wallet.create_request(amount, memo, expiry, addr) req = wallet.get_request(key) @@ -1586,6 +1589,8 @@ command_options = { 'addtransaction': (None,'Whether transaction is to be used for broadcasting afterwards. Adds transaction to the wallet'), 'domain': ("-D", "List of addresses"), 'memo': ("-m", "Description of the request"), + 'amount': (None, "Requested amount (in btc)"), + 'lightning': (None, "Create lightning request"), 'expiry': (None, "Time in seconds"), 'timeout': (None, "Timeout in seconds"), 'force': (None, "Create new address beyond gap limit, if no more addresses are available."), diff --git a/electrum/gui/qml/components/ReceiveDetailsDialog.qml b/electrum/gui/qml/components/ReceiveDetailsDialog.qml index 4f9b164f6..23c840e9e 100644 --- a/electrum/gui/qml/components/ReceiveDetailsDialog.qml +++ b/electrum/gui/qml/components/ReceiveDetailsDialog.qml @@ -11,12 +11,13 @@ import "controls" ElDialog { id: dialog - title: qsTr('Receive payment') + title: qsTr('Create Invoice') iconSource: Qt.resolvedUrl('../../icons/tab_receive.png') property alias amount: amountBtc.text property alias description: message.text property alias expiry: expires.currentValue + property bool isLightning: false padding: 0 @@ -93,11 +94,23 @@ ElDialog { } } - FlatButton { - Layout.fillWidth: true - text: qsTr('Create request') - icon.source: '../../icons/confirmed.png' - onClicked: doAccept() + GridLayout { + width: parent.width + columns: 2 + + FlatButton { + Layout.fillWidth: true + text: qsTr('Onchain') + icon.source: '../../icons/bitcoin.png' + onClicked: { dialog.isLightning = false; doAccept() } + } + FlatButton { + Layout.fillWidth: true + enabled: Daemon.currentWallet.isLightning && Daemon.currentWallet.lightningCanReceive.satsInt > amountBtc.textAsSats.satsInt + text: qsTr('Lightning') + icon.source: '../../icons/lightning.png' + onClicked: { dialog.isLightning = true; doAccept() } + } } } diff --git a/electrum/gui/qml/components/ReceiveDialog.qml b/electrum/gui/qml/components/ReceiveDialog.qml index 5ac80e71e..c4837cd66 100644 --- a/electrum/gui/qml/components/ReceiveDialog.qml +++ b/electrum/gui/qml/components/ReceiveDialog.qml @@ -48,21 +48,6 @@ ElDialog { width: parent.width spacing: constants.paddingMedium - states: [ - State { - name: 'bolt11' - PropertyChanges { target: qrloader; sourceComponent: qri_bolt11 } - }, - State { - name: 'bip21uri' - PropertyChanges { target: qrloader; sourceComponent: qri_bip21uri } - }, - State { - name: 'address' - PropertyChanges { target: qrloader; sourceComponent: qri_address } - } - ] - TextHighlightPane { Layout.alignment: Qt.AlignHCenter Layout.fillWidth: true @@ -80,90 +65,15 @@ ElDialog { color: 'white' - Loader { - id: qrloader + QRImage { anchors.centerIn: parent - Component { - id: qri_bolt11 - QRImage { - qrdata: _bolt11 - render: _render_qr - enableToggleText: true - } - } - Component { - id: qri_bip21uri - QRImage { - qrdata: _bip21uri - render: _render_qr - enableToggleText: true - } - } - Component { - id: qri_address - QRImage { - qrdata: _address - render: _render_qr - enableToggleText: true - } - } - } - } - - ButtonContainer { - Layout.fillWidth: true - showSeparator: false - Component { - id: _ind - Rectangle { - color: Material.dialogColor - opacity: parent.checked ? 1 : 0 - radius: 5 - width: parent.width - height: parent.height - - Behavior on opacity { - NumberAnimation { duration: 200 } - } - } - } - TabButton { - id: bolt11Button - Layout.fillWidth: true - Layout.preferredWidth: 1 - text: qsTr('Lightning') - enabled: _bolt11 - checked: rootLayout.state == 'bolt11' - indicator: _ind.createObject() - onClicked: { - rootLayout.state = 'bolt11' - Config.preferredRequestType = 'bolt11' - } - } - TabButton { - id: bip21Button - Layout.fillWidth: true - Layout.preferredWidth: 1 - text: qsTr('URI') - enabled: _bip21uri - checked: rootLayout.state == 'bip21uri' - indicator: _ind.createObject() - onClicked: { - rootLayout.state = 'bip21uri' - Config.preferredRequestType = 'bip21uri' - } - } - TabButton { - id: addressButton - Layout.fillWidth: true - Layout.preferredWidth: 1 - text: qsTr('Address') - checked: rootLayout.state == 'address' - indicator: _ind.createObject() - onClicked: { - rootLayout.state = 'address' - Config.preferredRequestType = 'address' - } + qrdata: _bolt11 + ? _bolt11 + : _bip21uri + ? _bip21uri + : _address + render: _render_qr + enableToggleText: true } } } @@ -307,22 +217,6 @@ ElDialog { RequestDetails { id: request wallet: Daemon.currentWallet - onDetailsChanged: { - var req_type = Config.preferredRequestType - if (bolt11 && req_type == 'bolt11') { - rootLayout.state = 'bolt11' - } else if (bip21 && req_type == 'bip21uri') { - rootLayout.state = 'bip21uri' - } else if (req_type == 'address') { - rootLayout.state = 'address' - } else if (bolt11) { - rootLayout.state = 'bolt11' - } else if (bip21) { - rootLayout.state = 'bip21uri' - } else { - rootLayout.state = 'address' - } - } onStatusChanged: { if (status == RequestDetails.Paid || status == RequestDetails.Unconfirmed) { _ispaid = true diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index bfdb9f522..1edc8299b 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -126,9 +126,9 @@ Item { dialog.open() } - function createRequest(lightning_only, reuse_address) { + function createRequest(lightning, reuse_address) { var qamt = Config.unitsToSats(_request_amount) - Daemon.currentWallet.createRequest(qamt, _request_description, _request_expiry, lightning_only, reuse_address) + Daemon.currentWallet.createRequest(qamt, _request_description, _request_expiry, lightning, reuse_address) } function startSweep() { @@ -596,7 +596,7 @@ Item { _request_amount = _receiveDetailsDialog.amount _request_description = _receiveDetailsDialog.description _request_expiry = _receiveDetailsDialog.expiry - createRequest(false, false) + createRequest(_receiveDetailsDialog.isLightning, false) } onRejected: { console.log('rejected') diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 5442b1e9f..f79532670 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -664,24 +664,25 @@ class QEWallet(AuthMixin, QObject, QtEventListener): @pyqtSlot(QEAmount, str, int, bool) @pyqtSlot(QEAmount, str, int, bool, bool) @pyqtSlot(QEAmount, str, int, bool, bool, bool) - def createRequest(self, amount: QEAmount, message: str, expiration: int, lightning_only: bool = False, reuse_address: bool = False): + def createRequest(self, amount: QEAmount, message: str, expiration: int, lightning: bool = False, reuse_address: bool = False): self.deleteExpiredRequests() try: amount = amount.satsInt - addr = self.wallet.get_unused_address() - if addr is None: - if reuse_address: - addr = self.wallet.get_receiving_address() - elif lightning_only: - addr = None - else: - msg = [ - _('No address available.'), - _('All your addresses are used in pending requests.'), - _('To see the list, press and hold the Receive button.'), - ] - self.requestCreateError.emit(' '.join(msg)) - return + if not lightning: + addr = self.wallet.get_unused_address() + if addr is None: + if reuse_address: + addr = self.wallet.get_receiving_address() + else: + msg = [ + _('No address available.'), + _('All your addresses are used in pending requests.'), + _('To see the list, press and hold the Receive button.'), + ] + self.requestCreateError.emit(' '.join(msg)) + return + else: + addr = None key = self.wallet.create_request(amount, message, expiration, addr) except InvoiceError as e: diff --git a/electrum/gui/qt/receive_tab.py b/electrum/gui/qt/receive_tab.py index 07b2aa8f1..47aa153b4 100644 --- a/electrum/gui/qt/receive_tab.py +++ b/electrum/gui/qt/receive_tab.py @@ -69,13 +69,21 @@ class ReceiveTab(QWidget, MessageBoxMixin, Logger): 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()) + text = _('Onchain') if self.wallet.has_lightning() else _('Request') + self.create_onchain_invoice_button = QPushButton(text) + self.create_onchain_invoice_button.setIcon(read_QIcon("bitcoin.png")) + self.create_onchain_invoice_button.clicked.connect(lambda: self.create_invoice(False)) + self.create_lightning_invoice_button = QPushButton(_('Lightning')) + self.create_lightning_invoice_button.setIcon(read_QIcon("lightning.png")) + self.create_lightning_invoice_button.clicked.connect(lambda: self.create_invoice(True)) + self.create_lightning_invoice_button.setVisible(self.wallet.has_lightning()) + 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) + buttons.addStretch(1) + buttons.addWidget(self.create_onchain_invoice_button) + buttons.addWidget(self.create_lightning_invoice_button) + grid.addLayout(buttons, 4, 1, 1, -1) self.receive_e = QTextEdit() self.receive_e.setFont(QFont(MONOSPACE_FONT)) @@ -116,6 +124,7 @@ class ReceiveTab(QWidget, MessageBoxMixin, Logger): self.receive_widget = ReceiveWidget( self, self.receive_e, self.receive_qr, self.receive_help_widget) + #self.receive_widget.mouseReleaseEvent = lambda x: self.toggle_receive_qr() receive_widget_sp = QSizePolicy(QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.MinimumExpanding) receive_widget_sp.setRetainSizeWhenHidden(True) @@ -138,12 +147,6 @@ class ReceiveTab(QWidget, MessageBoxMixin, Logger): 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) @@ -204,24 +207,6 @@ class ReceiveTab(QWidget, MessageBoxMixin, Logger): 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) @@ -277,16 +262,14 @@ class ReceiveTab(QWidget, MessageBoxMixin, Logger): # 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: + if self.URI: out = self.URI, self.URI, self.URI_help, _('Bitcoin URI') - elif i == 1: + elif self.addr: out = self.addr, self.addr, self.address_help, _('Address') - elif i == 2: + else: # 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') @@ -297,17 +280,16 @@ class ReceiveTab(QWidget, MessageBoxMixin, Logger): text, data, help_text, title = self.get_tab_data() self.window.qr_window.qrw.setData(data) - def create_invoice(self): + def create_invoice(self, is_lightning: bool): 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(): + if is_lightning: address = None - if not self.wallet.has_lightning(): + else: + if amount_sat and amount_sat < self.wallet.dust_threshold(): 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 @@ -358,7 +340,6 @@ class ReceiveTab(QWidget, MessageBoxMixin, Logger): 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() diff --git a/electrum/gui/qt/request_list.py b/electrum/gui/qt/request_list.py index 75cb23a86..92b51a961 100644 --- a/electrum/gui/qt/request_list.py +++ b/electrum/gui/qt/request_list.py @@ -157,6 +157,7 @@ class RequestList(MyTreeView): #items[self.Columns.DATE].setData(request_type, ROLE_REQUEST_TYPE) items[self.Columns.DATE].setData(key, ROLE_KEY) items[self.Columns.DATE].setData(timestamp, ROLE_SORT_ORDER) + items[self.Columns.DATE].setIcon(read_QIcon("lightning" if req.is_lightning() else "bitcoin")) items[self.Columns.AMOUNT].setData(amount_str_nots.strip(), self.ROLE_CLIPBOARD_DATA) items[self.Columns.STATUS].setIcon(read_QIcon(pr_icons.get(status))) self.std_model.insertRow(self.std_model.rowCount(), items) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index a87d9e1b1..356cf90bb 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -109,12 +109,14 @@ class SendTab(QWidget, MessageBoxMixin, Logger): + _('Keyboard shortcut: type "!" to send all your coins.')) amount_label = HelpLabel(_('Amount'), msg) grid.addWidget(amount_label, 3, 0) - grid.addWidget(self.amount_e, 3, 1) + + amount_widgets = QHBoxLayout() + amount_widgets.addWidget(self.amount_e) self.fiat_send_e = AmountEdit(self.fx.get_currency if self.fx else '') if not self.fx or not self.fx.is_enabled(): self.fiat_send_e.setVisible(False) - grid.addWidget(self.fiat_send_e, 3, 2) + amount_widgets.addWidget(self.fiat_send_e) self.amount_e.frozen.connect( lambda: self.fiat_send_e.setFrozen(self.amount_e.isReadOnly())) @@ -125,20 +127,20 @@ class SendTab(QWidget, MessageBoxMixin, Logger): self.max_button.setFixedWidth(btn_width) self.max_button.setCheckable(True) self.max_button.setEnabled(False) - grid.addWidget(self.max_button, 3, 3) + amount_widgets.addWidget(self.max_button) + amount_widgets.addStretch(1) + grid.addLayout(amount_widgets, 3, 1, 1, -1) invoice_error_icon = read_QIcon("warning.png") self.invoice_error = IconLabel(reverse=True, hide_if_empty=True) self.invoice_error.setIcon(invoice_error_icon) grid.addWidget(self.invoice_error, 3, 4, Qt.AlignmentFlag.AlignRight) - self.paste_button = QPushButton() + self.paste_button = QPushButton(_('Paste')) self.paste_button.clicked.connect(self.do_paste) self.paste_button.setIcon(read_QIcon('copy.png')) self.paste_button.setToolTip(_('Paste invoice from clipboard')) - self.paste_button.setMaximumWidth(35) self.paste_button.setFocusPolicy(Qt.FocusPolicy.NoFocus) - grid.addWidget(self.paste_button, 0, 5) self.spinner = QMovie(icon_path('spinner.gif')) self.spinner.setScaledSize(QSize(24, 24)) @@ -155,10 +157,16 @@ class SendTab(QWidget, MessageBoxMixin, Logger): self.send_button.setEnabled(False) self.clear_button = EnterButton(_("Clear"), self.do_clear) - buttons = QHBoxLayout() - buttons.addStretch(1) + #buttons1 = QHBoxLayout() + #buttons1.addWidget(self.paste_button) + #buttons1.addWidget(self.clear_button) + #buttons1.addStretch(1) + #grid.addLayout(buttons1, 0, 1, 1, 4) + buttons = QHBoxLayout() + buttons.addWidget(self.paste_button) buttons.addWidget(self.clear_button) + buttons.addStretch(1) buttons.addWidget(self.save_button) buttons.addWidget(self.send_button) grid.addLayout(buttons, 6, 1, 1, 4) diff --git a/electrum/gui/text.py b/electrum/gui/text.py index e34dbca93..5eeef8828 100644 --- a/electrum/gui/text.py +++ b/electrum/gui/text.py @@ -226,15 +226,16 @@ class ElectrumGui(BaseElectrumGui, EventListener): def print_receive_tab(self): self.stdscr.clear() self.buttons = {} - self.max_pos = 5 + len(list(self.wallet.get_unpaid_requests())) + self.max_pos = 6 + len(list(self.wallet.get_unpaid_requests())) self.index = 0 self.add_edit_line(3, 2, _("Description"), self.str_recv_description, 40) self.add_edit_line(5, 2, _("Amount"), self.str_recv_amount, 15) self.stdscr.addstr(5, 31, self.config.get_base_unit()) self.add_edit_line(7, 2, _("Expiry"), self.str_recv_expiry, 15) - self.add_button(9, 15, _("[Create]"), self.do_create_request) - self.add_button(9, 25, _("[Clear]"), self.do_clear_request) - self.print_requests_list(13, 2, offset_pos=5) + self.add_button(9, 15, _("[Clear]"), self.do_clear_request) + self.add_button(9, 25, _("[Onchain]"), lambda: self.do_create_request(lightning=False)) + self.add_button(9, 35, _("[Lightning]"), lambda: self.do_create_request(lightning=True)) + self.print_requests_list(13, 2, offset_pos=6) return def run_receive_tab(self, c): @@ -244,8 +245,8 @@ class ElectrumGui(BaseElectrumGui, EventListener): self.str_recv_amount = self.edit_str(self.str_recv_amount, c) elif self.pos in self.buttons and c == ord("\n"): self.buttons[self.pos]() - elif self.pos >= 5 and c == ord("\n"): - key = self.requests[self.pos - 5] + elif self.pos >= 6 and c == ord("\n"): + key = self.requests[self.pos - 6] self.show_request(key) def question(self, msg): @@ -557,19 +558,22 @@ class ElectrumGui(BaseElectrumGui, EventListener): self.str_fee = '' self.str_description = '' - def do_create_request(self): - amount_sat = self.parse_amount(self.str_recv_amount) - if not amount_sat: - self.show_message(_('Invalid Amount')) - return - if amount_sat < self.wallet.dust_threshold(): - address = None - if not self.wallet.has_lightning(): + def do_create_request(self, lightning:bool): + amount_sat = self.parse_amount(self.str_recv_amount) or 0 + if not lightning: + if amount_sat and amount_sat < self.wallet.dust_threshold(): + self.show_message(_('Amount too low')) return - else: address = self.wallet.get_unused_address() if not address: + self.show_message(_('Nor more unused sddress')) return + else: + if not self.wallet.has_lightning(): + self.show_message(_('Lightning is disabled on this wallet')) + return + address = None + message = self.str_recv_description expiry = self.config.WALLET_PAYREQ_EXPIRY_SECONDS key = self.wallet.create_request(amount_sat, message, expiry, address) @@ -877,31 +881,26 @@ class ElectrumGui(BaseElectrumGui, EventListener): URI = self.wallet.get_request_URI(req) or '' lnaddr = self.wallet.get_bolt11_invoice(req) or '' w = curses.newwin(self.maxy - 2, self.maxx - 2, 1, 1) - pos = 4 + pos = 2 + text = URI or addr or lnaddr + data = URI or addr or lnaddr.upper() while True: - if pos == 1: - text = URI - data = URI - elif pos == 2: - text = lnaddr - data = lnaddr.upper() - else: - text = addr - data = addr - w.clear() w.border(0) w.addstr(0, 2, ' ' + _('Payment Request') + ' ') y = 2 - w.addstr(y, 2, "Address") - h1 = self.print_textbox(w, y, 13, addr, pos==0) - y += h1 + 2 - w.addstr(y, 2, "URI") - h2 = self.print_textbox(w, y, 13, URI, pos==1) - y += h2 + 2 - w.addstr(y, 2, "Lightning") - h3 = self.print_textbox(w, y, 13, lnaddr, pos==2) - y += h3 + 2 + if URI: + w.addstr(y, 2, "URI") + h = self.print_textbox(w, y, 13, URI, False) + elif addr: + w.addstr(y, 2, "Address") + h = self.print_textbox(w, y, 13, addr, False) + elif lnaddr: + w.addstr(y, 2, "Lightning") + h = self.print_textbox(w, y, 13, lnaddr, False) + else: + return + y += h + 2 lines = self.get_qr(data) qr_width = len(lines) * 2 x = self.maxx - qr_width @@ -909,27 +908,28 @@ class ElectrumGui(BaseElectrumGui, EventListener): self.print_qr(w, 1, x, lines) else: w.addstr(y, 35, "(Window too small for QR code)") - w.addstr(y, 13, "[Delete]", curses.A_REVERSE if pos==3 else curses.color_pair(2)) - w.addstr(y, 25, "[Close]", curses.A_REVERSE if pos==4 else curses.color_pair(2)) + w.addstr(y, 13, "[Copy]", curses.A_REVERSE if pos==0 else curses.color_pair(2)) + w.addstr(y, 23, "[Delete]", curses.A_REVERSE if pos==1 else curses.color_pair(2)) + w.addstr(y, 35, "[Close]", curses.A_REVERSE if pos==2 else curses.color_pair(2)) w.refresh() c = self.getch() - if c in [curses.KEY_UP]: + if c in [curses.KEY_UP, curses.KEY_LEFT]: pos -= 1 - elif c in [curses.KEY_DOWN, ord("\t")]: + elif c in [curses.KEY_DOWN, curses.KEY_RIGHT, ord("\t")]: pos += 1 elif c == ord("\n"): - if pos in [0,1,2]: + if pos == 0: pyperclip.copy(text) self.show_message('Text copied to clipboard') - elif pos == 3: + elif pos == 1: if self.question("Delete Request?"): self.wallet.delete_request(key) self.max_pos -= 1 break - elif pos ==4: + elif pos == 2: break else: break - pos = pos % 5 + pos = pos % 3 self.stdscr.refresh() return diff --git a/electrum/invoices.py b/electrum/invoices.py index 880471785..95fd67004 100644 --- a/electrum/invoices.py +++ b/electrum/invoices.py @@ -344,8 +344,9 @@ class Request(BaseInvoice): ) -> Optional[str]: addr = self.get_address() amount = self.get_amount_sat() - if amount is not None: - amount = int(amount) + if amount is None: + return + amount = int(amount) message = self.message extra = {} if self.time and self.exp: diff --git a/electrum/plugins/payserver/payserver.py b/electrum/plugins/payserver/payserver.py index 64b2f8d69..00b1f6706 100644 --- a/electrum/plugins/payserver/payserver.py +++ b/electrum/plugins/payserver/payserver.py @@ -125,14 +125,22 @@ class PayServer(Logger, EventListener): params = await request.post() wallet = self.wallet if 'amount_sat' not in params or not params['amount_sat'].isdigit(): - raise web.HTTPUnsupportedMediaType() + raise web.HTTPBadRequest(reason='No amount provided') + if 'onchain' in params: + address = wallet.get_unused_address() + if not address: + raise web.HTTPBadRequest(reason='wallet does not have any unused address') + else: + if not wallet.has_lightning(): + raise web.HTTPBadRequest(reason='wallet does not support lightning') + address = None amount = int(params['amount_sat']) message = params['message'] or "donation" key = wallet.create_request( amount_sat=amount, message=message, exp_delay=3600, - address=None) + address=address) raise web.HTTPFound(self.root + '/pay?id=' + key) async def get_request(self, r): diff --git a/electrum/plugins/payserver/www b/electrum/plugins/payserver/www index bde9d3b5f..bcb6d9ecf 160000 --- a/electrum/plugins/payserver/www +++ b/electrum/plugins/payserver/www @@ -1 +1 @@ -Subproject commit bde9d3b5fbf34623ca04c14eb6b0db6676c5ec52 +Subproject commit bcb6d9ecf75b6785451402068651cfa785b709fa diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 51a24d682..12ca0319b 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -1097,7 +1097,6 @@ Warning: setting this to too low will result in lots of payment failures."""), 'gui_qt_tx_dialog_export_include_global_xpubs', default=False, type_=bool, short_desc=lambda: _('For hardware device; include xpubs'), ) - GUI_QT_RECEIVE_TABS_INDEX = ConfigVar('receive_tabs_index', default=0, type_=int) GUI_QT_RECEIVE_TAB_QR_VISIBLE = ConfigVar('receive_qr_visible', default=False, type_=bool) GUI_QT_TX_EDITOR_SHOW_IO = ConfigVar( 'show_tx_io', default=False, type_=bool, diff --git a/electrum/wallet.py b/electrum/wallet.py index d0c1039a1..9c5468776 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -2833,6 +2833,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): return invoice def create_request(self, amount_sat: Optional[int], message: Optional[str], exp_delay: Optional[int], address: Optional[str]): + """ will create a lightning request if address is None """ # for receiving amount_sat = amount_sat or 0 assert isinstance(amount_sat, int), f"{amount_sat!r}" @@ -2841,9 +2842,11 @@ class Abstract_Wallet(ABC, Logger, EventListener): address = address or None # converts "" to None exp_delay = exp_delay or 0 timestamp = int(Request._get_cur_time()) - payment_hash = None # type: Optional[bytes] - if self.has_lightning(): + if address is None: + assert self.has_lightning() payment_hash = self.lnworker.create_payment_info(amount_msat=amount_msat, write_to_disk=False) + else: + payment_hash = None outputs = [PartialTxOutput.from_address_and_value(address, amount_sat)] if address else [] height = self.adb.get_local_height() req = Request( diff --git a/tests/regtest/regtest.sh b/tests/regtest/regtest.sh index 3b2b5d454..62f04943f 100755 --- a/tests/regtest/regtest.sh +++ b/tests/regtest/regtest.sh @@ -133,12 +133,12 @@ if [[ $1 == "breach" ]]; then channel=$($alice open_channel $bob_node 0.15 --password='') new_blocks 3 wait_until_channel_open alice - request=$($bob add_request 0.01 -m "blah" | jq -r ".lightning_invoice") + request=$($bob add_request 0.01 --lightning -m "blah" | jq -r ".lightning_invoice") echo "alice pays" $alice lnpay $request sleep 2 ctx=$($alice get_channel_ctx $channel --iknowwhatimdoing) - request=$($bob add_request 0.01 -m "blah2" | jq -r ".lightning_invoice") + request=$($bob add_request 0.01 --lightning -m "blah2" | jq -r ".lightning_invoice") echo "alice pays again" $alice lnpay $request echo "alice broadcasts old ctx" @@ -289,7 +289,7 @@ if [[ $1 == "extract_preimage" ]]; then wait_until_channel_open alice chan_id=$($alice list_channels | jq -r ".[0].channel_point") # alice pays bob - invoice=$($bob add_request 0.04 -m "test" | jq -r ".lightning_invoice") + invoice=$($bob add_request 0.04 --lightning -m "test" | jq -r ".lightning_invoice") screen -S alice_payment -dm -L -Logfile /tmp/alice/screen.log $alice lnpay $invoice --timeout=600 sleep 1 unsettled=$($alice list_channels | jq '.[] | .local_unsettled_sent') @@ -319,7 +319,7 @@ if [[ $1 == "redeem_htlcs" ]]; then new_blocks 3 wait_until_channel_open alice # alice pays bob - invoice=$($bob add_request 0.04 -m "test" | jq -r ".lightning_invoice") + invoice=$($bob add_request 0.04 --lightning -m "test" | jq -r ".lightning_invoice") $alice lnpay $invoice --timeout=1 || true unsettled=$($alice list_channels | jq '.[] | .local_unsettled_sent') if [[ "$unsettled" == "0" ]]; then @@ -361,7 +361,7 @@ if [[ $1 == "breach_with_unspent_htlc" ]]; then new_blocks 3 wait_until_channel_open alice echo "alice pays bob" - invoice=$($bob add_request 0.04 -m "test" | jq -r ".lightning_invoice") + invoice=$($bob add_request 0.04 --lightning -m "test" | jq -r ".lightning_invoice") $alice lnpay $invoice --timeout=1 || true unsettled=$($alice list_channels | jq '.[] | .local_unsettled_sent') if [[ "$unsettled" == "0" ]]; then @@ -390,7 +390,7 @@ if [[ $1 == "breach_with_spent_htlc" ]]; then new_blocks 3 wait_until_channel_open alice echo "alice pays bob" - invoice=$($bob add_request 0.04 -m "test" | jq -r ".lightning_invoice") + invoice=$($bob add_request 0.04 --lightning -m "test" | jq -r ".lightning_invoice") $alice lnpay $invoice --timeout=1 || true ctx=$($alice get_channel_ctx $channel --iknowwhatimdoing) unsettled=$($alice list_channels | jq '.[] | .local_unsettled_sent') @@ -453,11 +453,11 @@ if [[ $1 == "watchtower" ]]; then new_blocks 3 wait_until_channel_open alice echo "alice pays bob" - invoice1=$($bob add_request 0.01 -m "invoice1" | jq -r ".lightning_invoice") + invoice1=$($bob add_request 0.01 --lightning -m "invoice1" | jq -r ".lightning_invoice") $alice lnpay $invoice1 ctx=$($alice get_channel_ctx $channel --iknowwhatimdoing) echo "alice pays bob again" - invoice2=$($bob add_request 0.01 -m "invoice2" | jq -r ".lightning_invoice") + invoice2=$($bob add_request 0.01 --lightning -m "invoice2" | jq -r ".lightning_invoice") $alice lnpay $invoice2 bob_ctn=$($bob list_channels | jq '.[0].local_ctn') msg="waiting until watchtower is synchronized" @@ -492,7 +492,7 @@ if [[ $1 == "just_in_time" ]]; then wait_until_channel_open carol echo "carol pays alice" # note: set amount to 0.001 to test failure: 'payment too low' - invoice=$($alice add_request 0.01 -m "invoice" | jq -r ".lightning_invoice") + invoice=$($alice add_request 0.01 --lightning -m "invoice" | jq -r ".lightning_invoice") $carol lnpay $invoice fi diff --git a/tests/test_invoices.py b/tests/test_invoices.py index a0aa41680..7011dac4d 100644 --- a/tests/test_invoices.py +++ b/tests/test_invoices.py @@ -45,7 +45,7 @@ class TestWalletPaymentRequests(ElectrumTestCase): self.assertTrue(wallet1.has_lightning()) # create payreq addr = wallet1.get_unused_address() - pr_key = wallet1.create_request(amount_sat=10000, message="msg", address=addr, exp_delay=86400) + pr_key = wallet1.create_request(amount_sat=10000, message="msg", address=None, exp_delay=86400) pr = wallet1.get_request(pr_key) self.assertIsNotNone(pr) self.assertTrue(pr.is_lightning()) @@ -66,7 +66,7 @@ class TestWalletPaymentRequests(ElectrumTestCase): pr_key = wallet1.create_request(amount_sat=10000, message="msg", address=addr, exp_delay=86400) pr = wallet1.get_request(pr_key) self.assertIsNotNone(pr) - self.assertTrue(pr.is_lightning()) + self.assertTrue(not pr.is_lightning()) self.assertEqual(PR_UNPAID, wallet1.get_invoice_status(pr)) self.assertEqual(1000, pr.height) # get paid onchain @@ -126,7 +126,7 @@ class TestWalletPaymentRequests(ElectrumTestCase): pr_key = wallet1.create_request(amount_sat=10000, message="msg", address=addr, exp_delay=86400) pr = wallet1.get_request(pr_key) self.assertIsNotNone(pr) - self.assertTrue(pr.is_lightning()) + self.assertTrue(not pr.is_lightning()) self.assertEqual(PR_UNPAID, wallet1.get_invoice_status(pr)) self.assertEqual(1000, pr.height) # get paid onchain @@ -143,59 +143,6 @@ class TestWalletPaymentRequests(ElectrumTestCase): wallet1.adb.add_verified_tx(tx.txid(), tx_info) self.assertEqual(PR_UNPAID, wallet1.get_invoice_status(pr)) - async def test_wallet_reuse_unused_fallback_onchain_addr_when_getting_paid_with_lightning(self): - text = 'bitter grass shiver impose acquire brush forget axis eager alone wine silver' - d = restore_wallet_from_text(text, path=self.wallet1_path, gap_limit=5, config=self.config) - wallet1 = d['wallet'] # type: Standard_Wallet - self.assertIsNotNone(wallet1.lnworker) - self.assertTrue(wallet1.has_lightning()) - # create payreq1 - addr1 = wallet1.get_unused_address() - pr1_key = wallet1.create_request(amount_sat=10000, message="msg", address=addr1, exp_delay=86400) - pr1 = wallet1.get_request(pr1_key) - self.assertTrue(pr1.is_lightning()) - self.assertEqual(PR_UNPAID, wallet1.get_invoice_status(pr1)) - self.assertEqual(addr1, pr1.get_address()) - self.assertFalse(pr1.has_expired()) - - # create payreq2 - addr2 = wallet1.get_unused_address() - self.assertNotEqual(addr1, addr2) - pr2_key = wallet1.create_request(amount_sat=10000, message="msg", address=addr2, exp_delay=86400) - pr2 = wallet1.get_request(pr2_key) - self.assertTrue(pr2.is_lightning()) - self.assertEqual(PR_UNPAID, wallet1.get_invoice_status(pr2)) - self.assertEqual(addr2, pr2.get_address()) - - # pr1 gets paid on LN - wallet1.lnworker.set_request_status(bytes.fromhex(pr1.rhash), PR_PAID) - self.assertEqual(PR_PAID, wallet1.get_invoice_status(pr1)) - - # create payreq3, which should auto-reuse addr1 - addr3 = wallet1.get_unused_address() - self.assertEqual(addr1, addr3) - pr3_key = wallet1.create_request(amount_sat=10000, message="msg", address=addr3, exp_delay=86400) - pr3 = wallet1.get_request(pr3_key) - self.assertTrue(pr3.is_lightning()) - self.assertEqual(PR_UNPAID, wallet1.get_invoice_status(pr3)) - self.assertEqual(addr3, pr3.get_address()) - - # pr2 gets paid onchain - wallet2 = self.create_wallet2() # type: Standard_Wallet - outputs = [PartialTxOutput.from_address_and_value(pr2.get_address(), pr2.get_amount_sat())] - tx = wallet2.create_transaction(outputs=outputs, fee=5000) - wallet1.adb.receive_tx_callback(tx, TX_HEIGHT_UNCONFIRMED) - self.assertEqual(PR_UNCONFIRMED, wallet1.get_invoice_status(pr2)) - - # create payreq4, which should not reuse addr2 - addr4 = wallet1.get_unused_address() - self.assertEqual(3, len({addr1, addr2, addr3, addr4})) - pr4_key = wallet1.create_request(amount_sat=10000, message="msg", address=addr4, exp_delay=86400) - pr4 = wallet1.get_request(pr4_key) - self.assertTrue(pr4.is_lightning()) - self.assertEqual(PR_UNPAID, wallet1.get_invoice_status(pr4)) - self.assertEqual(addr4, pr4.get_address()) - async def test_wallet_reuse_addr_of_expired_request(self): text = 'bitter grass shiver impose acquire brush forget axis eager alone wine silver' d = restore_wallet_from_text(text, path=self.wallet1_path, gap_limit=3, config=self.config) @@ -206,7 +153,7 @@ class TestWalletPaymentRequests(ElectrumTestCase): addr1 = wallet1.get_unused_address() pr1_key = wallet1.create_request(amount_sat=10000, message="msg", address=addr1, exp_delay=86400) pr1 = wallet1.get_request(pr1_key) - self.assertTrue(pr1.is_lightning()) + self.assertTrue(not pr1.is_lightning()) self.assertEqual(PR_UNPAID, wallet1.get_invoice_status(pr1)) self.assertEqual(addr1, pr1.get_address()) self.assertFalse(pr1.has_expired()) @@ -219,7 +166,7 @@ class TestWalletPaymentRequests(ElectrumTestCase): self.assertEqual(addr1, addr2) pr2_key = wallet1.create_request(amount_sat=10000, message="msg", address=addr2, exp_delay=86400) pr2 = wallet1.get_request(pr2_key) - self.assertTrue(pr2.is_lightning()) + self.assertTrue(not pr2.is_lightning()) self.assertEqual(PR_UNPAID, wallet1.get_invoice_status(pr2)) self.assertEqual(addr2, pr2.get_address()) self.assertFalse(pr2.has_expired())