Browse Source

Kivy: open channel dialog

dependabot/pip/contrib/deterministic-build/ecdsa-0.13.3
Janus 6 years ago
committed by ThomasV
parent
commit
7db9a22d63
  1. 1
      .gitignore
  2. 1
      electrum/gui/kivy/Makefile
  3. 5
      electrum/gui/kivy/main.kv
  4. 12
      electrum/gui/kivy/main_window.py
  5. BIN
      electrum/gui/kivy/theming/light/network.png
  6. 6
      electrum/gui/kivy/theming/light/network.svg
  7. 54
      electrum/gui/kivy/uix/dialogs/lightning_channels.py
  8. 141
      electrum/gui/kivy/uix/dialogs/lightning_open_channel.py
  9. 93
      electrum/gui/kivy/uix/dialogs/lightning_payer.py
  10. 8
      electrum/gui/kivy/uix/screens.py
  11. 3
      electrum/lnworker.py

1
.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/

1
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

5
electrum/gui/kivy/main.kv

@ -449,12 +449,9 @@ BoxLayout:
ActionOvrButton:
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')

12
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)

BIN
electrum/gui/kivy/theming/light/network.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

6
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>

After

Width:  |  Height:  |  Size: 1.3 KiB

54
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}

141
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()

93
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)

8
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)

3
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]:

Loading…
Cancel
Save