lightning: fixup after rebasing on restructured master
This commit is contained in:
123
electrum/gui/kivy/uix/dialogs/lightning_channels.py
Normal file
123
electrum/gui/kivy/uix/dialogs/lightning_channels.py
Normal file
@@ -0,0 +1,123 @@
|
||||
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
|
||||
|
||||
Builder.load_string('''
|
||||
<LightningChannelItem@CardItem>
|
||||
details: {}
|
||||
active: False
|
||||
channelId: '<channelId not set>'
|
||||
Label:
|
||||
text: root.channelId
|
||||
|
||||
<LightningChannelsDialog@Popup>:
|
||||
name: 'lightning_channels'
|
||||
BoxLayout:
|
||||
id: box
|
||||
orientation: 'vertical'
|
||||
spacing: '1dp'
|
||||
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.lnworker.subscribe_channel_list_updates_from_other_thread(self.rpc_result_handler)
|
||||
|
||||
def show_channel_details(self, obj):
|
||||
p = Factory.ChannelDetailsPopup()
|
||||
p.data = [{'keyName': key, 'value': str(obj.details[key])} for key in obj.details.keys()]
|
||||
p.open()
|
||||
|
||||
def close_channel(self, obj):
|
||||
print("UNIMPLEMENTED asked to close channel", obj.channelId) # TODO
|
||||
|
||||
def show_menu(self, obj):
|
||||
self.hide_menu()
|
||||
self.context_menu = ContextMenu(obj, [("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 rpc_result_handler(self, res):
|
||||
channel_cards = self.ids.lightning_channels_container
|
||||
channel_cards.clear_widgets()
|
||||
if "channels" in res:
|
||||
for i in res["channels"]:
|
||||
item = Factory.LightningChannelItem()
|
||||
item.screen = self
|
||||
print(i)
|
||||
item.channelId = i["chan_id"]
|
||||
item.active = i["active"]
|
||||
item.details = i
|
||||
channel_cards.add_widget(item)
|
||||
else:
|
||||
self.app.show_info(res)
|
||||
93
electrum/gui/kivy/uix/dialogs/lightning_payer.py
Normal file
93
electrum/gui/kivy/uix/dialogs/lightning_payer.py
Normal file
@@ -0,0 +1,93 @@
|
||||
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)
|
||||
116
electrum/gui/qt/channels_list.py
Normal file
116
electrum/gui/qt/channels_list.py
Normal file
@@ -0,0 +1,116 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
from PyQt5.QtWidgets import *
|
||||
|
||||
from electrum.util import inv_dict, bh2u, bfh
|
||||
from electrum.i18n import _
|
||||
from electrum.lnhtlc import HTLCStateMachine
|
||||
from .util import MyTreeWidget, SortableTreeWidgetItem, WindowModalDialog, Buttons, OkButton, CancelButton
|
||||
from .amountedit import BTCAmountEdit
|
||||
|
||||
class ChannelsList(MyTreeWidget):
|
||||
update_rows = QtCore.pyqtSignal()
|
||||
update_single_row = QtCore.pyqtSignal(HTLCStateMachine)
|
||||
|
||||
def __init__(self, parent):
|
||||
MyTreeWidget.__init__(self, parent, self.create_menu, [_('Node ID'), _('Balance'), _('Remote'), _('Status')], 0)
|
||||
self.main_window = parent
|
||||
self.update_rows.connect(self.do_update_rows)
|
||||
self.update_single_row.connect(self.do_update_single_row)
|
||||
self.status = QLabel('')
|
||||
|
||||
def format_fields(self, chan):
|
||||
status = self.parent.wallet.lnworker.channel_state[chan.channel_id]
|
||||
return [
|
||||
bh2u(chan.node_id),
|
||||
self.parent.format_amount(chan.local_state.amount_msat//1000),
|
||||
self.parent.format_amount(chan.remote_state.amount_msat//1000),
|
||||
status
|
||||
]
|
||||
|
||||
def create_menu(self, position):
|
||||
menu = QMenu()
|
||||
channel_id = self.currentItem().data(0, QtCore.Qt.UserRole)
|
||||
print('ID', bh2u(channel_id))
|
||||
def close():
|
||||
suc, msg = self.parent.wallet.lnworker.close_channel(channel_id)
|
||||
if not suc:
|
||||
print('channel close broadcast failed:', msg)
|
||||
assert suc # TODO show error message in dialog
|
||||
menu.addAction(_("Close channel"), close)
|
||||
menu.exec_(self.viewport().mapToGlobal(position))
|
||||
|
||||
@QtCore.pyqtSlot(HTLCStateMachine)
|
||||
def do_update_single_row(self, chan):
|
||||
for i in range(self.topLevelItemCount()):
|
||||
item = self.topLevelItem(i)
|
||||
if item.data(0, QtCore.Qt.UserRole) == chan.channel_id:
|
||||
for i, v in enumerate(self.format_fields(chan)):
|
||||
item.setData(i, QtCore.Qt.DisplayRole, v)
|
||||
|
||||
@QtCore.pyqtSlot()
|
||||
def do_update_rows(self):
|
||||
self.clear()
|
||||
for chan in self.parent.wallet.lnworker.channels.values():
|
||||
item = SortableTreeWidgetItem(self.format_fields(chan))
|
||||
item.setData(0, QtCore.Qt.UserRole, chan.channel_id)
|
||||
self.insertTopLevelItem(0, item)
|
||||
|
||||
def get_toolbar(self):
|
||||
b = QPushButton(_('Open Channel'))
|
||||
b.clicked.connect(self.new_channel_dialog)
|
||||
h = QHBoxLayout()
|
||||
h.addWidget(self.status)
|
||||
h.addStretch()
|
||||
h.addWidget(b)
|
||||
return h
|
||||
|
||||
def update_status(self):
|
||||
n = len(self.parent.network.lightning_nodes)
|
||||
nc = len(self.parent.network.channel_db)
|
||||
np = len(self.parent.wallet.lnworker.peers)
|
||||
self.status.setText(_('{} peers, {} nodes, {} channels').format(np, n, nc))
|
||||
|
||||
def new_channel_dialog(self):
|
||||
d = WindowModalDialog(self.parent, _('Open Channel'))
|
||||
d.setFixedWidth(700)
|
||||
vbox = QVBoxLayout(d)
|
||||
h = QGridLayout()
|
||||
local_nodeid = QLineEdit()
|
||||
local_nodeid.setText(bh2u(self.parent.wallet.lnworker.pubkey))
|
||||
local_nodeid.setReadOnly(True)
|
||||
local_nodeid.setCursorPosition(0)
|
||||
remote_nodeid = QLineEdit()
|
||||
local_amt_inp = BTCAmountEdit(self.parent.get_decimal_point)
|
||||
local_amt_inp.setAmount(200000)
|
||||
push_amt_inp = BTCAmountEdit(self.parent.get_decimal_point)
|
||||
push_amt_inp.setAmount(0)
|
||||
h.addWidget(QLabel(_('Your Node ID')), 0, 0)
|
||||
h.addWidget(local_nodeid, 0, 1)
|
||||
h.addWidget(QLabel(_('Remote Node ID')), 1, 0)
|
||||
h.addWidget(remote_nodeid, 1, 1)
|
||||
h.addWidget(QLabel('Local amount'), 2, 0)
|
||||
h.addWidget(local_amt_inp, 2, 1)
|
||||
h.addWidget(QLabel('Push amount'), 3, 0)
|
||||
h.addWidget(push_amt_inp, 3, 1)
|
||||
vbox.addLayout(h)
|
||||
vbox.addLayout(Buttons(CancelButton(d), OkButton(d)))
|
||||
if not d.exec_():
|
||||
return
|
||||
nodeid_hex = str(remote_nodeid.text())
|
||||
local_amt = local_amt_inp.get_amount()
|
||||
push_amt = push_amt_inp.get_amount()
|
||||
try:
|
||||
node_id = bfh(nodeid_hex)
|
||||
except:
|
||||
self.parent.show_error(_('Invalid node ID'))
|
||||
return
|
||||
if node_id not in self.parent.wallet.lnworker.peers and node_id not in self.parent.network.lightning_nodes:
|
||||
self.parent.show_error(_('Unknown node:') + ' ' + nodeid_hex)
|
||||
return
|
||||
assert local_amt >= 200000
|
||||
assert local_amt >= push_amt
|
||||
self.main_window.protect(self.open_channel, (node_id, local_amt, push_amt))
|
||||
|
||||
def open_channel(self, *args, **kwargs):
|
||||
self.parent.wallet.lnworker.open_channel(*args, **kwargs)
|
||||
Reference in New Issue
Block a user