Files
lnbits/lnbits/wallets/opennode.py
fiatjaf 9185342c72 simplify environment variables required.
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.
2020-10-08 16:03:21 -03:00

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