commit e3409d32ef57a5b85da30a2c370f75896d16f2fe
parent 9d32031ca2ee5f69cfc5b46a6e154f1b1ec04ae4
Author: Janus <ysangkok@gmail.com>
Date: Mon, 19 Nov 2018 18:09:43 +0100
channel details with list of htlcs
Diffstat:
10 files changed, 195 insertions(+), 28 deletions(-)
diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py
@@ -294,7 +294,7 @@ class SendScreen(CScreen):
fut = asyncio.run_coroutine_threadsafe(coro, self.app.network.asyncio_loop)
fut.add_done_callback(self.ln_payment_result)
- def payment_completed_async_thread(self, event, direction, htlc, preimage):
+ def payment_completed_async_thread(self, event, date, direction, htlc, preimage, chan_id):
Clock.schedule_once(lambda dt: self.payment_completed(direction, htlc, preimage))
def payment_completed(self, direction, htlc, preimage):
diff --git a/electrum/gui/qt/channel_details.py b/electrum/gui/qt/channel_details.py
@@ -0,0 +1,141 @@
+from typing import Optional, TYPE_CHECKING
+
+import PyQt5.QtGui as QtGui
+import PyQt5.QtWidgets as QtWidgets
+import PyQt5.QtCore as QtCore
+
+from electrum.i18n import _
+from electrum.lnchan import UpdateAddHtlc
+from electrum.util import bh2u, format_time
+from electrum.lnchan import HTLCOwner
+from electrum.lnaddr import LnAddr, lndecode
+if TYPE_CHECKING:
+ from .main_window import ElectrumWindow
+
+class HTLCItem(QtGui.QStandardItem):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.setEditable(False)
+
+class ChannelDetailsDialog(QtWidgets.QDialog):
+
+ def make_inflight(self, lnaddr, i: UpdateAddHtlc):
+ it = HTLCItem(_('HTLC with ID ') + str(i.htlc_id))
+ it.appendRow([HTLCItem(_('Amount')),HTLCItem(self.format(i.amount_msat))])
+ it.appendRow([HTLCItem(_('CLTV expiry')),HTLCItem(str(i.cltv_expiry))])
+ it.appendRow([HTLCItem(_('Payment hash')),HTLCItem(bh2u(i.payment_hash))])
+ invoice = HTLCItem(_('Invoice'))
+ invoice.appendRow([HTLCItem(_('Remote node public key')), HTLCItem(bh2u(lnaddr.pubkey.serialize()))])
+ invoice.appendRow([HTLCItem(_('Amount in BTC')), HTLCItem(str(lnaddr.amount))])
+ invoice.appendRow([HTLCItem(_('Description')), HTLCItem(dict(lnaddr.tags).get('d', _('N/A')))])
+ invoice.appendRow([HTLCItem(_('Date')), HTLCItem(format_time(lnaddr.date))])
+ it.appendRow([invoice])
+ return it
+
+ def make_model(self, htlcs):
+ model = QtGui.QStandardItemModel(0, 2)
+ model.setHorizontalHeaderLabels(['HTLC', 'Property value'])
+ parentItem = model.invisibleRootItem()
+ folder_types = {'settled': _('Fulfilled HTLCs'), 'inflight': _('HTLCs in current commitment transaction')}
+ self.folders = {}
+
+ self.keyname_rows = {}
+
+ for keyname, i in folder_types.items():
+ myFont=QtGui.QFont()
+ myFont.setBold(True)
+ folder = HTLCItem(i)
+ folder.setFont(myFont)
+ parentItem.appendRow(folder)
+ self.folders[keyname] = folder
+ mapping = {}
+ num = 0
+ if keyname == 'inflight':
+ for lnaddr, i in htlcs[keyname]:
+ it = self.make_inflight(lnaddr, i)
+ self.folders[keyname].appendRow(it)
+ mapping[i.payment_hash] = num
+ num += 1
+ elif keyname == 'settled':
+ for date, direction, i, preimage in htlcs[keyname]:
+ it = HTLCItem(_('HTLC with ID ') + str(i.htlc_id))
+ it.appendRow([HTLCItem(_('Amount')),HTLCItem(self.format(i.amount_msat))])
+ it.appendRow([HTLCItem(_('CLTV expiry')),HTLCItem(str(i.cltv_expiry))])
+ it.appendRow([HTLCItem(_('Payment hash')),HTLCItem(bh2u(i.payment_hash))])
+ # NOTE no invoices because user can delete invoices after settlement
+ self.folders[keyname].appendRow(it)
+ mapping[i.payment_hash] = num
+ num += 1
+
+ self.keyname_rows[keyname] = mapping
+ return model
+
+ def move(self, fro: str, to: str, payment_hash: bytes):
+ assert fro != to
+ row_idx = self.keyname_rows[fro].pop(payment_hash)
+ row = self.folders[fro].takeRow(row_idx)
+ self.folders[to].appendRow(row)
+ dest_mapping = self.keyname_rows[to]
+ dest_mapping[payment_hash] = len(dest_mapping)
+
+ ln_payment_completed = QtCore.pyqtSignal(str, float, HTLCOwner, UpdateAddHtlc, bytes, bytes)
+ htlc_added = QtCore.pyqtSignal(str, UpdateAddHtlc, LnAddr, HTLCOwner)
+
+ @QtCore.pyqtSlot(str, UpdateAddHtlc, LnAddr, HTLCOwner)
+ def do_htlc_added(self, evtname, htlc, lnaddr, direction):
+ mapping = self.keyname_rows['inflight']
+ mapping[htlc.payment_hash] = len(mapping)
+ self.folders['inflight'].appendRow(self.make_inflight(lnaddr, htlc))
+
+ @QtCore.pyqtSlot(str, float, HTLCOwner, UpdateAddHtlc, bytes, bytes)
+ def do_ln_payment_completed(self, evtname, date, direction, htlc, preimage, chan_id):
+ self.move('inflight', 'settled', htlc.payment_hash)
+
+ def __init__(self, window: Optional['ElectrumWindow'], chan_id: bytes):
+ super().__init__(window)
+ self.window = window
+ assert type(window).__name__ in ['NoneType', 'ElectrumWindow']
+ self.ln_payment_completed.connect(self.do_ln_payment_completed)
+ self.htlc_added.connect(self.do_htlc_added)
+ if not window:
+ self.format = str
+ htlcs = {
+ 'settled':
+ [
+ ],
+ 'inflight':
+ [
+ (lndecode("lnbcrt100n1pdl9c2vpp5z6ztyjy8an80te3u6l0fxuhjzt9pfa27a27uqap3xt8nv6dq47esdqgw3jhxapncqzy3rzjq2j0zgr9slpsefhaem0rq9w3kgjx6mjfd9tp7pe8yw23jqydcdtrsqqrc5qqqqgqqqqqqqlgqqqqqqgqjq5v97p0f0ftkwzmpxhjj6magd5ars465krljcp5z28j3nxl8d0kqjkzf6acjerxdu3yvtus75kakx3yvyus6c68hdwm2hpunusr47w3gpee4hgp"), UpdateAddHtlc(amount_msat=10001, payment_hash=b"\x01"*32, cltv_expiry=500, htlc_id=1)),
+ (lndecode('lnbcrt22m1pdl9kc7pp5qw903tar0e3ar4mu4h8m3zratj0sddqhfftpsjgcx0jsekzk43dsdqqcqzy3a6ev4vh6lt62xrzlq5l23g59pv0g3tur6drnduhczqg8smqlm75nklwx8r0mm535e4x8uq6tzqw7j7tvy70qaapfnt3e9n6rltvcs7cppzmqys'), UpdateAddHtlc(amount_msat=10002, payment_hash=b"\x02"*32, cltv_expiry=501, htlc_id=2)),
+ (lndecode('lnbcrt1u1pdl9k6tpp58la47qfxz6mvtgjmnmkl8xe8vcrkhluxrldlhv3dgdlla6tr3mvqdqgw3jhxapncqzy3rzjq2j0zgr9slpsefhaem0rq9w3kgjx6mjfd9tp7pe8yw23jqydcdtrsqqrc5qqqqgqqqqqqqlgqqqqqqgqjqavsdk9qdjwgfdywhlqtuzn5atkhzt9sgjz6tfll67wc34rh80mqzjme3meqyutrj0p7tvxczeuag956h6fv0356ezstgpfgqy47d7vsq7vhx6l'), UpdateAddHtlc(amount_msat=10003, payment_hash=b"\x03"*32, cltv_expiry=502, htlc_id=3)),
+ ],
+ }
+ else:
+ htlcs = self.window.wallet.lnworker._list_invoices(chan_id)
+ self.format = lambda msat: self.window.format_amount_and_units(msat / 1000)
+ self.window.network.register_callback(self.ln_payment_completed.emit, ['ln_payment_completed'])
+ self.window.network.register_callback(self.htlc_added.emit, ['htlc_added'])
+ self.setWindowTitle(_('Channel Details'))
+ self.setMinimumSize(800, 400)
+ vbox = QtWidgets.QVBoxLayout(self)
+ w = QtWidgets.QTreeView(self)
+ w.setModel(self.make_model(htlcs))
+ #w.header().setStretchLastSection(False)
+ w.header().setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
+ vbox.addWidget(w)
+
+
+if __name__ == '__main__':
+ import sys
+ app = QtWidgets.QApplication(sys.argv)
+ d = ChannelDetailsDialog(None, b"\x01"*32)
+ d.show()
+
+ timer = QtCore.QTimer()
+ timer.setSingleShot(True)
+ def tick():
+ d.move('inflight', 'settled', b'\x02' * 32)
+ timer.timeout.connect(tick)
+ timer.start(3000)
+
+ sys.exit(app.exec_())
diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py
@@ -11,6 +11,7 @@ from electrum.lnutil import LOCAL, REMOTE, ConnStringFormatError
from .util import MyTreeWidget, SortableTreeWidgetItem, WindowModalDialog, Buttons, OkButton, CancelButton
from .amountedit import BTCAmountEdit
+from .channel_details import ChannelDetailsDialog
class ChannelsList(MyTreeWidget):
update_rows = QtCore.pyqtSignal()
@@ -62,10 +63,15 @@ class ChannelsList(MyTreeWidget):
coro = lnworker.force_close_channel(channel_id)
return network.run_from_another_thread(coro)
WaitingDialog(self, 'please wait..', task, on_success, on_failure)
+ menu.addAction(_("Details..."), lambda: self.details(channel_id))
menu.addAction(_("Close channel"), close)
menu.addAction(_("Force-close channel"), force_close)
menu.exec_(self.viewport().mapToGlobal(position))
+ def details(self, channel_id):
+ assert self.parent.wallet
+ ChannelDetailsDialog(self.parent, channel_id).show()
+
@QtCore.pyqtSlot(Channel)
def do_update_single_row(self, chan):
for i in range(self.topLevelItemCount()):
diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
@@ -396,7 +396,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.require_fee_update = True
self.history_model.on_fee_histogram()
elif event == 'ln_message':
- lnworker, message = args
+ lnworker, message, htlc_id = args
if lnworker == self.wallet.lnworker:
self.show_message(message)
else:
diff --git a/electrum/lnaddr.py b/electrum/lnaddr.py
@@ -250,7 +250,7 @@ class LnAddr(object):
def __str__(self):
return "LnAddr[{}, amount={}{} tags=[{}]]".format(
- hexlify(self.pubkey.serialize()).decode('utf-8'),
+ hexlify(self.pubkey.serialize()).decode('utf-8') if self.pubkey else None,
self.amount, self.currency,
", ".join([k + '=' + str(v) for k, v in self.tags])
)
diff --git a/electrum/lnbase.py b/electrum/lnbase.py
@@ -25,8 +25,8 @@ from .util import PrintError, bh2u, print_error, bfh, log_exceptions, list_enabl
from .transaction import Transaction, TxOutput
from .lnonion import (new_onion_packet, decode_onion_error, OnionFailureCode, calc_hops_data_for_payment,
process_onion_packet, OnionPacket, construct_onion_error, OnionRoutingFailureMessage)
-from .lnchan import Channel, RevokeAndAck, htlcsum
-from .lnutil import (Outpoint, LocalConfig,
+from .lnchan import Channel, RevokeAndAck, htlcsum, UpdateAddHtlc
+from .lnutil import (Outpoint, LocalConfig, RECEIVED,
RemoteConfig, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore,
funding_output_script, get_per_commitment_secret_from_seed,
secret_to_pubkey, LNPeerAddr, PaymentFailure, LnLocalFeatures,
@@ -896,7 +896,7 @@ class Peer(PrintError):
self.revoke(chan)
self.send_commitment(chan) # htlc will be removed
await self.receive_revoke(chan)
- self.network.trigger_callback('ln_message', self.lnworker, 'Payment failed')
+ self.network.trigger_callback('ln_message', self.lnworker, 'Payment failed', htlc_id)
async def _handle_error_code_from_failed_htlc(self, error_reason, route: List['RouteEdge'], channel_id, htlc_id):
chan = self.channels[channel_id]
@@ -969,6 +969,7 @@ class Peer(PrintError):
self.attempted_route[(chan.channel_id, htlc_id)] = route
self.print_error(f"starting payment. route: {route}")
await self.update_channel(chan, "update_add_htlc", channel_id=chan.channel_id, id=htlc_id, cltv_expiry=cltv, amount_msat=amount_msat, payment_hash=payment_hash, onion_routing_packet=onion.to_bytes())
+ return UpdateAddHtlc(**htlc, htlc_id=htlc_id)
async def receive_revoke(self, chan: Channel):
revoke_and_ack_msg = await self.revoke_and_ack[chan.channel_id].get()
@@ -1007,7 +1008,7 @@ class Peer(PrintError):
self.revoke(chan)
self.send_commitment(chan) # htlc will be removed
await self.receive_revoke(chan)
- self.network.trigger_callback('ln_message', self.lnworker, 'Payment sent')
+ self.network.trigger_callback('ln_message', self.lnworker, 'Payment sent', htlc_id)
# used in lightning-integration
self.payment_preimages[sha256(preimage)].put_nowait(preimage)
@@ -1034,7 +1035,7 @@ class Peer(PrintError):
pass # TODO fail the channel
# add htlc
htlc = {'amount_msat': amount_msat_htlc, 'payment_hash':payment_hash, 'cltv_expiry':cltv_expiry}
- chan.receive_htlc(htlc)
+ htlc_id = chan.receive_htlc(htlc)
await self.receive_commitment(chan)
self.revoke(chan)
self.send_commitment(chan)
@@ -1074,6 +1075,7 @@ class Peer(PrintError):
data=amount_msat_htlc.to_bytes(8, byteorder="big"))
await self.fail_htlc(chan, htlc_id, onion_packet, reason)
return
+ self.network.trigger_callback('htlc_added', UpdateAddHtlc(**htlc, htlc_id=htlc_id), invoice, RECEIVED)
# settle htlc
await self.settle_htlc(chan, htlc_id, preimage)
@@ -1083,7 +1085,7 @@ class Peer(PrintError):
channel_id=chan.channel_id,
id=htlc_id,
payment_preimage=preimage)
- self.network.trigger_callback('ln_message', self.lnworker, 'Payment received')
+ self.network.trigger_callback('ln_message', self.lnworker, 'Payment received', htlc_id)
async def fail_htlc(self, chan: Channel, htlc_id: int, onion_packet: OnionPacket,
reason: OnionRoutingFailureMessage):
diff --git a/electrum/lnchan.py b/electrum/lnchan.py
@@ -149,7 +149,7 @@ class Channel(PrintError):
def __init__(self, state, name = None, payment_completed : Optional[Callable[[HTLCOwner, UpdateAddHtlc, bytes], None]] = None):
self.preimages = {}
if not payment_completed:
- payment_completed = lambda x, y, z: None
+ payment_completed = lambda this, x, y, z: None
self.payment_completed = payment_completed
assert 'local_state' not in state
self.config = {}
@@ -505,7 +505,7 @@ class Channel(PrintError):
preimage = self.preimages.pop(htlc_id)
else:
preimage = None
- self.payment_completed(subject, htlc, preimage)
+ self.payment_completed(self, subject, htlc, preimage)
self.log[subject].settles.clear()
return old_amount - htlcsum(self.htlcs(subject, False))
diff --git a/electrum/lnutil.py b/electrum/lnutil.py
@@ -595,7 +595,7 @@ def extract_nodeid(connect_contents: str) -> Tuple[bytes, str]:
raise ConnStringFormatError(_('At least a hostname must be supplied after the at symbol.'))
try:
node_id = bfh(nodeid_hex)
- assert len(node_id) == 33
+ assert len(node_id) == 33, len(node_id)
except:
raise ConnStringFormatError(_('Invalid node ID, must be 33 bytes and hexadecimal'))
return node_id, rest
diff --git a/electrum/lnworker.py b/electrum/lnworker.py
@@ -12,6 +12,7 @@ import threading
import socket
import json
from datetime import datetime, timezone
+from functools import partial
import dns.resolver
import dns.exception
@@ -62,7 +63,7 @@ class LNWorker(PrintError):
def __init__(self, wallet: 'Abstract_Wallet', network: 'Network'):
self.wallet = wallet
# invoices we are currently trying to pay (might be pending HTLCs on a commitment transaction)
- self.paying = self.wallet.storage.get('lightning_payments_inflight', {}) # type: Dict[bytes, Tuple[str, Optional[int]]]
+ self.paying = self.wallet.storage.get('lightning_payments_inflight', {}) # type: Dict[bytes, Tuple[str, Optional[int], bytes]]
self.sweep_address = wallet.get_receiving_address()
self.network = network
self.channel_db = self.network.channel_db
@@ -71,9 +72,11 @@ class LNWorker(PrintError):
self.node_keypair = generate_keypair(self.ln_keystore, LnKeyFamily.NODE_KEY, 0)
self.config = network.config
self.peers = {} # type: Dict[bytes, Peer] # pubkey -> Peer
- channels_map = map(lambda x: Channel(x, payment_completed=self.payment_completed), wallet.storage.get("channels", []))
- self.channels = {x.channel_id: x for x in channels_map} # type: Dict[bytes, Channel]
- for c in self.channels.values():
+ self.channels = {} # type: Dict[bytes, Channel]
+ for x in wallet.storage.get("channels", []):
+ c = Channel(x, payment_completed=self.payment_completed)
+ self.channels[c.channel_id] = c
+
c.lnwatcher = network.lnwatcher
c.sweep_address = self.sweep_address
self.invoices = wallet.storage.get('lightning_invoices', {}) # type: Dict[str, Tuple[str,str]] # RHASH -> (preimage, invoice)
@@ -86,7 +89,8 @@ class LNWorker(PrintError):
self.network.register_callback(self.on_channel_txo, ['channel_txo'])
asyncio.run_coroutine_threadsafe(self.network.main_taskgroup.spawn(self.main_loop()), self.network.asyncio_loop)
- def payment_completed(self, direction, htlc, preimage):
+ def payment_completed(self, chan, direction, htlc, preimage):
+ chan_id = chan.channel_id
if direction == SENT:
assert htlc.payment_hash not in self.invoices
self.paying.pop(bh2u(htlc.payment_hash))
@@ -94,10 +98,11 @@ class LNWorker(PrintError):
l = self.wallet.storage.get('lightning_payments_completed', [])
if not preimage:
preimage, _addr = self.get_invoice(htlc.payment_hash)
- l.append((time.time(), direction, json.loads(encoder.encode(htlc)), bh2u(preimage)))
+ tupl = (time.time(), direction, json.loads(encoder.encode(htlc)), bh2u(preimage), bh2u(chan_id))
+ l.append(tupl)
self.wallet.storage.put('lightning_payments_completed', l)
self.wallet.storage.write()
- self.network.trigger_callback('ln_payment_completed', direction, htlc, preimage)
+ self.network.trigger_callback('ln_payment_completed', tupl[0], direction, htlc, preimage, chan_id)
def list_invoices(self):
report = self._list_invoices()
@@ -128,13 +133,16 @@ class LNWorker(PrintError):
yield str(htlc)
yield ''
- def _list_invoices(self):
+ def _list_invoices(self, chan_id=None):
invoices = dict(self.invoices)
completed = self.wallet.storage.get('lightning_payments_completed', [])
settled = []
unsettled = []
inflight = []
- for date, direction, htlc, hex_preimage in completed:
+ for date, direction, htlc, hex_preimage, hex_chan_id in completed:
+ if chan_id is not None:
+ if bfh(hex_chan_id) != chan_id:
+ continue
htlcobj = UpdateAddHtlc(*htlc)
if direction == RECEIVED:
preimage = bfh(invoices.pop(bh2u(htlcobj.payment_hash))[0])
@@ -145,18 +153,21 @@ class LNWorker(PrintError):
for preimage, pay_req in invoices.values():
addr = lndecode(pay_req, expected_hrp=constants.net.SEGWIT_HRP)
unsettled.append((addr, bfh(preimage), pay_req))
- for pay_req, amount_sat in self.paying.values():
+ for pay_req, amount_sat, this_chan_id in self.paying.values():
+ if chan_id is not None and this_chan_id != chan_id:
+ continue
addr = lndecode(pay_req, expected_hrp=constants.net.SEGWIT_HRP)
if amount_sat is not None:
addr.amount = Decimal(amount_sat) / COIN
- htlc = self.find_htlc_for_addr(addr)
+ htlc = self.find_htlc_for_addr(addr, None if chan_id is None else [chan_id])
if not htlc:
self.print_error('Warning, in flight HTLC not found in any channel')
inflight.append((addr, htlc))
return {'settled': settled, 'unsettled': unsettled, 'inflight': inflight}
- def find_htlc_for_addr(self, addr):
- for chan in self.channels.values():
+ def find_htlc_for_addr(self, addr, whitelist=None):
+ channels = [y for x,y in self.channels.items() if x in whitelist or whitelist is None]
+ for chan in channels:
for htlc in chan.log[LOCAL].adds.values():
if htlc.payment_hash == addr.paymenthash:
return htlc
@@ -362,7 +373,13 @@ class LNWorker(PrintError):
addr = self._check_invoice(invoice, amount_sat)
route = self._create_route_from_invoice(decoded_invoice=addr)
peer = self.peers[route[0].node_id]
- self.paying[bh2u(addr.paymenthash)] = (invoice, amount_sat)
+ for chan in self.channels.values():
+ if chan.short_channel_id == route[0].short_channel_id:
+ chan_id = chan.channel_id
+ break
+ else:
+ assert False, 'Found route with short channel ID we don\'t have: ' + repr(route[0].short_channel_id)
+ self.paying[bh2u(addr.paymenthash)] = (invoice, amount_sat, chan_id)
self.wallet.storage.put('lightning_payments_inflight', self.paying)
self.wallet.storage.write()
return addr, peer, self._pay_to_route(route, addr)
@@ -377,7 +394,8 @@ class LNWorker(PrintError):
else:
raise Exception("PathFinder returned path with short_channel_id {} that is not in channel list".format(bh2u(short_channel_id)))
peer = self.peers[route[0].node_id]
- return await peer.pay(route, chan, int(addr.amount * COIN * 1000), addr.paymenthash, addr.get_min_final_cltv_expiry())
+ htlc = await peer.pay(route, chan, int(addr.amount * COIN * 1000), addr.paymenthash, addr.get_min_final_cltv_expiry())
+ self.network.trigger_callback('htlc_added', htlc, addr, SENT)
@staticmethod
def _check_invoice(invoice, amount_sat=None):
diff --git a/electrum/tests/test_lnbase.py b/electrum/tests/test_lnbase.py
@@ -195,7 +195,7 @@ class TestPeer(unittest.TestCase):
def prepare_ln_message_future(w2 # receiver
):
fut = asyncio.Future()
- def evt_set(event, _lnworker, msg):
+ def evt_set(event, _lnworker, msg, _htlc_id):
fut.set_result(msg)
w2.network.register_callback(evt_set, ['ln_message'])
return fut