mirror of
https://github.com/lnbits/lnbits.git
synced 2025-10-10 20:42:32 +02:00
instead of multiple keys/macaroons with different permissions we request only one. if someone wants to use lnbits with an invoice macaroon they're free to do it and we will just fail on 'pay' methods, as before. this also grandfathers the previous environment variables names so everything keeps working without people having to change their setups. in the meantime some bugs with lntxbot and c-lightning were fixed and the `requests` dependency was eliminated because I can't organize myself into meaningful chunks of changes.
101 lines
3.6 KiB
Python
101 lines
3.6 KiB
Python
import json
|
|
import trio # type: ignore
|
|
import hmac
|
|
import httpx
|
|
from http import HTTPStatus
|
|
from os import getenv
|
|
from typing import Optional, AsyncGenerator
|
|
from quart import request, url_for
|
|
|
|
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet, Unsupported
|
|
|
|
|
|
class OpenNodeWallet(Wallet):
|
|
"""https://developers.opennode.com/"""
|
|
|
|
def __init__(self):
|
|
endpoint = getenv("OPENNODE_API_ENDPOINT")
|
|
self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
|
|
|
|
key = getenv("OPENNODE_KEY") or getenv("OPENNODE_ADMIN_KEY") or getenv("OPENNODE_INVOICE_KEY")
|
|
self.auth = {"Authorization": key}
|
|
|
|
def create_invoice(
|
|
self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None
|
|
) -> InvoiceResponse:
|
|
if description_hash:
|
|
raise Unsupported("description_hash")
|
|
|
|
r = httpx.post(
|
|
f"{self.endpoint}/v1/charges",
|
|
headers=self.auth_invoice,
|
|
json={
|
|
"amount": amount,
|
|
"description": memo or "",
|
|
"callback_url": url_for("webhook_listener", _external=True),
|
|
},
|
|
)
|
|
|
|
if r.is_error:
|
|
error_message = r.json()["message"]
|
|
return InvoiceResponse(False, None, None, error_message)
|
|
|
|
data = r.json()["data"]
|
|
checking_id = data["id"]
|
|
payment_request = data["lightning_invoice"]["payreq"]
|
|
return InvoiceResponse(True, checking_id, payment_request, None)
|
|
|
|
def pay_invoice(self, bolt11: str) -> PaymentResponse:
|
|
r = httpx.post(f"{self.endpoint}/v2/withdrawals", headers=self.auth, json={"type": "ln", "address": bolt11})
|
|
|
|
if r.is_error:
|
|
error_message = r.json()["message"]
|
|
return PaymentResponse(False, None, 0, error_message)
|
|
|
|
data = r.json()["data"]
|
|
checking_id = data["id"]
|
|
fee_msat = data["fee"] * 1000
|
|
return PaymentResponse(True, checking_id, fee_msat, None)
|
|
|
|
def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
|
r = httpx.get(f"{self.endpoint}/v1/charge/{checking_id}", headers=self.auth_invoice)
|
|
|
|
if r.is_error:
|
|
return PaymentStatus(None)
|
|
|
|
statuses = {"processing": None, "paid": True, "unpaid": False}
|
|
return PaymentStatus(statuses[r.json()["data"]["status"]])
|
|
|
|
def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
|
r = httpx.get(f"{self.endpoint}/v1/withdrawal/{checking_id}", headers=self.auth)
|
|
|
|
if r.is_error:
|
|
return PaymentStatus(None)
|
|
|
|
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.send, receive = trio.open_memory_channel(0)
|
|
async for value in receive:
|
|
yield value
|
|
|
|
async def webhook_listener(self):
|
|
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
|
|
|
|
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
|
|
|
|
await self.send.send(charge_id)
|
|
return "", HTTPStatus.NO_CONTENT
|