tadd command for listing invoices and their progress, fix list_channels - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit 14256286047a015dc5124d99d13bbbe3c605f9b7 DIR parent 783cac1f2357810cff780a90ee5f841e7d0c6823 HTML Author: Janus <ysangkok@gmail.com> Date: Wed, 7 Nov 2018 17:44:49 +0100 add command for listing invoices and their progress, fix list_channels Diffstat: M electrum/commands.py | 4 ++++ M electrum/lnbase.py | 4 ++-- M electrum/lnchan.py | 18 +++++++++++++++--- M electrum/lnworker.py | 94 ++++++++++++++++++++++++++++--- 4 files changed, 108 insertions(+), 12 deletions(-) --- DIR diff --git a/electrum/commands.py b/electrum/commands.py t@@ -804,6 +804,10 @@ class Commands: def clear_ln_blacklist(self): self.network.path_finder.blacklist.clear() + @command('w') + def listinvoices(self): + return "\n".join(self.wallet.lnworker.list_invoices()) + def eval_bool(x: str) -> bool: if x == 'false': return False if x == 'true': return True DIR diff --git a/electrum/lnbase.py b/electrum/lnbase.py t@@ -499,7 +499,7 @@ class Peer(PrintError): "constraints": ChannelConstraints(capacity=funding_sat, is_initiator=True, funding_txn_minimum_depth=funding_txn_minimum_depth, feerate=feerate), "remote_commitment_to_be_revoked": None, } - chan = Channel(chan_dict) + chan = Channel(chan_dict, payment_completed=self.lnworker.payment_completed) chan.lnwatcher = self.lnwatcher chan.sweep_address = self.lnworker.sweep_address sig_64, _ = chan.sign_next_commitment() t@@ -597,7 +597,7 @@ class Peer(PrintError): "constraints": ChannelConstraints(capacity=funding_sat, is_initiator=False, funding_txn_minimum_depth=min_depth, feerate=feerate), "remote_commitment_to_be_revoked": None, } - chan = Channel(chan_dict) + chan = Channel(chan_dict, payment_completed=self.lnworker.payment_completed) chan.lnwatcher = self.lnwatcher chan.sweep_address = self.lnworker.sweep_address remote_sig = funding_created['signature'] DIR diff --git a/electrum/lnchan.py b/electrum/lnchan.py t@@ -26,7 +26,7 @@ from collections import namedtuple, defaultdict import binascii import json from enum import Enum, auto -from typing import Optional, Dict, List, Tuple, NamedTuple, Set +from typing import Optional, Dict, List, Tuple, NamedTuple, Set, Callable, Iterable from copy import deepcopy from .util import bfh, PrintError, bh2u t@@ -52,7 +52,9 @@ class ChannelJsonEncoder(json.JSONEncoder): return binascii.hexlify(o).decode("ascii") if isinstance(o, RevocationStore): return o.serialize() - return super(ChannelJsonEncoder, self) + if isinstance(o, set): + return list(o) + return super().default(o) RevokeAndAck = namedtuple("RevokeAndAck", ["per_commitment_secret", "next_per_commitment_point"]) t@@ -144,7 +146,11 @@ class Channel(PrintError): except: return super().diagnostic_name() - def __init__(self, state, name = None): + 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: None + self.payment_completed = payment_completed assert 'local_state' not in state self.config = {} self.config[LOCAL] = state["local_config"] t@@ -495,6 +501,11 @@ class Channel(PrintError): adds = self.log[subject].adds htlc = adds.pop(htlc_id) self.settled[subject].append(htlc.amount_msat) + if subject == LOCAL: + preimage = self.preimages.pop(htlc_id) + else: + preimage = None + self.payment_completed(subject, htlc, preimage) self.log[subject].settles.clear() return old_amount - htlcsum(self.htlcs(subject, False)) t@@ -647,6 +658,7 @@ class Channel(PrintError): htlc = log.adds[htlc_id] assert htlc.payment_hash == sha256(preimage) assert htlc_id not in log.settles + self.preimages[htlc_id] = preimage log.settles.add(htlc_id) # we don't save the preimage because we don't need to forward it anyway DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py t@@ -11,7 +11,7 @@ from typing import Optional, Sequence, Tuple, List, Dict, TYPE_CHECKING import threading import socket import json -from decimal import Decimal +from datetime import datetime, timezone import dns.resolver import dns.exception t@@ -27,13 +27,13 @@ from .lntransport import LNResponderTransport from .lnbase import Peer from .lnaddr import lnencode, LnAddr, lndecode from .ecc import der_sig_from_sig_string -from .lnchan import Channel, ChannelJsonEncoder +from .lnchan import Channel, ChannelJsonEncoder, UpdateAddHtlc from .lnutil import (Outpoint, calc_short_channel_id, LNPeerAddr, get_compressed_pubkey_from_bech32, extract_nodeid, PaymentFailure, split_host_port, ConnStringFormatError, generate_keypair, LnKeyFamily, LOCAL, REMOTE, UnknownPaymentHash, MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE, - NUM_MAX_EDGES_IN_PAYMENT_PATH) + NUM_MAX_EDGES_IN_PAYMENT_PATH, SENT, RECEIVED, HTLCOwner) from .i18n import _ from .lnrouter import RouteEdge, is_route_sane_to_use from .address_synchronizer import TX_HEIGHT_LOCAL t@@ -55,10 +55,14 @@ FALLBACK_NODE_LIST_MAINNET = ( LNPeerAddr('13.80.67.162', 9735, bfh('02c0ac82c33971de096d87ce5ed9b022c2de678f08002dc37fdb1b6886d12234b5')), # Stampery ) +encoder = ChannelJsonEncoder() + 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.sweep_address = wallet.get_receiving_address() self.network = network self.channel_db = self.network.channel_db t@@ -67,7 +71,8 @@ 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 - self.channels = {x.channel_id: x for x in map(Channel, wallet.storage.get("channels", []))} # type: Dict[bytes, Channel] + 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(): c.lnwatcher = network.lnwatcher c.sweep_address = self.sweep_address t@@ -81,6 +86,79 @@ 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): + if direction == SENT: + assert htlc.payment_hash not in self.invoices + self.paying.pop(bh2u(htlc.payment_hash)) + self.wallet.storage.put('lightning_payments_inflight', self.paying) + 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))) + self.wallet.storage.put('lightning_payments_completed', l) + self.wallet.storage.write() + + def list_invoices(self): + report = self._list_invoices() + if report['settled']: + yield 'Settled invoices:' + yield '-----------------' + for date, direction, htlc, preimage in sorted(report['settled']): + # astimezone converts to local time + # replace removes the tz info since we don't need to display it + yield 'Paid at: ' + date.astimezone().replace(tzinfo=None).isoformat(sep=' ', timespec='minutes') + yield 'We paid' if direction == SENT else 'They paid' + yield str(htlc) + yield 'Preimage: ' + (bh2u(preimage) if preimage else 'Not available') # if delete_invoice was called + yield '' + if report['unsettled']: + yield 'Your unsettled invoices:' + yield '------------------------' + for addr, preimage in report['unsettled']: + yield str(addr) + yield 'Preimage: ' + bh2u(preimage) + yield '' + if report['inflight']: + yield 'Outgoing payments in progress:' + yield '------------------------------' + for addr, htlc in report['inflight']: + yield str(addr) + yield str(htlc) + yield '' + + def _list_invoices(self): + invoices = dict(self.invoices) + completed = self.wallet.storage.get('lightning_payments_completed', []) + settled = [] + unsettled = [] + inflight = [] + for date, direction, htlc, hex_preimage in completed: + htlcobj = UpdateAddHtlc(*htlc) + if direction == RECEIVED: + preimage = bfh(invoices.pop(bh2u(htlcobj.payment_hash))[0]) + else: + preimage = bfh(hex_preimage) + # FIXME use fromisoformat when minimum Python is 3.7 + settled.append((datetime.fromtimestamp(date, timezone.utc), HTLCOwner(direction), htlcobj, preimage)) + for preimage, pay_req in invoices.values(): + addr = lndecode(pay_req, expected_hrp=constants.net.SEGWIT_HRP) + unsettled.append((addr, bfh(preimage))) + for pay_req, amount_sat in self.paying.values(): + 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) + 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(): + for htlc in chan.log[LOCAL].adds.values(): + if htlc.payment_hash == addr.paymenthash: + return htlc + def _read_ln_keystore(self) -> BIP32_KeyStore: xprv = self.wallet.storage.get('lightning_privkey2') if xprv is None: t@@ -280,6 +358,9 @@ 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) + self.wallet.storage.put('lightning_payments_inflight', self.paying) + self.wallet.storage.write() return addr, peer, self._pay_to_route(route, addr) async def _pay_to_route(self, route, addr): t@@ -437,13 +518,12 @@ class LNWorker(PrintError): self.wallet.storage.write() def list_channels(self): - encoder = ChannelJsonEncoder() with self.lock: # we output the funding_outpoint instead of the channel_id because lnd uses channel_point (funding outpoint) to identify channels for channel_id, chan in self.channels.items(): yield { - 'local_htlcs': json.loads(encoder.encode(chan.log[LOCAL ])), - 'remote_htlcs': json.loads(encoder.encode(chan.log[REMOTE])), + 'local_htlcs': json.loads(encoder.encode(chan.log[LOCAL ]._asdict())), + 'remote_htlcs': json.loads(encoder.encode(chan.log[REMOTE]._asdict())), 'channel_id': bh2u(chan.short_channel_id), 'channel_point': chan.funding_outpoint.to_str(), 'state': chan.get_state(),