electrum

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

commit 7db9a22d6354c72e0ca0f267866030cd6d35aca5
parent 3430d1aaa3238e51ea514a4f0c11b9f61b5b2e52
Author: Janus <ysangkok@gmail.com>
Date:   Mon, 12 Nov 2018 18:01:59 +0100

Kivy: open channel dialog

Diffstat:
M.gitignore | 1+
Melectrum/gui/kivy/Makefile | 1+
Melectrum/gui/kivy/main.kv | 5+----
Melectrum/gui/kivy/main_window.py | 12++++++++----
Delectrum/gui/kivy/theming/light/network.png | 0
Aelectrum/gui/kivy/theming/light/network.svg | 6++++++
Melectrum/gui/kivy/uix/dialogs/lightning_channels.py | 54+++++++++++++++++++++++++++++++++++++-----------------
Aelectrum/gui/kivy/uix/dialogs/lightning_open_channel.py | 141+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Delectrum/gui/kivy/uix/dialogs/lightning_payer.py | 93-------------------------------------------------------------------------------
Melectrum/gui/kivy/uix/screens.py | 8+++++---
Melectrum/lnworker.py | 3+++
11 files changed, 203 insertions(+), 121 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -18,6 +18,7 @@ bin/ # icons electrum/gui/kivy/theming/light-0.png electrum/gui/kivy/theming/light.atlas +electrum/gui/kivy/theming/light/network.png # tests/tox .tox/ diff --git a/electrum/gui/kivy/Makefile b/electrum/gui/kivy/Makefile @@ -5,6 +5,7 @@ PYTHON = python3 .PHONY: theming apk clean theming: + bash -c "convert -background none theming/light/network.{svg,png}" $(PYTHON) -m kivy.atlas theming/light 1024 theming/light/*.png prepare: # running pre build setup diff --git a/electrum/gui/kivy/main.kv b/electrum/gui/kivy/main.kv @@ -450,11 +450,8 @@ BoxLayout: name: 'network' text: _('Network') ActionOvrButton: - name: 'lightning_payer_dialog' - text: _('Pay Lightning Invoice') - ActionOvrButton: name: 'lightning_channels_dialog' - text: _('Lightning Channels') + text: _('Channels') ActionOvrButton: name: 'settings' text: _('Settings') diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py @@ -75,7 +75,7 @@ from electrum.util import (base_units, NoDynamicFeeEstimates, decimal_point_to_b base_unit_name_to_decimal_point, NotEnoughFunds, UnknownBaseUnit, DECIMAL_POINT_DEFAULT) -from .uix.dialogs.lightning_payer import LightningPayerDialog +from .uix.dialogs.lightning_open_channel import LightningOpenChannelDialog from .uix.dialogs.lightning_channels import LightningChannelsDialog class ElectrumWindow(App): @@ -645,8 +645,8 @@ class ElectrumWindow(App): self._settings_dialog.update() self._settings_dialog.open() - def lightning_payer_dialog(self): - d = LightningPayerDialog(self) + def lightning_open_channel_dialog(self): + d = LightningOpenChannelDialog(self) d.open() def lightning_channels_dialog(self): @@ -803,7 +803,11 @@ class ElectrumWindow(App): inputs = self.wallet.get_spendable_coins(None, self.electrum_config) if not inputs: return '' - addr = str(self.send_screen.screen.address) or self.wallet.dummy_address() + addr = None + if self.send_screen: + addr = str(self.send_screen.screen.address) + if not addr: + addr = self.wallet.dummy_address() outputs = [TxOutput(TYPE_ADDRESS, addr, '!')] try: tx = self.wallet.make_unsigned_transaction(inputs, outputs, self.electrum_config) diff --git a/electrum/gui/kivy/theming/light/network.png b/electrum/gui/kivy/theming/light/network.png Binary files differ. diff --git a/electrum/gui/kivy/theming/light/network.svg b/electrum/gui/kivy/theming/light/network.svg @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg width="512" height="512" version="1.1" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <use transform="translate(-216 -252)" width="100%" height="100%" xlink:href="#path831"/> + <path id="path831" d="m244.94 256.71c-14.934 1.4562-25.469 19.091-24.913 37.987-2.155 55.246-0.0543 91.614 0.17146 143.49 0.95499 15.061 13.656 34.066 35.042 34.181 6.5619 0.30953 16.143 0.34717 30.995 0.34717 27.995 0 41.291 0.77865 41.291 2.4214 0 1.3327-2.9613 5.3827-6.5786 9-5.0074 5.0074-8.6562 6.5787-15.298 6.5787-10.341 0-14.124 3.1317-14.124 11.698v6.3018h144v-6.3018c0-8.5666-3.7834-11.698-14.124-11.698-6.6413 0-10.29-1.5713-15.297-6.5787-3.6174-3.6173-6.5786-7.6673-6.5786-9 0-1.6428 13.297-2.4214 41.291-2.4214 15.112-7e-3 63.701 6.378 66.081-35.293 0.62209-11.405 0.62843-32.056 0.62843-72.708 0-39.56-7e-3 -60.178-0.58008-71.767-0.6711-12.6-1.8798-36.301-24.834-36.233h-118.59zm10.586 36h216v144h-216v-72z" fill="#fff"/> + <path d="m110.41 469.11c-4.9443-1.8094-13.262-7.4832-18.485-12.608-15.863-15.568-16.401-19.038-16.401-105.69v-76.1h36v150.96l5.5228 5.5227c5.4382 5.4383 6.1188 5.5228 44.468 5.5228h38.945c3.4461 14.807 6.6856 24.504 14.871 36-30.131-0.82757-61.12 0.99452-90.818-1.2282-6.8699-0.53546-11.167-1.3039-14.104-2.3788z" fill="#fff"/> +</svg> diff --git a/electrum/gui/kivy/uix/dialogs/lightning_channels.py b/electrum/gui/kivy/uix/dialogs/lightning_channels.py @@ -7,22 +7,35 @@ from kivy.clock import Clock from electrum.gui.kivy.uix.context_menu import ContextMenu from electrum.util import bh2u from electrum.lnutil import LOCAL, REMOTE +from electrum.gui.kivy.i18n import _ -Builder.load_string(''' +Builder.load_string(r''' <LightningChannelItem@CardItem> details: {} active: False channelId: '<channelId not set>' + id: card + _chan: None Label: + color: (.5,.5,.5,1) if not card.active else (1,1,1,1) text: root.channelId + Label: + text: _('State:\n') + (card._chan.get_state() if card._chan else 'n/a') + font_size: '10sp' <LightningChannelsDialog@Popup>: name: 'lightning_channels' - title: 'Lightning channels. Tap to select.' + title: _('Lightning channels. Tap for options.') + id: popup BoxLayout: id: box orientation: 'vertical' spacing: '1dp' + Button: + size_hint: 1, None + height: '48dp' + text: _('New channel...') + on_press: popup.app.popup_dialog('lightning_open_channel_dialog') ScrollView: GridLayout: cols: 1 @@ -95,7 +108,7 @@ class LightningChannelsDialog(Factory.Popup): def show_channel_details(self, obj): p = Factory.ChannelDetailsPopup() - p.title = 'Lightning channels details for ' + self.presentable_chan_id(obj._chan) + p.title = _('Details for channel ') + self.presentable_chan_id(obj._chan) p.data = [{'keyName': key, 'value': str(obj.details[key])} for key in obj.details.keys()] p.open() @@ -104,25 +117,28 @@ class LightningChannelsDialog(Factory.Popup): coro = asyncio.run_coroutine_threadsafe(self.app.wallet.lnworker.close_channel(obj._chan.channel_id), loop) try: coro.result(5) - self.app.show_info('Channel closed') + self.app.show_info(_('Channel closed')) except Exception as e: - self.app.show_info('Could not close channel: ' + repr(e)) # repr because str(Exception()) == '' + self.app.show_info(_('Could not close channel: ') + repr(e)) # repr because str(Exception()) == '' def force_close_channel(self, obj): + if obj._chan.get_state() == 'CLOSED': + self.app.show_error(_('Channel already closed')) + return loop = self.app.wallet.network.asyncio_loop coro = asyncio.run_coroutine_threadsafe(self.app.wallet.lnworker.force_close_channel(obj._chan.channel_id), loop) try: coro.result(1) - self.app.show_info('Channel closed, you may need to wait at least ' + str(obj._chan.config[REMOTE].to_self_delay) + ' blocks, because of CSV delays') + self.app.show_info(_('Channel closed, you may need to wait at least {} blocks, because of CSV delays'.format(obj._chan.config[REMOTE].to_self_delay))) except Exception as e: - self.app.show_info('Could not force close channel: ' + repr(e)) # repr because str(Exception()) == '' + self.app.show_info(_('Could not force close channel: ') + repr(e)) # repr because str(Exception()) == '' def show_menu(self, obj): self.hide_menu() self.context_menu = ContextMenu(obj, [ - ("Force close", self.force_close_channel), - ("Co-op close", self.close_channel), - ("Details", self.show_channel_details)]) + (_("Force close"), self.force_close_channel), + (_("Co-op close"), self.close_channel), + (_("Details"), self.show_channel_details)]) self.ids.box.add_widget(self.context_menu) def hide_menu(self): @@ -136,6 +152,8 @@ class LightningChannelsDialog(Factory.Popup): def channels_update(self, evt): channel_cards = self.ids.lightning_channels_container channel_cards.clear_widgets() + if not self.app.wallet: + return lnworker = self.app.wallet.lnworker for i in lnworker.channels.values(): item = Factory.LightningChannelItem() @@ -147,10 +165,12 @@ class LightningChannelsDialog(Factory.Popup): channel_cards.add_widget(item) def channel_details(self, chan): - return {'Node ID': bh2u(chan.node_id), - 'Channel ID': bh2u(chan.channel_id), - 'Capacity': self.app.format_amount_and_units(chan.constraints.capacity), - 'Funding TXID': chan.funding_outpoint.txid, - 'Short Chan ID': bh2u(chan.short_channel_id) if chan.short_channel_id else 'Not available', - 'Available to spend': self.app.format_amount_and_units(chan.available_to_spend(LOCAL) // 1000), - 'State': chan.get_state()} + return {_('Node ID'): bh2u(chan.node_id), + _('Channel ID'): bh2u(chan.channel_id), + _('Capacity'): self.app.format_amount_and_units(chan.constraints.capacity), + _('Funding TXID'): chan.funding_outpoint.txid, + _('Short Chan ID'): bh2u(chan.short_channel_id) if chan.short_channel_id else _('Not available'), + _('Available to spend'): self.app.format_amount_and_units(chan.available_to_spend(LOCAL) // 1000), + _('State'): chan.get_state(), + _('Initiator'): 'Opened/funded by us' if chan.constraints.is_initiator else 'Opened/funded by remote party', + _('Current feerate'): chan.constraints.feerate} diff --git a/electrum/gui/kivy/uix/dialogs/lightning_open_channel.py b/electrum/gui/kivy/uix/dialogs/lightning_open_channel.py @@ -0,0 +1,141 @@ +from kivy.lang import Builder +from kivy.factory import Factory +from electrum.gui.kivy.i18n import _ +from electrum.lnaddr import lndecode +from electrum.gui.kivy.uix.dialogs.choice_dialog import ChoiceDialog +from electrum.util import bh2u +from electrum.bitcoin import COIN +import electrum.simple_config as config +from .label_dialog import LabelDialog + +Builder.load_string(''' +<LightningOpenChannelDialog@Popup> + id: s + name: 'lightning_open_channel' + title: _('Open Lightning Channel') + pubkey: '' + amount: '' + ipport: '' + BoxLayout + spacing: '12dp' + padding: '12dp' + orientation: 'vertical' + SendReceiveBlueBottom: + id: blue_bottom + size_hint: 1, None + height: self.minimum_height + BoxLayout: + size_hint: 1, None + height: blue_bottom.item_height + Image: + source: 'atlas://electrum/gui/kivy/theming/light/globe' + size_hint: None, None + size: '22dp', '22dp' + pos_hint: {'center_y': .5} + BlueButton: + text: s.pubkey if s.pubkey else _('Node ID, [pubkey]@[host]:[port]') + shorten: True + on_release: s.choose_node() + IconButton: + on_release: app.scan_qr(on_complete=s.on_pubkey) + icon: 'atlas://electrum/gui/kivy/theming/light/camera' + color: blue_bottom.foreground_color + size: '22dp', '22dp' + pos_hint: {'center_y': .5} + size_hint: None, None + CardSeparator: + color: blue_bottom.foreground_color + BoxLayout: + size_hint: 1, None + height: blue_bottom.item_height + Image: + source: 'atlas://electrum/gui/kivy/theming/light/network' + size_hint: None, None + size: '22dp', '22dp' + pos_hint: {'center_y': .5} + BlueButton: + text: s.ipport if s.ipport else _('Auto-detect IP/port') + on_release: s.ipport_dialog() + CardSeparator: + color: blue_bottom.foreground_color + BoxLayout: + size_hint: 1, None + height: blue_bottom.item_height + Image: + source: 'atlas://electrum/gui/kivy/theming/light/calculator' + size_hint: None, None + size: '22dp', '22dp' + pos_hint: {'center_y': .5} + BlueButton: + text: s.amount if s.amount else _('Channel capacity amount') + on_release: app.amount_dialog(s, True) + Button: + size_hint: 1, None + height: blue_bottom.item_height + text: _('Paste') + on_release: s.do_paste() + Button: + size_hint: 1, None + height: blue_bottom.item_height + text: _('Open Channel') + on_release: s.do_open_channel() +''') + +class LightningOpenChannelDialog(Factory.Popup): + def ipport_dialog(self): + def callback(text): + self.ipport = text + d = LabelDialog(_('IP/port in format:\n[host]:[port]'), self.ipport, callback) + d.open() + + def on_pubkey(self, data): + self.pubkey = data.replace('\n', '') # strip newlines if we choose from ChoiseDialog + + def choose_node(self): + lines = [] + suggested = self.app.wallet.lnworker.suggest_peer() + if suggested: + assert len(suggested) == 33 + for i in range(0, 34, 11): + lines += [bh2u(suggested[i:i+11])] + servers = ['\n'.join(lines)] + ChoiceDialog(_('Choose node to connect to'), sorted(servers), self.pubkey, self.on_pubkey).open() + + def __init__(self, app, lnaddr=None, msg=None): + super(LightningOpenChannelDialog, self).__init__() + self.app = app + self.lnaddr = lnaddr + self.msg = msg + + def open(self, *args, **kwargs): + super(LightningOpenChannelDialog, self).open(*args, **kwargs) + if self.lnaddr: + fee = self.app.electrum_config.fee_per_kb() + if not fee: + fee = config.FEERATE_FALLBACK_STATIC_FEE + self.amount = self.app.format_amount_and_units(self.lnaddr.amount * COIN + fee * 2) + self.pubkey = bh2u(self.lnaddr.pubkey.serialize()) + if self.msg: + self.app.show_info(self.msg) + + def do_paste(self): + contents = self.app._clipboard.paste() + if not contents: + self.app.show_info(_("Clipboard is empty")) + return + self.pubkey = contents + + def do_open_channel(self): + if not self.pubkey or not self.amount: + self.app.show_info(_('All fields must be filled out')) + return + conn_str = self.pubkey + if self.ipport: + conn_str += '@' + self.ipport.strip() + try: + node_id_hex = self.app.wallet.lnworker.open_channel(conn_str, self.app.get_amount(self.amount), 0) + except Exception as e: + self.app.show_error(_('Problem opening channel: ') + '\n' + repr(e)) + return + self.app.show_info(_('Please wait for confirmation, channel is opening with node ') + node_id_hex[:16]) + self.dismiss() diff --git a/electrum/gui/kivy/uix/dialogs/lightning_payer.py b/electrum/gui/kivy/uix/dialogs/lightning_payer.py @@ -1,93 +0,0 @@ -import binascii -from kivy.lang import Builder -from kivy.factory import Factory -from electrum.gui.kivy.i18n import _ -from kivy.clock import mainthread -from electrum.lnaddr import lndecode - -Builder.load_string(''' -<LightningPayerDialog@Popup> - id: s - name: 'lightning_payer' - invoice_data: '' - BoxLayout: - orientation: "vertical" - BlueButton: - text: s.invoice_data if s.invoice_data else _('Lightning invoice') - shorten: True - on_release: Clock.schedule_once(lambda dt: app.show_info(_('Copy and paste the lightning invoice using the Paste button, or use the camera to scan a QR code.'))) - GridLayout: - cols: 4 - size_hint: 1, None - height: '48dp' - IconButton: - id: qr - on_release: Clock.schedule_once(lambda dt: app.scan_qr(on_complete=s.on_lightning_qr)) - icon: 'atlas://gui/kivy/theming/light/camera' - Button: - text: _('Paste') - on_release: s.do_paste() - Button: - text: _('Paste using xclip') - on_release: s.do_paste_xclip() - Button: - text: _('Clear') - on_release: s.do_clear() - Button: - size_hint: 1, None - height: '48dp' - text: _('Open channel to pubkey in invoice') - on_release: s.do_open_channel() - Button: - size_hint: 1, None - height: '48dp' - text: _('Pay pasted/scanned invoice') - on_release: s.do_pay() -''') - -class LightningPayerDialog(Factory.Popup): - def __init__(self, app): - super(LightningPayerDialog, self).__init__() - self.app = app - - #def open(self, *args, **kwargs): - # super(LightningPayerDialog, self).open(*args, **kwargs) - #def dismiss(self, *args, **kwargs): - # super(LightningPayerDialog, self).dismiss(*args, **kwargs) - - def do_paste_xclip(self): - import subprocess - proc = subprocess.run(["xclip","-sel","clipboard","-o"], stdout=subprocess.PIPE) - self.invoice_data = proc.stdout.decode("ascii") - - def do_paste(self): - contents = self.app._clipboard.paste() - if not contents: - self.app.show_info(_("Clipboard is empty")) - return - self.invoice_data = contents - - def do_clear(self): - self.invoice_data = "" - - def do_open_channel(self): - compressed_pubkey_bytes = lndecode(self.invoice_data).pubkey.serialize() - hexpubkey = binascii.hexlify(compressed_pubkey_bytes).decode("ascii") - local_amt = 200000 - push_amt = 100000 - - def on_success(pw): - # node_id, local_amt, push_amt, emit_function, get_password - self.app.wallet.lnworker.open_channel_from_other_thread(hexpubkey, local_amt, push_amt, mainthread(lambda parent: self.app.show_info(_("Channel open, waiting for locking..."))), lambda: pw) - - if self.app.wallet.has_keystore_encryption(): - # wallet, msg, on_success (Tuple[str, str] -> ()), on_failure (() -> ()) - self.app.password_dialog(self.app.wallet, _("Password needed for opening channel"), on_success, lambda: self.app.show_error(_("Failed getting password from you"))) - else: - on_success("") - - def do_pay(self): - self.app.wallet.lnworker.pay_invoice_from_other_thread(self.invoice_data) - - def on_lightning_qr(self, data): - self.invoice_data = str(data) diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py @@ -32,7 +32,7 @@ from electrum.lnaddr import lndecode from electrum.lnutil import RECEIVED, SENT from .context_menu import ContextMenu - +from .dialogs.lightning_open_channel import LightningOpenChannelDialog from electrum.gui.kivy.i18n import _ @@ -280,11 +280,13 @@ class SendScreen(CScreen): return invoice = self.screen.address amount_sat = self.app.get_amount(self.screen.amount) + addr = self.app.wallet.lnworker._check_invoice(invoice, amount_sat) 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)) + dia = LightningOpenChannelDialog(self.app, addr, str(e) + _(':\nYou can open a channel.')) + dia.open() + return 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) diff --git a/electrum/lnworker.py b/electrum/lnworker.py @@ -240,6 +240,7 @@ class LNWorker(PrintError): if conf >= chan.constraints.funding_txn_minimum_depth > 0: chan.short_channel_id = chan.short_channel_id_predicted self.save_channel(chan) + self.on_channels_updated() return True, conf return False, conf @@ -255,6 +256,7 @@ class LNWorker(PrintError): if is_spent: if chan.get_state() != 'FORCE_CLOSING': chan.set_state("CLOSED") + self.on_channels_updated() self.channel_db.remove_channel(chan.short_channel_id) self.network.trigger_callback('channel', chan) @@ -543,6 +545,7 @@ class LNWorker(PrintError): tx = chan.force_close_tx() chan.set_state('FORCE_CLOSING') self.save_channel(chan) + self.on_channels_updated() return await self.network.broadcast_transaction(tx) def _get_next_peers_to_try(self) -> Sequence[LNPeerAddr]: