commit 7db9a22d6354c72e0ca0f267866030cd6d35aca5
parent 3430d1aaa3238e51ea514a4f0c11b9f61b5b2e52
Author: Janus <ysangkok@gmail.com>
Date: Mon, 12 Nov 2018 18:01:59 +0100
Kivy: open channel dialog
Diffstat:
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]: