electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 1352b0ce9f3549f9bdb98a68dac87235c6ec4e7e
parent f803bb571d196dcc02a7a97406aa221b800f8b41
Author: Janus <ysangkok@gmail.com>
Date:   Thu,  8 Nov 2018 11:38:18 +0100

Kivy: Support Lightning in Send tab

Diffstat:
Melectrum/gui/kivy/main_window.py | 16++++++++++++----
Melectrum/gui/kivy/uix/screens.py | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Melectrum/gui/kivy/uix/ui_screens/send.kv | 23++++++++++++-----------
Melectrum/lnworker.py | 1+
4 files changed, 88 insertions(+), 25 deletions(-)

diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py @@ -162,11 +162,16 @@ class ElectrumWindow(App): self.switch_to('send') self.send_screen.set_URI(uri) + def set_ln_invoice(self, invoice): + self.switch_to('send') + self.send_screen.set_ln_invoice(invoice) + def on_new_intent(self, intent): - if intent.getScheme() != 'bitcoin': - return - uri = intent.getDataString() - self.set_URI(uri) + data = intent.getDataString() + if intent.getScheme() == 'bitcoin': + self.set_URI(data) + elif intent.getScheme() == 'lightning': + self.set_ln_invoice(data) def on_language(self, instance, language): Logger.info('language: {}'.format(language)) @@ -355,6 +360,9 @@ class ElectrumWindow(App): if data.startswith('bitcoin:'): self.set_URI(data) return + if data.startswith('ln'): + self.set_ln_invoice(data) + return # try to decode transaction from electrum.transaction import Transaction from electrum.util import bh2u diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py @@ -1,8 +1,10 @@ +import asyncio from weakref import ref from decimal import Decimal import re import datetime import traceback, sys +from enum import Enum, auto from kivy.app import App from kivy.cache import Cache @@ -19,19 +21,26 @@ from kivy.factory import Factory from kivy.utils import platform from electrum.util import profiler, parse_URI, format_time, InvalidPassword, NotEnoughFunds, Fiat -from electrum import bitcoin +from electrum import bitcoin, constants from electrum.transaction import TxOutput, Transaction, tx_from_str from electrum.util import send_exception_to_crash_reporter, parse_URI, InvalidBitcoinURI from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED from electrum.plugin import run_hook from electrum.wallet import InternalAddressCorruption from electrum import simple_config +from electrum.lnaddr import lndecode +from electrum.lnutil import RECEIVED, SENT from .context_menu import ContextMenu from electrum.gui.kivy.i18n import _ +class Destination(Enum): + Address = auto() + PR = auto() + LN = auto() + class HistoryRecycleView(RecycleView): pass @@ -184,7 +193,19 @@ class SendScreen(CScreen): self.screen.message = uri.get('message', '') self.screen.amount = self.app.format_amount_and_units(amount) if amount else '' self.payment_request = None - self.screen.is_pr = False + self.screen.destinationtype = Destination.Address + + def set_ln_invoice(self, invoice): + try: + lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP) + except Exception as e: + self.app.show_info(invoice + _(" is not a valid Lightning invoice: ") + repr(e)) # repr because str(Exception()) == '' + return + self.screen.address = invoice + self.screen.message = dict(lnaddr.tags).get('d', None) + self.screen.amount = self.app.format_amount_and_units(lnaddr.amount * bitcoin.COIN) if lnaddr.amount else '' + self.payment_request = None + self.screen.destinationtype = Destination.LN def update(self): if self.app.wallet and self.payment_request_queued: @@ -196,7 +217,7 @@ class SendScreen(CScreen): self.screen.message = '' self.screen.address = '' self.payment_request = None - self.screen.is_pr = False + self.screen.destinationtype = Destination.Address def set_request(self, pr): self.screen.address = pr.get_requestor() @@ -204,16 +225,16 @@ class SendScreen(CScreen): self.screen.amount = self.app.format_amount_and_units(amount) if amount else '' self.screen.message = pr.get_memo() if pr.is_pr(): - self.screen.is_pr = True + self.screen.destinationtype = Destination.PR self.payment_request = pr else: - self.screen.is_pr = False + self.screen.destinationtype = Destination.Address self.payment_request = None def do_save(self): if not self.screen.address: return - if self.screen.is_pr: + if self.screen.destinationtype == Destination.PR: # it should be already saved return # save address as invoice @@ -226,10 +247,10 @@ class SendScreen(CScreen): self.app.wallet.invoices.add(pr) self.app.show_info(_("Invoice saved")) if pr.is_pr(): - self.screen.is_pr = True + self.screen.destinationtype = Destination.PR self.payment_request = pr else: - self.screen.is_pr = False + self.screen.destinationtype = Destination.Address self.payment_request = None def do_paste(self): @@ -248,10 +269,42 @@ class SendScreen(CScreen): self.app.tx_dialog(tx) return # try to decode as URI/address - self.set_URI(data) + if data.startswith('ln'): + self.set_ln_invoice(data.rstrip()) + else: + self.set_URI(data) + + def _do_send_lightning(self): + if not self.screen.amount: + self.app.show_error(_('Since the invoice contained no amount, you must enter one')) + return + invoice = self.screen.address + amount_sat = self.app.get_amount(self.screen.amount) + try: + addr = self.app.wallet.lnworker._check_invoice(invoice, amount_sat) + route = self.app.wallet.lnworker._create_route_from_invoice(decoded_invoice=addr) + except Exception as e: + self.app.show_error(_('Could not find path for payment. Check if you have open channels. Error details:') + ':\n' + repr(e)) + self.app.network.register_callback(self.payment_completed_async_thread, ['ln_payment_completed']) + _addr, _peer, coro = self.app.wallet.lnworker._pay(invoice, amount_sat) + fut = asyncio.run_coroutine_threadsafe(coro, self.app.network.asyncio_loop) + fut.add_done_callback(self.ln_payment_result) + + def payment_completed_async_thread(self, event, direction, htlc, preimage): + Clock.schedule_once(lambda dt: self.payment_completed(direction, htlc, preimage)) + + def payment_completed(self, direction, htlc, preimage): + self.app.show_info(_('Payment received') if direction == RECEIVED else _('Payment sent')) + + def ln_payment_result(self, fut): + if fut.exception(): + self.app.show_error(_('Lightning payment failed:') + '\n' + repr(fut.exception())) def do_send(self): - if self.screen.is_pr: + if self.screen.destinationtype == Destination.LN: + self._do_send_lightning() + return + elif self.screen.destinationtype == Destination.PR: if self.payment_request.has_expired(): self.app.show_error(_('Payment request has expired')) return diff --git a/electrum/gui/kivy/uix/ui_screens/send.kv b/electrum/gui/kivy/uix/ui_screens/send.kv @@ -1,4 +1,5 @@ #:import _ electrum.gui.kivy.i18n._ +#:import Destination electrum.gui.kivy.uix.screens.Destination #:import Decimal decimal.Decimal #:set btc_symbol chr(171) #:set mbtc_symbol chr(187) @@ -11,7 +12,7 @@ SendScreen: address: '' amount: '' message: '' - is_pr: False + destinationtype: Destination.Address BoxLayout padding: '12dp', '12dp', '12dp', '12dp' spacing: '12dp' @@ -36,7 +37,7 @@ SendScreen: on_release: Clock.schedule_once(lambda dt: app.show_info(_('Copy and paste the recipient address using the Paste button, or use the camera to scan a QR code.'))) #on_release: Clock.schedule_once(lambda dt: app.popup_dialog('contacts')) CardSeparator: - opacity: int(not root.is_pr) + opacity: int(root.destinationtype == Destination.Address) color: blue_bottom.foreground_color BoxLayout: size_hint: 1, None @@ -52,10 +53,10 @@ SendScreen: id: amount_e default_text: _('Amount') text: s.amount if s.amount else _('Amount') - disabled: root.is_pr + disabled: root.destinationtype == Destination.PR or root.destinationtype == Destination.LN and not s.amount on_release: Clock.schedule_once(lambda dt: app.amount_dialog(s, True)) CardSeparator: - opacity: int(not root.is_pr) + opacity: int(root.destinationtype == Destination.Address) color: blue_bottom.foreground_color BoxLayout: id: message_selection @@ -69,27 +70,27 @@ SendScreen: pos_hint: {'center_y': .5} BlueButton: id: description - text: s.message if s.message else (_('No Description') if root.is_pr else _('Description')) - disabled: root.is_pr + text: s.message if s.message else ({Destination.LN: _('Lightning invoice contains no description'), Destination.Address: _('Description'), Destination.PR: _('No Description')}[root.destinationtype]) + disabled: root.destinationtype != Destination.Address on_release: Clock.schedule_once(lambda dt: app.description_dialog(s)) CardSeparator: - opacity: int(not root.is_pr) + opacity: int(root.destinationtype == Destination.Address) color: blue_bottom.foreground_color BoxLayout: size_hint: 1, None - height: blue_bottom.item_height + height: blue_bottom.item_height if root.destinationtype != Destination.LN else 0 spacing: '5dp' Image: source: 'atlas://electrum/gui/kivy/theming/light/star_big_inactive' - opacity: 0.7 + opacity: 0.7 if root.destinationtype != Destination.LN else 0 size_hint: None, None size: '22dp', '22dp' pos_hint: {'center_y': .5} BlueButton: id: fee_e default_text: _('Fee') - text: app.fee_status - on_release: Clock.schedule_once(lambda dt: app.fee_dialog(s, True)) + text: app.fee_status if root.destinationtype != Destination.LN else '' + on_release: Clock.schedule_once(lambda dt: app.fee_dialog(s, True)) if root.destinationtype != Destination.LN else None BoxLayout: size_hint: 1, None height: '48dp' diff --git a/electrum/lnworker.py b/electrum/lnworker.py @@ -97,6 +97,7 @@ class LNWorker(PrintError): l.append((time.time(), direction, json.loads(encoder.encode(htlc)), bh2u(preimage))) self.wallet.storage.put('lightning_payments_completed', l) self.wallet.storage.write() + self.network.trigger_callback('ln_payment_completed', direction, htlc, preimage) def list_invoices(self): report = self._list_invoices()