URI: 
       tchannel details with list of htlcs - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit e3409d32ef57a5b85da30a2c370f75896d16f2fe
   DIR parent 9d32031ca2ee5f69cfc5b46a6e154f1b1ec04ae4
  HTML Author: Janus <ysangkok@gmail.com>
       Date:   Mon, 19 Nov 2018 18:09:43 +0100
       
       channel details with list of htlcs
       
       Diffstat:
         M electrum/gui/kivy/uix/screens.py    |       2 +-
         A electrum/gui/qt/channel_details.py  |     141 +++++++++++++++++++++++++++++++
         M electrum/gui/qt/channels_list.py    |       6 ++++++
         M electrum/gui/qt/main_window.py      |       2 +-
         M electrum/lnaddr.py                  |       2 +-
         M electrum/lnbase.py                  |      14 ++++++++------
         M electrum/lnchan.py                  |       4 ++--
         M electrum/lnutil.py                  |       2 +-
         M electrum/lnworker.py                |      48 +++++++++++++++++++++----------
         M electrum/tests/test_lnbase.py       |       2 +-
       
       10 files changed, 195 insertions(+), 28 deletions(-)
       ---
   DIR diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py
       t@@ -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):
   DIR diff --git a/electrum/gui/qt/channel_details.py b/electrum/gui/qt/channel_details.py
       t@@ -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_())
   DIR diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py
       t@@ -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()
       t@@ -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()):
   DIR diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
       t@@ -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:
   DIR diff --git a/electrum/lnaddr.py b/electrum/lnaddr.py
       t@@ -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])
                )
   DIR diff --git a/electrum/lnbase.py b/electrum/lnbase.py
       t@@ -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,
       t@@ -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]
       t@@ -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()
       t@@ -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)
       t@@ -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)
       t@@ -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)
        
       t@@ -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):
   DIR diff --git a/electrum/lnchan.py b/electrum/lnchan.py
       t@@ -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 = {}
       t@@ -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))
   DIR diff --git a/electrum/lnutil.py b/electrum/lnutil.py
       t@@ -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
   DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py
       t@@ -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
       t@@ -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
       t@@ -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)
       t@@ -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))
       t@@ -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()
       t@@ -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])
       t@@ -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
       t@@ -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)
       t@@ -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):
   DIR diff --git a/electrum/tests/test_lnbase.py b/electrum/tests/test_lnbase.py
       t@@ -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