You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
176 lines
6.3 KiB
176 lines
6.3 KiB
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.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(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 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 |
|
id: lightning_channels_container |
|
size_hint: 1, None |
|
height: self.minimum_height |
|
spacing: '2dp' |
|
padding: '12dp' |
|
|
|
<ChannelDetailsItem@BoxLayout>: |
|
canvas.before: |
|
Color: |
|
rgba: 0.5, 0.5, 0.5, 1 |
|
Rectangle: |
|
size: self.size |
|
pos: self.pos |
|
value: '' |
|
Label: |
|
text: root.value |
|
text_size: self.size # this makes the text not overflow, but wrap |
|
|
|
<ChannelDetailsRow@BoxLayout>: |
|
keyName: '' |
|
value: '' |
|
ChannelDetailsItem: |
|
value: root.keyName |
|
size_hint_x: 0.5 # this makes the column narrower |
|
|
|
# see https://blog.kivy.org/2014/07/wrapping-text-in-kivys-label/ |
|
ScrollView: |
|
Label: |
|
text: root.value |
|
size_hint_y: None |
|
text_size: self.width, None |
|
height: self.texture_size[1] |
|
|
|
<ChannelDetailsList@RecycleView>: |
|
scroll_type: ['bars', 'content'] |
|
scroll_wheel_distance: dp(114) |
|
bar_width: dp(10) |
|
viewclass: 'ChannelDetailsRow' |
|
RecycleBoxLayout: |
|
default_size: None, dp(56) |
|
default_size_hint: 1, None |
|
size_hint_y: None |
|
height: self.minimum_height |
|
orientation: 'vertical' |
|
spacing: dp(2) |
|
|
|
<ChannelDetailsPopup@Popup>: |
|
id: popuproot |
|
data: [] |
|
ChannelDetailsList: |
|
data: popuproot.data |
|
''') |
|
|
|
class ChannelDetailsPopup(Popup): |
|
def __init__(self, data, **kwargs): |
|
super(ChanenlDetailsPopup,self).__init__(**kwargs) |
|
self.data = data |
|
|
|
class LightningChannelsDialog(Factory.Popup): |
|
def __init__(self, app): |
|
super(LightningChannelsDialog, self).__init__() |
|
self.clocks = [] |
|
self.app = app |
|
self.context_menu = None |
|
self.app.wallet.network.register_callback(self.channels_update, ['channels']) |
|
self.channels_update('bogus evt') |
|
|
|
def show_channel_details(self, obj): |
|
p = Factory.ChannelDetailsPopup() |
|
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() |
|
|
|
def close_channel(self, obj): |
|
loop = self.app.wallet.network.asyncio_loop |
|
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')) |
|
except Exception as e: |
|
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 {} 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()) == '' |
|
|
|
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)]) |
|
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 |
|
|
|
def presentable_chan_id(self, i): |
|
return bh2u(i.short_channel_id) if i.short_channel_id else bh2u(i.channel_id)[:16] |
|
|
|
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() |
|
item.screen = self |
|
item.channelId = self.presentable_chan_id(i) |
|
item.active = i.node_id in lnworker.peers |
|
item.details = self.channel_details(i) |
|
item._chan = i |
|
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(), |
|
_('Initiator'): 'Opened/funded by us' if chan.constraints.is_initiator else 'Opened/funded by remote party', |
|
_('Current feerate'): chan.constraints.feerate}
|
|
|