feat: watch for invoice changes and push via SSE

Currently only LND is implemented.
This commit is contained in:
Stefan Stammberger 2021-07-25 18:15:26 +02:00
parent 13883d43c3
commit da5d97a550
No known key found for this signature in database
GPG key ID: 645FA807E935D9D5
6 changed files with 395 additions and 8 deletions

View file

@ -1,8 +1,354 @@
from enum import Enum
from typing import List, Optional
from pydantic import BaseModel
class InvoiceState(str, Enum):
open = "open"
settled = "settled"
canceled = "canceled"
accepted = "accepted"
def invoice_settled_from_grpc(id):
if(id == 0):
return InvoiceState.open
elif(id == 1):
return InvoiceState.settled
elif(id == 2):
return InvoiceState.canceled
elif(id == 3):
return InvoiceState.accepted
else:
raise NotImplementedError(f"InvoiceState {id} is not implemented")
class InvoiceHTLCState(str, Enum):
accepted = "accepted"
settled = "settled"
canceled = "canceled"
def invoice_settled_from_grpc(id):
if(id == 0):
return InvoiceHTLCState.accepted
elif(id == 1):
return InvoiceHTLCState.settled
elif(id == 2):
return InvoiceHTLCState.canceled
else:
raise NotImplementedError(f"InvoiceHTLCState {id} is not implemented")
class Feature(BaseModel):
name: str
is_required: bool
is_known: bool
def feature_from_grpc(f):
return Feature(
name=f.name,
is_required=f.is_required,
is_known=f.is_known,
)
class FeaturesEntry(BaseModel):
key: int
value: Feature
def features_entry_from_grpc(entry_key, feature):
return FeaturesEntry(
key=entry_key,
value=feature_from_grpc(feature),
)
class AMP(BaseModel):
# An n-of-n secret share of the root seed from
# which child payment hashes and preimages are derived.
root_share: str
# An identifier for the HTLC set that this HTLC belongs to.
set_id: str
# A nonce used to randomize the child preimage and
# child hash from a given root_share.
child_index: int
# The payment hash of the AMP HTLC.
hash: str
# The preimage used to settle this AMP htlc.
# This field will only be populated if the invoice
# is in InvoiceState_ACCEPTED or InvoiceState_SETTLED.
preimage: str
def amp_from_grpc(a) -> AMP:
return AMP(
root_share=a.root_share.hex(),
set_id=a.set_id.hex(),
child_index=a.child_index,
hash=a.hash.hex(),
preimage=a.preimage.hex(),
)
class CustomRecordsEntry(BaseModel):
key : int
value: str
def custom_record_entry_from_grpc(e) -> CustomRecordsEntry:
return CustomRecordsEntry(
key=e.key,
value=e.value,
)
class InvoiceHTLC(BaseModel):
# Short channel id over which the htlc was received.
chan_id: int
# Index identifying the htlc on the channel.
htlc_index: int
# The amount of the htlc in msat.
amt_msat: int
# Block height at which this htlc was accepted.
accept_height: int
# Time at which this htlc was accepted.
accept_time: int
# Time at which this htlc was settled or canceled.
resolve_time: int
# Block height at which this htlc expires.
expiry_height: int
# Current state the htlc is in.
state: InvoiceHTLCState
# Custom tlv records.
custom_records: List[CustomRecordsEntry]
# The total amount of the mpp payment in msat.
mpp_total_amt_msat: int
# Details relevant to AMP HTLCs, only populated
# if this is an AMP HTLC.
amp: AMP
def invoice_htlc_from_grpc(h) -> InvoiceHTLC:
def _crecords(recs):
l = []
for r in recs:
l.append(custom_record_entry_from_grpc(r))
return l
return InvoiceHTLC(
chan_id=h.chan_id,
htlc_index=h.htlc_index,
amt_msat=h.amt_msat,
accept_height=h.accept_height,
accept_time=h.accept_time,
resolve_time=h.resolve_time,
expiry_height=h.expiry_height,
state=invoice_settled_from_grpc(h.state),
custom_records=_crecords(h.custom_records),
mpp_total_amt_msat=h.mpp_total_amt_msat,
amp=amp_from_grpc(h.amp),
)
class RouteHint(BaseModel):
# The public key of the node at the start of the channel.
node_id: str
# The unique identifier of the channel.
chan_id: int
# The base fee of the channel denominated in millisatoshis.
fee_base_msat: int
# The fee rate of the channel for sending one satoshi
# across it denominated in millionths of a satoshi.
fee_proportional_millionths: int
# The time-lock delta of the channel.
cltv_expiry_delta: int
def route_hint_from_grpc(h) -> RouteHint:
return RouteHint(
node_id=h.node_id,
chan_id=h.chan_id,
fee_base_msat=h.fee_base_msat,
fee_proportional_millionths=h.fee_proportional_millionths,
cltv_expiry_delta=h.cltv_expiry_delta,
)
class Invoice(BaseModel):
r_hash: str
payment_request: str
add_index: int
payment_addr: str
# optional memo to attach along with the invoice.
# Used for record keeping purposes for the invoice's
# creator, and will also be set in the description
# field of the encoded payment request if the
# description_hash field is not being used.
memo: Optional[str]
# The hex-encoded preimage(32 byte) which will allow
# settling an incoming HTLC payable to this preimage.
r_preimage: Optional[str]
# The hash of the preimage.
r_hash: Optional[str]
# The value of this invoice in satoshis
# The fields value and value_msat are mutually exclusive.
value: Optional[int]
# The value of this invoice in millisatoshis The
# fields value and value_msat are mutually exclusive.
value_msat: Optional[int]
# Whether this invoice has been fulfilled
settled: Optional[bool]
# When this invoice was created
creation_date: Optional[int]
# When this invoice was settled
settle_date: Optional[int]
# A bare-bones invoice for a payment within the
# Lightning Network. With the details of the invoice,
# the sender has all the data necessary to send a
# payment to the recipient.
payment_request: Optional[str]
# Hash(SHA-256) of a description of the payment.
# Used if the description of payment(memo) is too
# long to naturally fit within the description field of
# an encoded payment request.
description_hash: Optional[str]
# Payment request expiry time in seconds. Default is 3600 (1 hour).
expiry: Optional[int]
# Fallback on-chain address.
fallback_addr: Optional[str]
# Delta to use for the time-lock of the CLTV extended to the final hop.
cltv_expiry : Optional[int]
# Route hints that can each be individually used
# to assist in reaching the invoice's destination.
route_hints: Optional[List[RouteHint]]
# Whether this invoice should include routing hints for private channels.
private: Optional[bool]
# The "add" index of this invoice. Each newly created invoice
# will increment this index making it monotonically increasing.
# Callers to the SubscribeInvoices call can use this to instantly
# get notified of all added invoices with an add_index greater than this one.
add_index: Optional[int]
# The "settle" index of this invoice. Each newly settled invoice will
# increment this index making it monotonically increasing. Callers to
# the SubscribeInvoices call can use this to instantly get notified of
# all settled invoices with an settle_index greater than this one.
settle_index: Optional[int]
# The amount that was accepted for this invoice, in satoshis. This
# will ONLY be set if this invoice has been settled. We provide
# this field as if the invoice was created with a zero value,
# then we need to record what amount was ultimately accepted.
# Additionally, it's possible that the sender paid MORE that
# was specified in the original invoice. So we'll record that here as well.
amt_paid_sat: Optional[int]
# The amount that was accepted for this invoice, in millisatoshis.
# This will ONLY be set if this invoice has been settled. We
# provide this field as if the invoice was created with a zero value,
# then we need to record what amount was ultimately accepted. Additionally,
# it's possible that the sender paid MORE that was specified in the
# original invoice. So we'll record that here as well.
amt_paid_msat: Optional[int]
# The state the invoice is in.
state: Optional[InvoiceState]
# List of HTLCs paying to this invoice[EXPERIMENTAL].
htlcs: Optional[List[InvoiceHTLC]]
# List of features advertised on the invoice.
features: Optional[List[FeaturesEntry]]
# Indicates if this invoice was a spontaneous payment
# that arrived via keysend[EXPERIMENTAL].
is_keysend: Optional[bool]
# The payment address of this invoice. This value will
# be used in MPP payments, and also for newer invoices
# that always require the MPP payload for added end-to-end security.
payment_addr: Optional[str]
# Signals whether or not this is an AMP invoice.
is_amp: Optional[bool]
def invoice_from_grpc(i) -> Invoice:
def _route_hints(hints):
l = []
for h in hints:
l.append(route_hint_from_grpc(h))
return l
def _htlcs(htlcs):
l = []
for h in htlcs:
l.append(invoice_htlc_from_grpc(h))
return l
def _features(features):
l = []
for k in features:
l.append(features_entry_from_grpc(k, features[k]))
return l
return Invoice(
memo=i.memo,
r_preimage=i.r_preimage.hex(),
r_hash=i.r_hash.hex(),
value=i.value,
value_msat=i.value_msat,
settled=i.settled,
creation_date=i.creation_date,
settle_date=i.settle_date,
payment_request=i.payment_request,
description_hash=i.description_hash,
expiry=i.expiry,
fallback_addr=i.fallback_addr,
cltv_expiry=i.cltv_expiry,
route_hints=_route_hints(i.route_hints),
private=i.private,
add_index=i.add_index,
settle_index=i.settle_index,
amt_paid_sat=i.amt_paid_sat,
amt_paid_msat=i.amt_paid_msat,
state=invoice_settled_from_grpc(i.state),
htlcs=_htlcs(i.htlcs),
features=_features(i.features),
is_keysend=i.is_keysend,
payment_addr=i.payment_addr.hex(),
is_amp=i.is_amp,
)

View file

@ -3,10 +3,12 @@ from app.utils import lightning_config
if lightning_config.ln_node == "lnd":
from app.repositories.ln_impl.lnd import (add_invoice_impl,
get_wallet_balance_impl)
get_wallet_balance_impl,
register_lightning_listener_impl)
else:
from app.repositories.ln_impl.clightning import (add_invoice_impl,
get_wallet_balance_impl)
from app.repositories.ln_impl.clightning import (
add_invoice_impl, get_wallet_balance_impl,
register_lightning_listener_impl)
async def get_wallet_balance():
@ -15,3 +17,7 @@ async def get_wallet_balance():
async def add_invoice(value_msat: int, memo: str = "", expiry: int = 3600, is_keysend: bool = False) -> Invoice:
return await add_invoice_impl(memo, value_msat, expiry, is_keysend)
async def register_lightning_listener():
await register_lightning_listener_impl()

View file

@ -7,3 +7,7 @@ async def get_wallet_balance_impl():
async def add_invoice_impl(value_msat: int, memo: str = "", expiry: int = 3600, is_keysend: bool = False) -> Invoice:
raise NotImplementedError("c-lightning not yet implemented")
async def register_lightning_listener_impl():
raise NotImplementedError("c-lightning not yet implemented")

View file

@ -1,6 +1,11 @@
import asyncio
from os import error
import app.repositories.ln_impl.protos.rpc_pb2 as ln
from app.models.lightning import Invoice
from app.models.lightning import Invoice, InvoiceState, invoice_from_grpc
from app.utils import SSE
from app.utils import lightning_config as lncfg
from app.utils import send_sse_message
async def get_wallet_balance_impl() -> object:
@ -24,11 +29,33 @@ async def add_invoice_impl(value_msat: int, memo: str = "", expiry: int = 3600,
response = await lncfg.lnd_stub.AddInvoice(i)
# Can't use invoice_from_grpc() here because
# the response is not a standard invoice
invoice = Invoice(
memo=memo,
expiry=expiry,
r_hash=response.r_hash.hex(),
payment_request=response.payment_request,
add_index=response.add_index,
payment_addr=response.payment_addr.hex(),
state=InvoiceState.open,
is_keysend=is_keysend,
)
return invoice
async def register_lightning_listener_impl():
loop = asyncio.get_event_loop()
t = loop.create_task(_handle_invoice_listener())
async def _handle_invoice_listener():
request = ln.InvoiceSubscription()
try:
async for r in lncfg.lnd_stub.SubscribeInvoices(request):
i = invoice_from_grpc(r)
await send_sse_message(SSE.LN_INVOICE_STATUS, i.dict())
except error:
print(error)

View file

@ -132,3 +132,5 @@ class SSE():
BTC_MEMPOOL_STATUS = "btc_mempool_status"
BTC_NEW_BLOC = "btc_new_bloc"
BTC_INFO = "btc_info"
LN_INVOICE_STATUS = "ln_invoice_status"

View file

@ -10,6 +10,7 @@ from starlette import status
from app.repositories.bitcoin import (register_bitcoin_info_gatherer,
register_bitcoin_zmq_sub)
from app.repositories.hardware_info import register_hardware_info_gatherer
from app.repositories.lightning import register_lightning_listener
from app.routers import apps, bitcoin, lightning, setup, system
from app.sse_starlette import EventSourceResponse
@ -82,3 +83,4 @@ async def register_all_handlers():
await register_bitcoin_zmq_sub()
await register_bitcoin_info_gatherer()
await register_hardware_info_gatherer()
await register_lightning_listener()