From ffdfe9fed68c554cd5c0d234d11afd232672517d Mon Sep 17 00:00:00 2001 From: Stefan Stammberger Date: Mon, 20 Sep 2021 17:00:36 +0200 Subject: [PATCH] feat: implement decode pay req endpoint closes #16 --- app/models/lightning.py | 84 ++++++++++++++++++-------- app/repositories/lightning.py | 14 ++++- app/repositories/ln_impl/clightning.py | 6 +- app/repositories/ln_impl/lnd.py | 20 ++++++ app/routers/lightning.py | 18 +++++- 5 files changed, 114 insertions(+), 28 deletions(-) diff --git a/app/models/lightning.py b/app/models/lightning.py index 2d1e274..b17c3f4 100644 --- a/app/models/lightning.py +++ b/app/models/lightning.py @@ -48,25 +48,25 @@ class Feature(BaseModel): 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, - ) + @classmethod + def from_grpc(cls, f): + return cls( + 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), - ) + @classmethod + def from_grpc(cls, entry_key, feature): + return cls( + key=entry_key, + value=Feature.from_grpc(feature), + ) class AMP(BaseModel): @@ -187,15 +187,15 @@ class RouteHint(BaseModel): # 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, - ) + @classmethod + def from_grpc(cls, h) -> "RouteHint": + return cls( + 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): @@ -311,7 +311,7 @@ def invoice_from_grpc(i) -> Invoice: def _route_hints(hints): l = [] for h in hints: - l.append(route_hint_from_grpc(h)) + l.append(RouteHint.from_grpc((h))) return l def _htlcs(htlcs): @@ -323,7 +323,7 @@ def invoice_from_grpc(i) -> Invoice: def _features(features): l = [] for k in features: - l.append(features_entry_from_grpc(k, features[k])) + l.append(FeaturesEntry.from_grpc(k, features[k])) return l return Invoice( @@ -833,7 +833,7 @@ def ln_info_from_grpc(i) -> LnInfo: _features = [] for f in i.features: - _features.append(features_entry_from_grpc(f, i.features[f])) + _features.append(FeaturesEntry.from_grpc(f, i.features[f])) _uris = [u for u in i.uris] @@ -935,3 +935,37 @@ class WalletBalance(BaseModel): channel_pending_open_local_balance=channel.pending_open_local_balance.msat, channel_pending_open_remote_balance=channel.pending_open_remote_balance.msat, ) + + +class PaymentRequest(BaseModel): + destination: str + payment_hash: str + num_satoshis: int + timestamp: int + expiry: int + description: str + description_hash: str + fallback_addr: Optional[str] + cltv_expiry: int + route_hints: List[RouteHint] = Query([]) + payment_addr: str = Query(..., description="The payment address in hex format") + num_msat: int + features: List[FeaturesEntry] = Query([]) + + @classmethod + def from_grpc(cls, r): + return cls( + destination=r.destination, + payment_hash=r.payment_hash, + num_satoshis=r.num_satoshis, + timestamp=r.timestamp, + expiry=r.expiry, + description=r.description, + description_hash=r.description_hash, + fallback_addr=r.fallback_addr, + cltv_expiry=r.cltv_expiry, + route_hints=[RouteHint.from_grpc(rh) for rh in r.route_hints], + payment_addr=r.payment_addr.hex(), + num_msat=r.num_msat, + features=[FeaturesEntry.from_grpc(k, r.features[k]) for k in r.features], + ) diff --git a/app/repositories/lightning.py b/app/repositories/lightning.py index 5aae485..61d237b 100644 --- a/app/repositories/lightning.py +++ b/app/repositories/lightning.py @@ -1,12 +1,19 @@ import asyncio -from app.models.lightning import Invoice, LightningStatus, LnInfo, Payment +from app.models.lightning import ( + Invoice, + LightningStatus, + LnInfo, + Payment, + PaymentRequest, +) from app.utils import SSE, lightning_config, send_sse_message from decouple import config if lightning_config.ln_node == "lnd": from app.repositories.ln_impl.lnd import ( add_invoice_impl, + decode_pay_request_impl, get_implementation_name, get_ln_info_impl, get_wallet_balance_impl, @@ -16,6 +23,7 @@ if lightning_config.ln_node == "lnd": else: from app.repositories.ln_impl.clightning import ( add_invoice_impl, + decode_pay_request_impl, get_implementation_name, get_ln_info_impl, get_wallet_balance_impl, @@ -44,6 +52,10 @@ async def add_invoice( return await add_invoice_impl(memo, value_msat, expiry, is_keysend) +async def decode_pay_request(pay_req: str) -> PaymentRequest: + return await decode_pay_request_impl(pay_req) + + async def send_payment( pay_req: str, timeout_seconds: int, fee_limit_msat: int ) -> Payment: diff --git a/app/repositories/ln_impl/clightning.py b/app/repositories/ln_impl/clightning.py index a443ae7..6b317bb 100644 --- a/app/repositories/ln_impl/clightning.py +++ b/app/repositories/ln_impl/clightning.py @@ -1,4 +1,4 @@ -from app.models.lightning import Invoice, LnInfo, Payment +from app.models.lightning import Invoice, LnInfo, Payment, PaymentRequest def get_implementation_name() -> str: @@ -15,6 +15,10 @@ async def add_invoice_impl( raise NotImplementedError("c-lightning not yet implemented") +async def decode_pay_request_impl(pay_req: str) -> PaymentRequest: + raise NotImplementedError("c-lightning not yet implemented") + + async def send_payment_impl( pay_req: str, timeout_seconds: int, fee_limit_msat: int ) -> Payment: diff --git a/app/repositories/ln_impl/lnd.py b/app/repositories/ln_impl/lnd.py index 11d53ab..5db5848 100644 --- a/app/repositories/ln_impl/lnd.py +++ b/app/repositories/ln_impl/lnd.py @@ -9,6 +9,7 @@ from app.models.lightning import ( InvoiceState, LnInfo, Payment, + PaymentRequest, WalletBalance, invoice_from_grpc, ln_info_from_grpc, @@ -62,6 +63,25 @@ async def add_invoice_impl( return invoice +async def decode_pay_request_impl(pay_req: str) -> PaymentRequest: + try: + req = ln.PayReqString(pay_req=pay_req) + res = await lncfg.lnd_stub.DecodePayReq(req) + return PaymentRequest.from_grpc(res) + except grpc.aio._call.AioRpcError as error: + if ( + error.details() != None + and error.details().find("checksum failed.") > -1 + ): + raise HTTPException( + status.HTTP_400_BAD_REQUEST, detail="Invalid payment request string" + ) + else: + raise HTTPException( + status.HTTP_500_INTERNAL_SERVER_ERROR, detail=error.details() + ) + + async def send_payment_impl( pay_req: str, timeout_seconds: int, fee_limit_msat: int ) -> Payment: diff --git a/app/routers/lightning.py b/app/routers/lightning.py index 03972db..6f095f6 100644 --- a/app/routers/lightning.py +++ b/app/routers/lightning.py @@ -4,17 +4,19 @@ from app.models.lightning import ( LightningStatus, LnInfo, Payment, + PaymentRequest, WalletBalance, ) from app.repositories.lightning import ( add_invoice, + decode_pay_request, get_ln_info, get_ln_status, get_wallet_balance, send_payment, ) from app.routers.lightning_docs import send_payment_desc -from fastapi import APIRouter, HTTPException, status +from fastapi import APIRouter, HTTPException, Query, status from fastapi.params import Depends router = APIRouter(prefix="/lightning", tags=["Lightning"]) @@ -107,3 +109,17 @@ async def get_info(): raise HTTPException(r.status_code, detail=r.detail) except NotImplementedError as r: raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED, detail=r.args[0]) + + +@router.get( + "/decode-pay-req", + name="lightning.decode-pay-req", + summary="DecodePayReq takes an encoded payment request string and attempts to decode it, returning a full description of the conditions encoded within the payment request.", + response_model=PaymentRequest, + response_description="A fully decoded payment request or a HTTP status 400 if the payment request cannot be decoded.", + dependencies=[Depends(JWTBearer())], +) +async def get_decode_pay_request( + pay_req: str = Query(..., description="The payment request string to be decoded") +): + return await decode_pay_request(pay_req)