From 9d32031ca2ee5f69cfc5b46a6e154f1b1ec04ae4 Mon Sep 17 00:00:00 2001 From: Janus Date: Tue, 13 Nov 2018 16:53:29 +0100 Subject: [PATCH] Kivy: Lightning support in Receive tab --- .gitignore | 4 + electrum/gui/kivy/Makefile | 4 +- electrum/gui/kivy/main_window.py | 9 + electrum/gui/kivy/theming/light/lightning.svg | 6 + .../kivy/theming/light/lightning_switch.svg | 288 ++++++++++++++++++ .../kivy/uix/dialogs/lightning_invoices.py | 65 ++++ electrum/gui/kivy/uix/screens.py | 51 +++- electrum/gui/kivy/uix/ui_screens/receive.kv | 27 +- electrum/lnworker.py | 5 +- 9 files changed, 439 insertions(+), 20 deletions(-) create mode 100644 electrum/gui/kivy/theming/light/lightning.svg create mode 100644 electrum/gui/kivy/theming/light/lightning_switch.svg create mode 100644 electrum/gui/kivy/uix/dialogs/lightning_invoices.py diff --git a/.gitignore b/.gitignore index 34f4b5908..a865b2c91 100644 --- a/.gitignore +++ b/.gitignore @@ -17,8 +17,12 @@ bin/ # icons electrum/gui/kivy/theming/light-0.png +electrum/gui/kivy/theming/light-1.png electrum/gui/kivy/theming/light.atlas electrum/gui/kivy/theming/light/network.png +electrum/gui/kivy/theming/light/lightning_switch_off.png +electrum/gui/kivy/theming/light/lightning_switch_on.png +electrum/gui/kivy/theming/light/lightning.png # tests/tox .tox/ diff --git a/electrum/gui/kivy/Makefile b/electrum/gui/kivy/Makefile index 44667efd1..6c8226095 100644 --- a/electrum/gui/kivy/Makefile +++ b/electrum/gui/kivy/Makefile @@ -5,7 +5,9 @@ PYTHON = python3 .PHONY: theming apk clean theming: - bash -c "convert -background none theming/light/network.{svg,png}" + bash -c 'for i in network lightning; do convert -background none theming/light/$$i.{svg,png}; done' + convert -background none -crop +0+390 theming/light/lightning_switch.svg theming/light/lightning_switch_off.png + convert -background none -crop 840x390+0+0 theming/light/lightning_switch.svg theming/light/lightning_switch_on.png $(PYTHON) -m kivy.atlas theming/light 1024 theming/light/*.png prepare: # running pre build setup diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py index 7acba95cc..519eeb4e3 100644 --- a/electrum/gui/kivy/main_window.py +++ b/electrum/gui/kivy/main_window.py @@ -1016,6 +1016,15 @@ class ElectrumWindow(App): popup = AmountDialog(show_max, amount, cb) popup.open() + def lightning_invoices_dialog(self, cb): + from .uix.dialogs.lightning_invoices import LightningInvoicesDialog + report = self.wallet.lnworker._list_invoices() + if not report['unsettled']: + self.show_info(_('No unsettled invoices. Type in an amount to generate a new one.')) + return + popup = LightningInvoicesDialog(report, cb) + popup.open() + def invoices_dialog(self, screen): from .uix.dialogs.invoices import InvoicesDialog if len(self.wallet.invoices.sorted_list()) == 0: diff --git a/electrum/gui/kivy/theming/light/lightning.svg b/electrum/gui/kivy/theming/light/lightning.svg new file mode 100644 index 000000000..a2ba24a8c --- /dev/null +++ b/electrum/gui/kivy/theming/light/lightning.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/electrum/gui/kivy/theming/light/lightning_switch.svg b/electrum/gui/kivy/theming/light/lightning_switch.svg new file mode 100644 index 000000000..a7e473a0a --- /dev/null +++ b/electrum/gui/kivy/theming/light/lightning_switch.svg @@ -0,0 +1,288 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + ON + OFF + + + diff --git a/electrum/gui/kivy/uix/dialogs/lightning_invoices.py b/electrum/gui/kivy/uix/dialogs/lightning_invoices.py new file mode 100644 index 000000000..56c628d3d --- /dev/null +++ b/electrum/gui/kivy/uix/dialogs/lightning_invoices.py @@ -0,0 +1,65 @@ +from kivy.factory import Factory +from kivy.lang import Builder +from electrum.gui.kivy.i18n import _ +from kivy.uix.recycleview import RecycleView +from electrum.gui.kivy.uix.context_menu import ContextMenu + +Builder.load_string(''' + + addr: '' + desc: '' + screen: None + BoxLayout: + orientation: 'vertical' + Label + text: root.addr + text_size: self.width, None + shorten: True + Label + text: root.desc if root.desc else _('No description') + text_size: self.width, None + shorten: True + font_size: '10dp' + + + id: popup + title: _('Lightning Invoices') + BoxLayout: + orientation: 'vertical' + id: box + RecycleView: + viewclass: 'Item' + id: recycleview + data: [] + RecycleBoxLayout: + default_size: None, dp(56) + default_size_hint: 1, None + size_hint_y: None + height: self.minimum_height + orientation: 'vertical' +''') + +class LightningInvoicesDialog(Factory.Popup): + + def __init__(self, report, callback): + super().__init__() + self.context_menu = None + self.callback = callback + self.menu_actions = [(_('Show'), self.do_show)] + for addr, preimage, pay_req in report['unsettled']: + self.ids.recycleview.data.append({'screen': self, 'addr': pay_req, 'desc': dict(addr.tags).get('d', '')}) + + def do_show(self, obj): + self.hide_menu() + self.dismiss() + self.callback(obj.addr) + + def show_menu(self, obj): + self.hide_menu() + self.context_menu = ContextMenu(obj, self.menu_actions) + self.ids.box.add_widget(self.context_menu) + + def hide_menu(self): + if self.context_menu is not None: + self.ids.box.remove_widget(self.context_menu) + self.context_menu = None diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py index f531aee6b..c2fbfb00b 100644 --- a/electrum/gui/kivy/uix/screens.py +++ b/electrum/gui/kivy/uix/screens.py @@ -15,6 +15,8 @@ from kivy.properties import (ObjectProperty, DictProperty, NumericProperty, from kivy.uix.recycleview import RecycleView from kivy.uix.label import Label +from kivy.uix.behaviors import ToggleButtonBehavior +from kivy.uix.image import Image from kivy.lang import Builder from kivy.factory import Factory @@ -398,6 +400,7 @@ class ReceiveScreen(CScreen): self.screen.address = '' self.screen.amount = '' self.screen.message = '' + self.screen.lnaddr = '' def get_new_address(self) -> bool: """Sets the address field, and returns whether the set address @@ -440,18 +443,30 @@ class ReceiveScreen(CScreen): @profiler def update_qr(self): - uri = self.get_URI() qr = self.screen.ids.qr - qr.set_data(uri) + if self.screen.ids.lnbutton.state == 'down': + qr.set_data(self.screen.lnaddr) + else: + uri = self.get_URI() + qr.set_data(uri) def do_share(self): - uri = self.get_URI() - self.app.do_share(uri, _("Share Bitcoin Request")) + if self.screen.ids.lnbutton.state == 'down': + if self.screen.lnaddr: + self.app.do_share('lightning://' + self.lnaddr, _('Share Lightning invoice')) + else: + uri = self.get_URI() + self.app.do_share(uri, _("Share Bitcoin Request")) def do_copy(self): - uri = self.get_URI() - self.app._clipboard.copy(uri) - self.app.show_info(_('Request copied to clipboard')) + if self.screen.ids.lnbutton.state == 'down': + if self.screen.lnaddr: + self.app._clipboard.copy(self.screen.lnaddr) + self.app.show_info(_('Invoice copied to clipboard')) + else: + uri = self.get_URI() + self.app._clipboard.copy(uri) + self.app.show_info(_('Request copied to clipboard')) def save_request(self): addr = self.screen.address @@ -472,6 +487,9 @@ class ReceiveScreen(CScreen): return added_request def on_amount_or_message(self): + if self.screen.ids.lnbutton.state == 'down': + if self.screen.amount: + self.screen.lnaddr = self.app.wallet.lnworker.add_invoice(self.app.get_amount(self.screen.amount), self.screen.message) Clock.schedule_once(lambda dt: self.update_qr()) def do_new(self): @@ -483,6 +501,13 @@ class ReceiveScreen(CScreen): if self.save_request(): self.app.show_info(_('Request was saved.')) + def do_open_lnaddr(self, lnaddr): + self.clear() + self.screen.lnaddr = lnaddr + obj = lndecode(lnaddr, expected_hrp=constants.net.SEGWIT_HRP) + self.screen.message = dict(obj.tags).get('d', '') + self.screen.amount = self.app.format_amount_and_units(int(obj.amount * bitcoin.COIN)) + self.on_amount_or_message() class TabbedCarousel(Factory.TabbedPanel): '''Custom TabbedPanel using a carousel used in the Main Screen @@ -556,3 +581,15 @@ class TabbedCarousel(Factory.TabbedPanel): self.carousel.add_widget(widget) return super(TabbedCarousel, self).add_widget(widget, index=index) + +class LightningButton(ToggleButtonBehavior, Image): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.source = 'atlas://electrum/gui/kivy/theming/light/lightning_switch_off' + + def on_state(self, widget, value): + self.state = value + if value == 'down': + self.source = 'atlas://electrum/gui/kivy/theming/light/lightning_switch_on' + else: + self.source = 'atlas://electrum/gui/kivy/theming/light/lightning_switch_off' diff --git a/electrum/gui/kivy/uix/ui_screens/receive.kv b/electrum/gui/kivy/uix/ui_screens/receive.kv index 574e16c00..81d47a58b 100644 --- a/electrum/gui/kivy/uix/ui_screens/receive.kv +++ b/electrum/gui/kivy/uix/ui_screens/receive.kv @@ -14,6 +14,7 @@ ReceiveScreen: amount: '' message: '' status: '' + lnaddr: '' on_address: self.parent.on_address(self.address) @@ -30,6 +31,7 @@ ReceiveScreen: FloatLayout: id: bl QRCodeWidget: + opacity: 0 if lnbutton.state == 'down' and not s.lnaddr else 1 id: qr size_hint: None, 1 width: min(self.height, bl.width) @@ -62,15 +64,15 @@ ReceiveScreen: height: blue_bottom.item_height spacing: '5dp' Image: - source: 'atlas://electrum/gui/kivy/theming/light/globe' + source: 'atlas://electrum/gui/kivy/theming/light/lightning' if lnbutton.state == 'down' else 'atlas://electrum/gui/kivy/theming/light/globe' size_hint: None, None size: '22dp', '22dp' pos_hint: {'center_y': .5} BlueButton: id: address_label - text: s.address if s.address else _('Bitcoin Address') + text: (s.address if s.address else _('Bitcoin Address')) if lnbutton.state != 'down' else (s.lnaddr if s.lnaddr else _('Please enter amount')) shorten: True - on_release: Clock.schedule_once(lambda dt: app.addresses_dialog(s)) + on_release: Clock.schedule_once(lambda dt: app.addresses_dialog(s) if lnbutton.state != 'down' else s.parent.do_copy()) CardSeparator: opacity: message_selection.opacity color: blue_bottom.foreground_color @@ -111,15 +113,17 @@ ReceiveScreen: size_hint: 1, None height: '48dp' IconButton: - icon: 'atlas://electrum/gui/kivy/theming/light/save' - size_hint: 0.6, None + opacity: 1 if lnbutton.state != 'down' else 0 + icon: 'atlas://electrum/gui/kivy/theming/light/save' if lnbutton.state != 'down' else '' + size_hint: (0 if lnbutton.state == 'down' else 0.6), None height: '48dp' - on_release: s.parent.do_save() + on_release: s.parent.do_save() if lnbutton.state != 'down' else None + width: (0 if lnbutton.state == 'down' else 100) Button: - text: _('Requests') - size_hint: 1, None + text: _('Requests') if lnbutton.state != 'down' else _('Lightning Invoices') + size_hint: 1 + (.6 if lnbutton.state == 'down' else 0), None height: '48dp' - on_release: Clock.schedule_once(lambda dt: app.requests_dialog(s)) + on_release: Clock.schedule_once(lambda dt: app.requests_dialog(s) if lnbutton.state != 'down' else app.lightning_invoices_dialog(s.parent.do_open_lnaddr)) Button: text: _('Copy') size_hint: 1, None @@ -133,8 +137,11 @@ ReceiveScreen: BoxLayout: size_hint: 1, None height: '48dp' + LightningButton + id: lnbutton + on_state: s.parent.on_amount_or_message() Widget - size_hint: 2, 1 + size_hint: 1, 1 Button: text: _('New') size_hint: 1, None diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 36d17be66..6b7b2e243 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -115,7 +115,8 @@ class LNWorker(PrintError): if report['unsettled']: yield 'Your unsettled invoices:' yield '------------------------' - for addr, preimage in report['unsettled']: + for addr, preimage, pay_req in report['unsettled']: + yield pay_req yield str(addr) yield 'Preimage: ' + bh2u(preimage) yield '' @@ -143,7 +144,7 @@ class LNWorker(PrintError): settled.append((datetime.fromtimestamp(date, timezone.utc), HTLCOwner(direction), htlcobj, preimage)) for preimage, pay_req in invoices.values(): addr = lndecode(pay_req, expected_hrp=constants.net.SEGWIT_HRP) - unsettled.append((addr, bfh(preimage))) + unsettled.append((addr, bfh(preimage), pay_req)) for pay_req, amount_sat in self.paying.values(): addr = lndecode(pay_req, expected_hrp=constants.net.SEGWIT_HRP) if amount_sat is not None: