LBRY-Vault/electrum/gui/kivy/uix/dialogs/lightning_channels.py
2020-03-06 05:50:45 +01:00

311 lines
10 KiB
Python

import asyncio
import binascii
from kivy.lang import Builder
from kivy.factory import Factory
from kivy.uix.popup import Popup
from kivy.clock import Clock
from electrum.util import bh2u
from electrum.lnutil import LOCAL, REMOTE, format_short_channel_id
from electrum.gui.kivy.i18n import _
from .question import Question
Builder.load_string(r'''
<LightningChannelItem@CardItem>
details: {}
active: False
short_channel_id: '<channelId not set>'
status: ''
local_balance: ''
remote_balance: ''
_chan: None
BoxLayout:
spacing: '8dp'
height: '32dp'
orientation: 'vertical'
Widget
CardLabel:
color: (.5,.5,.5,1) if not root.active else (1,1,1,1)
text: root.short_channel_id
font_size: '15sp'
Widget
CardLabel:
font_size: '13sp'
shorten: True
text: root.status
Widget
BoxLayout:
spacing: '8dp'
height: '32dp'
orientation: 'vertical'
Widget
CardLabel:
text: root.local_balance
font_size: '13sp'
halign: 'right'
Widget
CardLabel:
text: root.remote_balance
font_size: '13sp'
halign: 'right'
Widget
<LightningChannelsDialog@Popup>:
name: 'lightning_channels'
title: _('Lightning channels.')
can_send: ''
can_receive: ''
id: popup
BoxLayout:
id: box
orientation: 'vertical'
spacing: '2dp'
padding: '12dp'
BoxLabel:
text: _('Can send') + ':'
value: root.can_send
BoxLabel:
text: _('Can receive') + ':'
value: root.can_receive
ScrollView:
GridLayout:
cols: 1
id: lightning_channels_container
size_hint: 1, None
height: self.minimum_height
spacing: '2dp'
BoxLayout:
size_hint: 1, None
height: '48dp'
Widget:
size_hint: 0.7, None
height: '48dp'
Button:
size_hint: 0.3, None
height: '48dp'
text: _('New...')
on_press: popup.app.popup_dialog('lightning_open_channel_dialog')
<ChannelDetailsPopup@Popup>:
id: popuproot
data: []
is_closed: False
is_redeemed: False
node_id:''
short_id:''
initiator:''
capacity:''
funding_txid:''
state:''
local_ctn:0
remote_ctn:0
local_csv:0
remote_csv:0
feerate:0
can_send:''
can_receive:''
BoxLayout:
padding: '12dp', '12dp', '12dp', '12dp'
spacing: '12dp'
orientation: 'vertical'
ScrollView:
scroll_type: ['bars', 'content']
scroll_wheel_distance: dp(114)
BoxLayout:
orientation: 'vertical'
height: self.minimum_height
size_hint_y: None
spacing: '5dp'
BoxLabel:
text: _('Channel ID')
value: root.short_id
BoxLabel:
text: _('State')
value: root.state
BoxLabel:
text: _('Initiator')
value: root.initiator
BoxLabel:
text: _('Capacity')
value: root.capacity
BoxLabel:
text: _('Can send')
value: root.can_send
BoxLabel:
text: _('Can receive')
value: root.can_receive
BoxLabel:
text: _('CSV delay')
value: 'Local: %d\nRemote: %d' % (root.local_csv, root.remote_csv)
BoxLabel:
text: _('CTN')
value: 'Local: %d\nRemote: %d' % (root.local_ctn, root.remote_ctn)
BoxLabel:
text: _('Fee rate')
value: '%d sat/kilobyte' % (root.feerate)
Widget:
size_hint: 1, 0.1
TopLabel:
text: _('Remote Node ID')
TxHashLabel:
data: root.node_id
name: _('Remote Node ID')
TopLabel:
text: _('Funding Transaction')
TxHashLabel:
data: root.funding_txid
name: _('Funding Transaction')
touch_callback: lambda: app.show_transaction(root.funding_txid)
Widget:
size_hint: 1, 0.1
Widget:
size_hint: 1, 0.05
BoxLayout:
size_hint: 1, None
height: '48dp'
Button:
size_hint: 0.5, None
height: '48dp'
text: _('Close')
on_release: root.close()
disabled: root.is_closed
Button:
size_hint: 0.5, None
height: '48dp'
text: _('Force-close')
on_release: root.force_close()
disabled: root.is_closed
Button:
size_hint: 0.5, None
height: '48dp'
text: _('Delete')
on_release: root.remove_channel()
disabled: not root.is_redeemed
''')
class ChannelDetailsPopup(Popup):
def __init__(self, chan, app, **kwargs):
super(ChannelDetailsPopup,self).__init__(**kwargs)
self.is_closed = chan.is_closed()
self.is_redeemed = chan.is_redeemed()
self.app = app
self.chan = chan
self.title = _('Channel details')
self.node_id = bh2u(chan.node_id)
self.channel_id = bh2u(chan.channel_id)
self.funding_txid = chan.funding_outpoint.txid
self.short_id = format_short_channel_id(chan.short_channel_id)
self.capacity = self.app.format_amount_and_units(chan.constraints.capacity)
self.state = self.app.wallet.lnworker.get_channel_status(chan)
self.local_ctn = chan.get_latest_ctn(LOCAL)
self.remote_ctn = chan.get_latest_ctn(REMOTE)
self.local_csv = chan.config[LOCAL].to_self_delay
self.remote_csv = chan.config[REMOTE].to_self_delay
self.initiator = 'Local' if chan.constraints.is_initiator else 'Remote'
self.feerate = chan.get_latest_feerate(LOCAL)
self.can_send = self.app.format_amount_and_units(chan.available_to_spend(LOCAL) // 1000)
self.can_receive = self.app.format_amount_and_units(chan.available_to_spend(REMOTE) // 1000)
def close(self):
Question(_('Close channel?'), self._close).open()
def _close(self, b):
if not b:
return
loop = self.app.wallet.network.asyncio_loop
coro = asyncio.run_coroutine_threadsafe(self.app.wallet.lnworker.close_channel(self.chan.channel_id), loop)
try:
coro.result(5)
self.app.show_info(_('Channel closed'))
except Exception as e:
self.app.show_info(_('Could not close channel: ') + repr(e)) # repr because str(Exception()) == ''
def remove_channel(self):
msg = _('Are you sure you want to delete this channel? This will purge associated transactions from your wallet history.')
Question(msg, self._remove_channel).open()
def _remove_channel(self, b):
if not b:
return
self.app.wallet.lnworker.remove_channel(self.chan.channel_id)
self.app._trigger_update_history()
self.dismiss()
def force_close(self):
Question(_('Force-close channel?'), self._force_close).open()
def _force_close(self, b):
if not b:
return
if self.chan.is_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(self.chan.channel_id), loop)
try:
coro.result(1)
self.app.show_info(_('Channel closed, you may need to wait at least {} blocks, because of CSV delays'.format(self.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()) == ''
class LightningChannelsDialog(Factory.Popup):
def __init__(self, app):
super(LightningChannelsDialog, self).__init__()
self.clocks = []
self.app = app
self.can_send = ''
self.update()
def show_item(self, obj):
p = ChannelDetailsPopup(obj._chan, self.app)
p.open()
def format_fields(self, chan):
labels = {}
for subject in (REMOTE, LOCAL):
bal_minus_htlcs = chan.balance_minus_outgoing_htlcs(subject)//1000
label = self.app.format_amount(bal_minus_htlcs)
other = subject.inverted()
bal_other = chan.balance(other)//1000
bal_minus_htlcs_other = chan.balance_minus_outgoing_htlcs(other)//1000
if bal_other != bal_minus_htlcs_other:
label += ' (+' + self.app.format_amount(bal_other - bal_minus_htlcs_other) + ')'
labels[subject] = label
closed = chan.is_closed()
return [
'n/a' if closed else labels[LOCAL],
'n/a' if closed else labels[REMOTE],
]
def update_item(self, item):
chan = item._chan
item.status = self.app.wallet.lnworker.get_channel_status(chan)
item.short_channel_id = format_short_channel_id(chan.short_channel_id)
l, r = self.format_fields(chan)
item.local_balance = _('Local') + ':' + l
item.remote_balance = _('Remote') + ': ' + r
self.update_can_send()
def update(self):
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()
item.screen = self
item.active = i.node_id in lnworker.peers
item._chan = i
self.update_item(item)
channel_cards.add_widget(item)
self.update_can_send()
def update_can_send(self):
lnworker = self.app.wallet.lnworker
self.can_send = self.app.format_amount_and_units(lnworker.can_send())
self.can_receive = self.app.format_amount_and_units(lnworker.can_receive())