URI: 
       tKivy: Support Lightning in Send tab - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 1352b0ce9f3549f9bdb98a68dac87235c6ec4e7e
   DIR parent f803bb571d196dcc02a7a97406aa221b800f8b41
  HTML Author: Janus <ysangkok@gmail.com>
       Date:   Thu,  8 Nov 2018 11:38:18 +0100
       
       Kivy: Support Lightning in Send tab
       
       Diffstat:
         M electrum/gui/kivy/main_window.py    |      16 ++++++++++++----
         M electrum/gui/kivy/uix/screens.py    |      73 ++++++++++++++++++++++++++-----
         M electrum/gui/kivy/uix/ui_screens/s… |      23 ++++++++++++-----------
         M electrum/lnworker.py                |       1 +
       
       4 files changed, 88 insertions(+), 25 deletions(-)
       ---
   DIR diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py
       t@@ -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))
       t@@ -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
   DIR diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py
       t@@ -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
       t@@ -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
        
       t@@ -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:
       t@@ -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()
       t@@ -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
       t@@ -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):
       t@@ -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
   DIR diff --git a/electrum/gui/kivy/uix/ui_screens/send.kv b/electrum/gui/kivy/uix/ui_screens/send.kv
       t@@ -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)
       t@@ -11,7 +12,7 @@ SendScreen:
            address: ''
            amount: ''
            message: ''
       -    is_pr: False
       +    destinationtype: Destination.Address
            BoxLayout
                padding: '12dp', '12dp', '12dp', '12dp'
                spacing: '12dp'
       t@@ -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
       t@@ -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
       t@@ -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'
   DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py
       t@@ -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()