Browse Source

add command for listing invoices and their progress, fix list_channels

dependabot/pip/contrib/deterministic-build/ecdsa-0.13.3
Janus 6 years ago
committed by ThomasV
parent
commit
1425628604
  1. 4
      electrum/commands.py
  2. 4
      electrum/lnbase.py
  3. 18
      electrum/lnchan.py
  4. 94
      electrum/lnworker.py

4
electrum/commands.py

@ -804,6 +804,10 @@ class Commands:
def clear_ln_blacklist(self): def clear_ln_blacklist(self):
self.network.path_finder.blacklist.clear() 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: def eval_bool(x: str) -> bool:
if x == 'false': return False if x == 'false': return False
if x == 'true': return True if x == 'true': return True

4
electrum/lnbase.py

@ -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), "constraints": ChannelConstraints(capacity=funding_sat, is_initiator=True, funding_txn_minimum_depth=funding_txn_minimum_depth, feerate=feerate),
"remote_commitment_to_be_revoked": None, "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.lnwatcher = self.lnwatcher
chan.sweep_address = self.lnworker.sweep_address chan.sweep_address = self.lnworker.sweep_address
sig_64, _ = chan.sign_next_commitment() sig_64, _ = chan.sign_next_commitment()
@ -597,7 +597,7 @@ class Peer(PrintError):
"constraints": ChannelConstraints(capacity=funding_sat, is_initiator=False, funding_txn_minimum_depth=min_depth, feerate=feerate), "constraints": ChannelConstraints(capacity=funding_sat, is_initiator=False, funding_txn_minimum_depth=min_depth, feerate=feerate),
"remote_commitment_to_be_revoked": None, "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.lnwatcher = self.lnwatcher
chan.sweep_address = self.lnworker.sweep_address chan.sweep_address = self.lnworker.sweep_address
remote_sig = funding_created['signature'] remote_sig = funding_created['signature']

18
electrum/lnchan.py

@ -26,7 +26,7 @@ from collections import namedtuple, defaultdict
import binascii import binascii
import json import json
from enum import Enum, auto 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 copy import deepcopy
from .util import bfh, PrintError, bh2u from .util import bfh, PrintError, bh2u
@ -52,7 +52,9 @@ class ChannelJsonEncoder(json.JSONEncoder):
return binascii.hexlify(o).decode("ascii") return binascii.hexlify(o).decode("ascii")
if isinstance(o, RevocationStore): if isinstance(o, RevocationStore):
return o.serialize() 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"]) RevokeAndAck = namedtuple("RevokeAndAck", ["per_commitment_secret", "next_per_commitment_point"])
@ -144,7 +146,11 @@ class Channel(PrintError):
except: except:
return super().diagnostic_name() 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 assert 'local_state' not in state
self.config = {} self.config = {}
self.config[LOCAL] = state["local_config"] self.config[LOCAL] = state["local_config"]
@ -495,6 +501,11 @@ class Channel(PrintError):
adds = self.log[subject].adds adds = self.log[subject].adds
htlc = adds.pop(htlc_id) htlc = adds.pop(htlc_id)
self.settled[subject].append(htlc.amount_msat) 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() self.log[subject].settles.clear()
return old_amount - htlcsum(self.htlcs(subject, False)) return old_amount - htlcsum(self.htlcs(subject, False))
@ -647,6 +658,7 @@ class Channel(PrintError):
htlc = log.adds[htlc_id] htlc = log.adds[htlc_id]
assert htlc.payment_hash == sha256(preimage) assert htlc.payment_hash == sha256(preimage)
assert htlc_id not in log.settles assert htlc_id not in log.settles
self.preimages[htlc_id] = preimage
log.settles.add(htlc_id) log.settles.add(htlc_id)
# we don't save the preimage because we don't need to forward it anyway # we don't save the preimage because we don't need to forward it anyway

94
electrum/lnworker.py

@ -11,7 +11,7 @@ from typing import Optional, Sequence, Tuple, List, Dict, TYPE_CHECKING
import threading import threading
import socket import socket
import json import json
from decimal import Decimal from datetime import datetime, timezone
import dns.resolver import dns.resolver
import dns.exception import dns.exception
@ -27,13 +27,13 @@ from .lntransport import LNResponderTransport
from .lnbase import Peer from .lnbase import Peer
from .lnaddr import lnencode, LnAddr, lndecode from .lnaddr import lnencode, LnAddr, lndecode
from .ecc import der_sig_from_sig_string 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, from .lnutil import (Outpoint, calc_short_channel_id, LNPeerAddr,
get_compressed_pubkey_from_bech32, extract_nodeid, get_compressed_pubkey_from_bech32, extract_nodeid,
PaymentFailure, split_host_port, ConnStringFormatError, PaymentFailure, split_host_port, ConnStringFormatError,
generate_keypair, LnKeyFamily, LOCAL, REMOTE, generate_keypair, LnKeyFamily, LOCAL, REMOTE,
UnknownPaymentHash, MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE, 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 .i18n import _
from .lnrouter import RouteEdge, is_route_sane_to_use from .lnrouter import RouteEdge, is_route_sane_to_use
from .address_synchronizer import TX_HEIGHT_LOCAL from .address_synchronizer import TX_HEIGHT_LOCAL
@ -55,10 +55,14 @@ FALLBACK_NODE_LIST_MAINNET = (
LNPeerAddr('13.80.67.162', 9735, bfh('02c0ac82c33971de096d87ce5ed9b022c2de678f08002dc37fdb1b6886d12234b5')), # Stampery LNPeerAddr('13.80.67.162', 9735, bfh('02c0ac82c33971de096d87ce5ed9b022c2de678f08002dc37fdb1b6886d12234b5')), # Stampery
) )
encoder = ChannelJsonEncoder()
class LNWorker(PrintError): class LNWorker(PrintError):
def __init__(self, wallet: 'Abstract_Wallet', network: 'Network'): def __init__(self, wallet: 'Abstract_Wallet', network: 'Network'):
self.wallet = wallet 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.sweep_address = wallet.get_receiving_address()
self.network = network self.network = network
self.channel_db = self.network.channel_db self.channel_db = self.network.channel_db
@ -67,7 +71,8 @@ class LNWorker(PrintError):
self.node_keypair = generate_keypair(self.ln_keystore, LnKeyFamily.NODE_KEY, 0) self.node_keypair = generate_keypair(self.ln_keystore, LnKeyFamily.NODE_KEY, 0)
self.config = network.config self.config = network.config
self.peers = {} # type: Dict[bytes, Peer] # pubkey -> Peer 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(): for c in self.channels.values():
c.lnwatcher = network.lnwatcher c.lnwatcher = network.lnwatcher
c.sweep_address = self.sweep_address c.sweep_address = self.sweep_address
@ -81,6 +86,79 @@ class LNWorker(PrintError):
self.network.register_callback(self.on_channel_txo, ['channel_txo']) 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) 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: def _read_ln_keystore(self) -> BIP32_KeyStore:
xprv = self.wallet.storage.get('lightning_privkey2') xprv = self.wallet.storage.get('lightning_privkey2')
if xprv is None: if xprv is None:
@ -280,6 +358,9 @@ class LNWorker(PrintError):
addr = self._check_invoice(invoice, amount_sat) addr = self._check_invoice(invoice, amount_sat)
route = self._create_route_from_invoice(decoded_invoice=addr) route = self._create_route_from_invoice(decoded_invoice=addr)
peer = self.peers[route[0].node_id] 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) return addr, peer, self._pay_to_route(route, addr)
async def _pay_to_route(self, route, addr): async def _pay_to_route(self, route, addr):
@ -437,13 +518,12 @@ class LNWorker(PrintError):
self.wallet.storage.write() self.wallet.storage.write()
def list_channels(self): def list_channels(self):
encoder = ChannelJsonEncoder()
with self.lock: with self.lock:
# we output the funding_outpoint instead of the channel_id because lnd uses channel_point (funding outpoint) to identify channels # 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(): for channel_id, chan in self.channels.items():
yield { yield {
'local_htlcs': json.loads(encoder.encode(chan.log[LOCAL ])), 'local_htlcs': json.loads(encoder.encode(chan.log[LOCAL ]._asdict())),
'remote_htlcs': json.loads(encoder.encode(chan.log[REMOTE])), 'remote_htlcs': json.loads(encoder.encode(chan.log[REMOTE]._asdict())),
'channel_id': bh2u(chan.short_channel_id), 'channel_id': bh2u(chan.short_channel_id),
'channel_point': chan.funding_outpoint.to_str(), 'channel_point': chan.funding_outpoint.to_str(),
'state': chan.get_state(), 'state': chan.get_state(),

Loading…
Cancel
Save