Browse Source

async invoice listeners through webhooks: lnpay and opennode.

atmext
fiatjaf 4 years ago
parent
commit
2c92205703
  1. 2
      lnbits/app.py
  2. 6
      lnbits/core/tasks.py
  3. 21
      lnbits/wallets/lnpay.py
  4. 44
      lnbits/wallets/opennode.py

2
lnbits/app.py

@ -93,7 +93,7 @@ def register_request_hooks(app: Quart):
def register_async_tasks(app): def register_async_tasks(app):
from lnbits.core.tasks import invoice_listener, webhook_handler 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(): async def webhook_listener():
return await webhook_handler() return await webhook_handler()

6
lnbits/core/tasks.py

@ -1,5 +1,6 @@
import asyncio 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 quart import Quart, Request, g
from werkzeug.datastructures import Headers 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(): async def webhook_handler():
handler = getattr(WALLET, "webhook_listener", None) handler = getattr(WALLET, "webhook_listener", None)
if handler: if handler:
await handler() return await handler()
return "", HTTPStatus.NO_CONTENT
async def invoice_listener(app): async def invoice_listener(app):

21
lnbits/wallets/lnpay.py

@ -1,6 +1,8 @@
import json
import asyncio import asyncio
import aiohttp import aiohttp
from os import getenv from os import getenv
from http import HTTPStatus
from typing import Optional, Dict, AsyncGenerator from typing import Optional, Dict, AsyncGenerator
from requests import get, post from requests import get, post
from quart import request from quart import request
@ -18,7 +20,6 @@ class LNPayWallet(Wallet):
self.auth_invoice = getenv("LNPAY_INVOICE_KEY") self.auth_invoice = getenv("LNPAY_INVOICE_KEY")
self.auth_read = getenv("LNPAY_READ_KEY") self.auth_read = getenv("LNPAY_READ_KEY")
self.auth_api = {"X-Api-Key": getenv("LNPAY_API_KEY")} self.auth_api = {"X-Api-Key": getenv("LNPAY_API_KEY")}
self.queue = asyncio.Queue()
def create_invoice( def create_invoice(
self, self,
@ -79,18 +80,26 @@ class LNPayWallet(Wallet):
return PaymentStatus(statuses[r.json()["settled"]]) return PaymentStatus(statuses[r.json()["settled"]])
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
self.queue: asyncio.Queue = asyncio.Queue()
while True: while True:
yield await self.queue.get() item = await self.queue.get()
yield item
self.queue.task_done() self.queue.task_done()
async def webhook_listener(self): async def webhook_listener(self):
data = await request.get_json() text: str = await request.get_data()
if "event" not in data or data["event"].get("name") != "wallet_receive": data = json.loads(text)
return "" 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"] lntx_id = data["data"]["wtx"]["lnTx"]["id"]
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get(f"{self.endpoint}/user/lntx/{lntx_id}?fields=settled") as resp: resp = await session.get(
f"{self.endpoint}/user/lntx/{lntx_id}?fields=settled",
headers=self.auth_api,
)
data = await resp.json() data = await resp.json()
if data["settled"]: if data["settled"]:
self.queue.put_nowait(lntx_id) self.queue.put_nowait(lntx_id)
return "", HTTPStatus.NO_CONTENT

44
lnbits/wallets/opennode.py

@ -1,6 +1,11 @@
import json
import asyncio
import hmac
from http import HTTPStatus
from os import getenv from os import getenv
from typing import Optional from typing import Optional, AsyncGenerator
from requests import get, post from requests import get, post
from quart import request, url_for
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet, Unsupported from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet, Unsupported
@ -23,13 +28,18 @@ class OpenNodeWallet(Wallet):
r = post( r = post(
url=f"{self.endpoint}/v1/charges", url=f"{self.endpoint}/v1/charges",
headers=self.auth_invoice, 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 ok, checking_id, payment_request, error_message = r.ok, None, None, None
if r.ok: if r.ok:
data = r.json()["data"] 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: else:
error_message = r.json()["message"] error_message = r.json()["message"]
@ -64,3 +74,31 @@ class OpenNodeWallet(Wallet):
statuses = {"initial": None, "pending": None, "confirmed": True, "error": False, "failed": False} statuses = {"initial": None, "pending": None, "confirmed": True, "error": False, "failed": False}
return PaymentStatus(statuses[r.json()["data"]["status"]]) 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

Loading…
Cancel
Save