mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2025-02-24 14:51:05 +01:00
before we were using requests which had no default timeouts, but httpx has a default timeout of 5 seconds. should have noticed that earlier. when the timeout expires we are left with a pending payment on the db with a temporary checking_id so we can never know if it was completed or not. this is still an issue, because technically a lightning payment may take 2 weeks or more, and we must have a way to dispatch a payment and check for it later. that should be the default (and we already do check for the payment status later, so half of the work is done), but on the other hand backends like lnpay and opennode do not give us a checking_id before the thing is already settled.
171 lines
5.8 KiB
Python
171 lines
5.8 KiB
Python
import trio # type: ignore
|
|
import httpx
|
|
import json
|
|
import base64
|
|
from os import getenv
|
|
from typing import Optional, Dict, AsyncGenerator
|
|
|
|
from .base import StatusResponse, InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
|
|
|
|
|
|
class LndRestWallet(Wallet):
|
|
"""https://api.lightning.community/rest/index.html#lnd-rest-api-reference"""
|
|
|
|
def __init__(self):
|
|
endpoint = getenv("LND_REST_ENDPOINT")
|
|
endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
|
|
endpoint = "https://" + endpoint if not endpoint.startswith("http") else endpoint
|
|
self.endpoint = endpoint
|
|
|
|
macaroon = (
|
|
getenv("LND_REST_MACAROON")
|
|
or getenv("LND_ADMIN_MACAROON")
|
|
or getenv("LND_REST_ADMIN_MACAROON")
|
|
or getenv("LND_INVOICE_MACAROON")
|
|
or getenv("LND_REST_INVOICE_MACAROON")
|
|
)
|
|
self.auth = {"Grpc-Metadata-macaroon": macaroon}
|
|
self.cert = getenv("LND_REST_CERT")
|
|
|
|
def status(self) -> StatusResponse:
|
|
try:
|
|
r = httpx.get(
|
|
f"{self.endpoint}/v1/balance/channels",
|
|
headers=self.auth,
|
|
verify=self.cert,
|
|
)
|
|
except (httpx.ConnectError, httpx.RequestError):
|
|
return StatusResponse(f"Unable to connect to {self.endpoint}.", 0)
|
|
|
|
try:
|
|
data = r.json()
|
|
if r.is_error:
|
|
raise Exception
|
|
except Exception:
|
|
return StatusResponse(r.text[:200], 0)
|
|
|
|
return StatusResponse(None, int(data["balance"]) * 1000)
|
|
|
|
def create_invoice(
|
|
self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None
|
|
) -> InvoiceResponse:
|
|
data: Dict = {
|
|
"value": amount,
|
|
"private": True,
|
|
}
|
|
if description_hash:
|
|
data["description_hash"] = base64.b64encode(description_hash).decode("ascii")
|
|
else:
|
|
data["memo"] = memo or ""
|
|
|
|
r = httpx.post(
|
|
url=f"{self.endpoint}/v1/invoices",
|
|
headers=self.auth,
|
|
verify=self.cert,
|
|
json=data,
|
|
)
|
|
|
|
if r.is_error:
|
|
error_message = r.text
|
|
try:
|
|
error_message = r.json()["error"]
|
|
except Exception:
|
|
pass
|
|
return InvoiceResponse(False, None, None, error_message)
|
|
|
|
data = r.json()
|
|
payment_request = data["payment_request"]
|
|
payment_hash = base64.b64decode(data["r_hash"]).hex()
|
|
checking_id = payment_hash
|
|
|
|
return InvoiceResponse(True, checking_id, payment_request, None)
|
|
|
|
def pay_invoice(self, bolt11: str) -> PaymentResponse:
|
|
r = httpx.post(
|
|
url=f"{self.endpoint}/v1/channels/transactions",
|
|
headers=self.auth,
|
|
verify=self.cert,
|
|
json={"payment_request": bolt11},
|
|
timeout=180,
|
|
)
|
|
|
|
if r.is_error:
|
|
error_message = r.text
|
|
try:
|
|
error_message = r.json()["error"]
|
|
except:
|
|
pass
|
|
return PaymentResponse(False, None, 0, None, error_message)
|
|
|
|
data = r.json()
|
|
payment_hash = data["payment_hash"]
|
|
checking_id = payment_hash
|
|
preimage = base64.b64decode(data["payment_preimage"]).hex()
|
|
return PaymentResponse(True, checking_id, 0, preimage, None)
|
|
|
|
def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
|
checking_id = checking_id.replace("_", "/")
|
|
r = httpx.get(
|
|
url=f"{self.endpoint}/v1/invoice/{checking_id}",
|
|
headers=self.auth,
|
|
verify=self.cert,
|
|
)
|
|
|
|
if r.is_error or not r.json().get("settled"):
|
|
# this must also work when checking_id is not a hex recognizable by lnd
|
|
# it will return an error and no "settled" attribute on the object
|
|
return PaymentStatus(None)
|
|
|
|
return PaymentStatus(True)
|
|
|
|
def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
|
r = httpx.get(
|
|
url=f"{self.endpoint}/v1/payments",
|
|
headers=self.auth,
|
|
verify=self.cert,
|
|
params={"max_payments": "20", "reversed": True},
|
|
)
|
|
|
|
if r.is_error:
|
|
return PaymentStatus(None)
|
|
|
|
# check payment.status:
|
|
# https://api.lightning.community/rest/index.html?python#peersynctype
|
|
statuses = {"UNKNOWN": None, "IN_FLIGHT": None, "SUCCEEDED": True, "FAILED": False}
|
|
|
|
# for some reason our checking_ids are in base64 but the payment hashes
|
|
# returned here are in hex, lnd is weird
|
|
checking_id = base64.b64decode(checking_id).hex()
|
|
|
|
for p in r.json()["payments"]:
|
|
if p["payment_hash"] == checking_id:
|
|
return PaymentStatus(statuses[p["status"]])
|
|
|
|
return PaymentStatus(None)
|
|
|
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
|
url = self.endpoint + "/v1/invoices/subscribe"
|
|
|
|
while True:
|
|
try:
|
|
async with httpx.AsyncClient(
|
|
timeout=None,
|
|
headers=self.auth,
|
|
verify=self.cert,
|
|
) as client:
|
|
async with client.stream("GET", url) as r:
|
|
async for line in r.aiter_lines():
|
|
try:
|
|
inv = json.loads(line)["result"]
|
|
if not inv["settled"]:
|
|
continue
|
|
except:
|
|
continue
|
|
|
|
payment_hash = base64.b64decode(inv["r_hash"]).hex()
|
|
yield payment_hash
|
|
except (OSError, httpx.ConnectError, httpx.ReadError):
|
|
pass
|
|
|
|
print("lost connection to lnd invoices stream, retrying in 5 seconds")
|
|
await trio.sleep(5)
|