lnbits-legend/lnbits/wallets/lndrest.py

234 lines
8.1 KiB
Python
Raw Normal View History

import asyncio
2020-04-26 01:10:57 +01:00
import base64
import hashlib
2022-07-16 14:23:03 +02:00
import json
from typing import AsyncGenerator, Dict, Optional
2022-07-16 14:23:03 +02:00
import httpx
from loguru import logger
[FEAT] Node Managment (#1895) * [FEAT] Node Managment feat: node dashboard channels and transactions fix: update channel variables better types refactor ui add onchain balances and backend_name mock values for fake wallet remove app tab start implementing peers and channel management peer and channel management implement channel closing add channel states, better errors seperate payments and invoices on transactions tab display total channel balance feat: optional public page feat: show node address fix: port conversion feat: details dialog on transactions fix: peer info without alias fix: rename channel balances small improvements to channels tab feat: pagination on transactions tab test caching transactions refactor: move WALLET into wallets module fix: backwards compatibility refactor: move get_node_class to nodes modules post merge bundle fundle feat: disconnect peer feat: initial lnd support only use filtered channels for total balance adjust closing logic add basic node tests add setting for disabling transactions tab revert unnecessary changes add tests for invoices and payments improve payment and invoice implementations the previously used invoice fixture has a session scope, but a new invoice is required tests and bug fixes for channels api use query instead of body in channel delete delete requests should generally not use a body take node id through path instead of body for delete endpoint add peer management tests more tests for errors improve error handling rename id and pubkey to peer_id for consistency remove dead code fix http status codes make cache keys safer cache node public info comments for node settings rename node prop in frontend adjust tests to new status codes cln: use amount_msat instead of value for onchain balance turn transactions tab off by default enable transactions in tests only allow super user to create or delete fix prop name in admin navbar --------- Co-authored-by: jacksn <jkranawetter05@gmail.com>
2023-09-25 15:04:44 +02:00
from lnbits.nodes.lndrest import LndRestNode
2022-10-05 13:01:41 +02:00
from lnbits.settings import settings
2021-11-29 18:10:11 +01:00
from .base import (
InvoiceResponse,
PaymentResponse,
PaymentStatus,
2022-07-16 14:23:03 +02:00
StatusResponse,
Wallet,
)
2022-07-16 14:23:03 +02:00
from .macaroon import AESCipher, load_macaroon
2020-04-25 22:10:45 +01:00
class LndRestWallet(Wallet):
"""https://api.lightning.community/rest/index.html#lnd-rest-api-reference"""
[FEAT] Node Managment (#1895) * [FEAT] Node Managment feat: node dashboard channels and transactions fix: update channel variables better types refactor ui add onchain balances and backend_name mock values for fake wallet remove app tab start implementing peers and channel management peer and channel management implement channel closing add channel states, better errors seperate payments and invoices on transactions tab display total channel balance feat: optional public page feat: show node address fix: port conversion feat: details dialog on transactions fix: peer info without alias fix: rename channel balances small improvements to channels tab feat: pagination on transactions tab test caching transactions refactor: move WALLET into wallets module fix: backwards compatibility refactor: move get_node_class to nodes modules post merge bundle fundle feat: disconnect peer feat: initial lnd support only use filtered channels for total balance adjust closing logic add basic node tests add setting for disabling transactions tab revert unnecessary changes add tests for invoices and payments improve payment and invoice implementations the previously used invoice fixture has a session scope, but a new invoice is required tests and bug fixes for channels api use query instead of body in channel delete delete requests should generally not use a body take node id through path instead of body for delete endpoint add peer management tests more tests for errors improve error handling rename id and pubkey to peer_id for consistency remove dead code fix http status codes make cache keys safer cache node public info comments for node settings rename node prop in frontend adjust tests to new status codes cln: use amount_msat instead of value for onchain balance turn transactions tab off by default enable transactions in tests only allow super user to create or delete fix prop name in admin navbar --------- Co-authored-by: jacksn <jkranawetter05@gmail.com>
2023-09-25 15:04:44 +02:00
__node_cls__ = LndRestNode
2020-04-25 22:10:45 +01:00
def __init__(self):
2022-10-05 13:01:41 +02:00
endpoint = settings.lnd_rest_endpoint
2023-04-19 09:57:19 +02:00
cert = settings.lnd_rest_cert
macaroon = (
2022-10-05 13:01:41 +02:00
settings.lnd_rest_macaroon
or settings.lnd_admin_macaroon
or settings.lnd_rest_admin_macaroon
or settings.lnd_invoice_macaroon
or settings.lnd_rest_invoice_macaroon
)
2022-10-05 13:01:41 +02:00
encrypted_macaroon = settings.lnd_rest_macaroon_encrypted
if encrypted_macaroon:
macaroon = AESCipher(description="macaroon decryption").decrypt(
encrypted_macaroon
)
2023-04-19 09:57:19 +02:00
if not endpoint:
raise Exception("cannot initialize lndrest: no endpoint")
if not macaroon:
raise Exception("cannot initialize lndrest: no macaroon")
if not cert:
logger.warning(
"no certificate for lndrest provided, this only works if you have a"
" publicly issued certificate"
2023-04-19 09:57:19 +02:00
)
endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
endpoint = (
2023-04-04 08:00:43 +02:00
f"https://{endpoint}" if not endpoint.startswith("http") else endpoint
)
self.endpoint = endpoint
self.macaroon = load_macaroon(macaroon)
2023-04-19 09:57:19 +02:00
# if no cert provided it should be public so we set verify to True
# and it will still check for validity of certificate and fail if its not valid
# even on startup
self.cert = cert or True
self.auth = {"Grpc-Metadata-macaroon": self.macaroon}
self.client = httpx.AsyncClient(
base_url=self.endpoint, headers=self.auth, verify=self.cert
)
async def cleanup(self):
try:
await self.client.aclose()
except RuntimeError as e:
logger.warning(f"Error closing wallet connection: {e}")
2020-04-25 22:10:45 +01:00
async def status(self) -> StatusResponse:
try:
r = await self.client.get("/v1/balance/channels")
r.raise_for_status()
except (httpx.ConnectError, httpx.RequestError) as exc:
return StatusResponse(f"Unable to connect to {self.endpoint}. {exc}", 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)
async def create_invoice(
self,
amount: int,
memo: Optional[str] = None,
description_hash: Optional[bytes] = None,
unhashed_description: Optional[bytes] = None,
**kwargs,
) -> InvoiceResponse:
data: Dict = {"value": amount, "private": True, "memo": memo or ""}
if kwargs.get("expiry"):
data["expiry"] = kwargs["expiry"]
if description_hash:
data["description_hash"] = base64.b64encode(description_hash).decode(
"ascii"
)
elif unhashed_description:
data["description_hash"] = base64.b64encode(
hashlib.sha256(unhashed_description).digest()
).decode("ascii")
r = await self.client.post(url="/v1/invoices", json=data)
2020-04-25 22:10:45 +01:00
if r.is_error:
error_message = r.text
try:
error_message = r.json()["error"]
except Exception:
pass
return InvoiceResponse(False, None, None, error_message)
2020-04-25 22:10:45 +01:00
data = r.json()
payment_request = data["payment_request"]
payment_hash = base64.b64decode(data["r_hash"]).hex()
checking_id = payment_hash
2020-04-25 22:10:45 +01:00
return InvoiceResponse(True, checking_id, payment_request, None)
2020-04-25 22:10:45 +01:00
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
# set the fee limit for the payment
lnrpcFeeLimit = dict()
lnrpcFeeLimit["fixed_msat"] = f"{fee_limit_msat}"
r = await self.client.post(
url="/v1/channels/transactions",
json={"payment_request": bolt11, "fee_limit": lnrpcFeeLimit},
timeout=None,
)
2020-04-25 22:10:45 +01:00
if r.is_error or r.json().get("payment_error"):
error_message = r.json().get("payment_error") or r.text
return PaymentResponse(False, None, None, None, error_message)
data = r.json()
checking_id = base64.b64decode(data["payment_hash"]).hex()
2021-12-09 00:26:08 +01:00
fee_msat = int(data["payment_route"]["total_fees_msat"])
preimage = base64.b64decode(data["payment_preimage"]).hex()
2021-12-09 00:26:08 +01:00
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
2020-04-25 22:10:45 +01:00
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
r = await self.client.get(url=f"/v1/invoice/{checking_id}")
2020-10-05 22:43:49 -03:00
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
2020-04-25 22:10:45 +01:00
return PaymentStatus(None)
2020-10-05 22:43:49 -03:00
return PaymentStatus(True)
2020-04-25 22:10:45 +01:00
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
"""
This routine checks the payment status using routerpc.TrackPaymentV2.
"""
# convert checking_id from hex to base64 and some LND magic
try:
checking_id = base64.urlsafe_b64encode(bytes.fromhex(checking_id)).decode(
"ascii"
)
except ValueError:
return PaymentStatus(None)
url = f"/v2/router/track/{checking_id}"
2020-04-26 01:10:57 +01:00
# check payment.status:
# https://api.lightning.community/?python=#paymentpaymentstatus
statuses = {
"UNKNOWN": None,
"IN_FLIGHT": None,
"SUCCEEDED": True,
"FAILED": False,
}
2020-04-25 22:10:45 +01:00
async with self.client.stream("GET", url, timeout=None) as r:
async for json_line in r.aiter_lines():
try:
line = json.loads(json_line)
if line.get("error"):
logger.error(
line["error"]["message"]
if "message" in line["error"]
else line["error"]
)
return PaymentStatus(None)
payment = line.get("result")
if payment is not None and payment.get("status"):
return PaymentStatus(
paid=statuses[payment["status"]],
fee_msat=payment.get("fee_msat"),
preimage=payment.get("payment_preimage"),
)
else:
return PaymentStatus(None)
except Exception:
continue
return PaymentStatus(None)
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
while True:
try:
url = "/v1/invoices/subscribe"
async with self.client.stream("GET", url, timeout=None) as r:
async for line in r.aiter_lines():
try:
inv = json.loads(line)["result"]
if not inv["settled"]:
continue
except Exception:
continue
payment_hash = base64.b64decode(inv["r_hash"]).hex()
yield payment_hash
except Exception as exc:
logger.error(
f"lost connection to lnd invoices stream: '{exc}', retrying in 5"
" seconds"
)
await asyncio.sleep(5)