From 2c922057032d2a0d0144e1ecce5800b5e11ee16a Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 29 Sep 2020 00:52:27 -0300 Subject: [PATCH] async invoice listeners through webhooks: lnpay and opennode. --- lnbits/app.py | 2 +- lnbits/core/tasks.py | 6 ++++-- lnbits/wallets/lnpay.py | 27 +++++++++++++++-------- lnbits/wallets/opennode.py | 44 +++++++++++++++++++++++++++++++++++--- 4 files changed, 64 insertions(+), 15 deletions(-) diff --git a/lnbits/app.py b/lnbits/app.py index 5df599d..a7f41ea 100644 --- a/lnbits/app.py +++ b/lnbits/app.py @@ -93,7 +93,7 @@ def register_request_hooks(app: Quart): def register_async_tasks(app): from lnbits.core.tasks import invoice_listener, webhook_handler - @app.route("/wallet/webhook") + @app.route("/wallet/webhook", methods=["GET", "POST", "PUT", "PATCH", "DELETE"]) async def webhook_listener(): return await webhook_handler() diff --git a/lnbits/core/tasks.py b/lnbits/core/tasks.py index e08965b..626bb86 100644 --- a/lnbits/core/tasks.py +++ b/lnbits/core/tasks.py @@ -1,5 +1,6 @@ import asyncio -from typing import Optional, List, Awaitable, Tuple, Callable +from http import HTTPStatus +from typing import Optional, Tuple, List, Callable, Awaitable from quart import Quart, Request, g from werkzeug.datastructures import Headers @@ -52,7 +53,8 @@ def register_invoice_listener(ext_name: str, cb: Callable[[Payment], Awaitable[N async def webhook_handler(): handler = getattr(WALLET, "webhook_listener", None) if handler: - await handler() + return await handler() + return "", HTTPStatus.NO_CONTENT async def invoice_listener(app): diff --git a/lnbits/wallets/lnpay.py b/lnbits/wallets/lnpay.py index 53be5ec..4978921 100644 --- a/lnbits/wallets/lnpay.py +++ b/lnbits/wallets/lnpay.py @@ -1,6 +1,8 @@ +import json import asyncio import aiohttp from os import getenv +from http import HTTPStatus from typing import Optional, Dict, AsyncGenerator from requests import get, post from quart import request @@ -18,7 +20,6 @@ class LNPayWallet(Wallet): self.auth_invoice = getenv("LNPAY_INVOICE_KEY") self.auth_read = getenv("LNPAY_READ_KEY") self.auth_api = {"X-Api-Key": getenv("LNPAY_API_KEY")} - self.queue = asyncio.Queue() def create_invoice( self, @@ -79,18 +80,26 @@ class LNPayWallet(Wallet): return PaymentStatus(statuses[r.json()["settled"]]) async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: + self.queue: asyncio.Queue = asyncio.Queue() while True: - yield await self.queue.get() + item = await self.queue.get() + yield item self.queue.task_done() async def webhook_listener(self): - data = await request.get_json() - if "event" not in data or data["event"].get("name") != "wallet_receive": - return "" + text: str = await request.get_data() + data = json.loads(text) + if type(data) is not dict or "event" not in data or data["event"].get("name") != "wallet_receive": + return "", HTTPStatus.NO_CONTENT lntx_id = data["data"]["wtx"]["lnTx"]["id"] async with aiohttp.ClientSession() as session: - async with session.get(f"{self.endpoint}/user/lntx/{lntx_id}?fields=settled") as resp: - data = await resp.json() - if data["settled"]: - self.queue.put_nowait(lntx_id) + resp = await session.get( + f"{self.endpoint}/user/lntx/{lntx_id}?fields=settled", + headers=self.auth_api, + ) + data = await resp.json() + if data["settled"]: + self.queue.put_nowait(lntx_id) + + return "", HTTPStatus.NO_CONTENT diff --git a/lnbits/wallets/opennode.py b/lnbits/wallets/opennode.py index 49307b7..2337d8c 100644 --- a/lnbits/wallets/opennode.py +++ b/lnbits/wallets/opennode.py @@ -1,6 +1,11 @@ +import json +import asyncio +import hmac +from http import HTTPStatus from os import getenv -from typing import Optional +from typing import Optional, AsyncGenerator from requests import get, post +from quart import request, url_for from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet, Unsupported @@ -23,13 +28,18 @@ class OpenNodeWallet(Wallet): r = post( url=f"{self.endpoint}/v1/charges", headers=self.auth_invoice, - json={"amount": f"{amount}", "description": memo}, # , "private": True}, + json={ + "amount": amount, + "description": memo or "", + "callback_url": url_for("webhook_listener", _external=True), + }, ) ok, checking_id, payment_request, error_message = r.ok, None, None, None if r.ok: data = r.json()["data"] - checking_id, payment_request = data["id"], data["lightning_invoice"]["payreq"] + checking_id = data["id"] + payment_request = data["lightning_invoice"]["payreq"] else: error_message = r.json()["message"] @@ -64,3 +74,31 @@ class OpenNodeWallet(Wallet): statuses = {"initial": None, "pending": None, "confirmed": True, "error": False, "failed": False} return PaymentStatus(statuses[r.json()["data"]["status"]]) + + async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: + self.queue: asyncio.Queue = asyncio.Queue() + while True: + item = await self.queue.get() + yield item + self.queue.task_done() + + async def webhook_listener(self): + print("a request!") + text: str = await request.get_data() + print("text", text) + data = json.loads(text) + if type(data) is not dict or "event" not in data or data["event"].get("name") != "wallet_receive": + return "", HTTPStatus.NO_CONTENT + + charge_id = data["id"] + if data["status"] != "paid": + return "", HTTPStatus.NO_CONTENT + + x = hmac.new(self.auth_invoice["Authorization"], digestmod="sha256") + x.update(charge_id) + if x.hexdigest() != data["hashed_order"]: + print("invalid webhook, not from opennode") + return "", HTTPStatus.NO_CONTENT + + self.queue.put_nowait(charge_id) + return "", HTTPStatus.NO_CONTENT