lnbits-legend/lnbits/wallets/eclair.py

206 lines
5.9 KiB
Python
Raw Normal View History

2022-04-29 12:39:27 +02:00
import asyncio
import base64
2021-06-23 17:13:48 +02:00
import json
import urllib.parse
from os import getenv
2022-05-05 16:44:34 +02:00
from typing import AsyncGenerator, Dict, Optional
2022-04-29 12:39:27 +02:00
import httpx
2022-07-16 14:23:03 +02:00
from loguru import logger
2022-07-20 10:34:01 +02:00
from websockets import connect # type: ignore
# TODO: https://github.com/lnbits/lnbits-legend/issues/764
# mypy https://github.com/aaugustin/websockets/issues/940
2022-07-20 10:34:01 +02:00
2022-05-05 16:44:34 +02:00
from websockets.exceptions import (
ConnectionClosed,
ConnectionClosedError,
ConnectionClosedOK,
)
2021-06-23 17:13:48 +02:00
from .base import (
InvoiceResponse,
PaymentResponse,
PaymentStatus,
2022-04-29 12:39:27 +02:00
StatusResponse,
2021-06-23 17:13:48 +02:00
Wallet,
)
2022-04-29 12:39:27 +02:00
2021-06-23 17:13:48 +02:00
class EclairError(Exception):
pass
class UnknownError(Exception):
pass
2022-06-01 14:53:05 +02:00
2021-06-23 17:13:48 +02:00
class EclairWallet(Wallet):
def __init__(self):
url = getenv("ECLAIR_URL")
self.url = url[:-1] if url.endswith("/") else url
2022-05-05 16:44:34 +02:00
self.ws_url = f"ws://{urllib.parse.urlsplit(self.url).netloc}/ws"
2021-06-23 17:13:48 +02:00
passw = getenv("ECLAIR_PASS")
encodedAuth = base64.b64encode(f":{passw}".encode("utf-8"))
auth = str(encodedAuth, "utf-8")
self.auth = {"Authorization": f"Basic {auth}"}
async def status(self) -> StatusResponse:
2022-05-05 16:44:34 +02:00
async with httpx.AsyncClient() as client:
r = await client.post(
2022-06-01 14:53:05 +02:00
f"{self.url}/usablebalances", headers=self.auth, timeout=40
2022-05-05 16:44:34 +02:00
)
2021-06-23 17:13:48 +02:00
try:
2022-05-05 16:44:34 +02:00
data = r.json()
except:
return StatusResponse(
f"Failed to connect to {self.url}, got: '{r.text[:200]}...'", 0
)
2022-06-01 14:53:05 +02:00
2022-05-05 16:44:34 +02:00
if r.is_error:
return StatusResponse(data["error"], 0)
return StatusResponse(None, data[0]["canSend"] * 1000)
2021-06-23 17:13:48 +02:00
async def create_invoice(
self,
amount: int,
memo: Optional[str] = None,
description_hash: Optional[bytes] = None,
) -> InvoiceResponse:
2022-05-05 16:44:34 +02:00
data: Dict = {"amountMsat": amount * 1000}
2021-06-23 17:13:48 +02:00
if description_hash:
2022-05-05 16:44:34 +02:00
data["description_hash"] = description_hash.hex()
else:
data["description"] = memo or ""
async with httpx.AsyncClient() as client:
r = await client.post(
2022-06-01 14:53:05 +02:00
f"{self.url}/createinvoice", headers=self.auth, data=data, timeout=40
2022-05-05 16:44:34 +02:00
)
if r.is_error:
try:
data = r.json()
error_message = data["error"]
except:
error_message = r.text
pass
2021-06-23 17:13:48 +02:00
2022-05-05 16:44:34 +02:00
return InvoiceResponse(False, None, None, error_message)
2021-06-23 17:13:48 +02:00
2022-05-05 16:44:34 +02:00
data = r.json()
return InvoiceResponse(True, data["paymentHash"], data["serialized"], None)
2021-06-23 17:13:48 +02:00
2022-05-05 16:44:34 +02:00
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
async with httpx.AsyncClient() as client:
r = await client.post(
f"{self.url}/payinvoice",
headers=self.auth,
data={"invoice": bolt11, "blocking": True},
timeout=40,
)
if "error" in r.json():
try:
data = r.json()
error_message = data["error"]
except:
error_message = r.text
pass
return PaymentResponse(False, None, 0, None, error_message)
2022-06-01 14:53:05 +02:00
2022-05-05 16:44:34 +02:00
data = r.json()
checking_id = data["paymentHash"]
preimage = data["paymentPreimage"]
async with httpx.AsyncClient() as client:
r = await client.post(
f"{self.url}/getsentinfo",
headers=self.auth,
data={"paymentHash": checking_id},
timeout=40,
)
if "error" in r.json():
try:
data = r.json()
error_message = data["error"]
except:
error_message = r.text
pass
2022-06-01 14:53:05 +02:00
return PaymentResponse(
True, checking_id, 0, preimage, error_message
) ## ?? is this ok ??
2022-05-05 16:44:34 +02:00
data = r.json()
fees = [i["status"] for i in data]
fee_msat = sum([i["feesPaid"] for i in fees])
2022-06-01 14:53:05 +02:00
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
2022-05-05 16:44:34 +02:00
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
async with httpx.AsyncClient() as client:
r = await client.post(
f"{self.url}/getreceivedinfo",
headers=self.auth,
2022-06-01 14:53:05 +02:00
data={"paymentHash": checking_id},
2022-05-05 16:44:34 +02:00
)
data = r.json()
if r.is_error or "error" in data:
2021-06-23 17:13:48 +02:00
return PaymentStatus(None)
2022-05-05 16:44:34 +02:00
if data["status"]["type"] != "received":
2021-06-23 17:13:48 +02:00
return PaymentStatus(False)
2022-05-05 16:44:34 +02:00
2022-06-01 14:53:05 +02:00
return PaymentStatus(True)
2021-06-23 17:13:48 +02:00
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
2022-05-05 16:44:34 +02:00
async with httpx.AsyncClient() as client:
r = await client.post(
url=f"{self.url}/getsentinfo",
headers=self.auth,
2022-06-01 14:53:05 +02:00
data={"paymentHash": checking_id},
2022-05-05 16:44:34 +02:00
)
data = r.json()[0]
if r.is_error:
2021-06-23 17:13:48 +02:00
return PaymentStatus(None)
2022-06-01 14:53:05 +02:00
2022-05-05 16:44:34 +02:00
if data["status"]["type"] != "sent":
return PaymentStatus(False)
2021-06-23 17:13:48 +02:00
2022-05-05 16:44:34 +02:00
return PaymentStatus(True)
2021-06-23 17:13:48 +02:00
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
2022-06-01 14:53:05 +02:00
2022-05-05 16:44:34 +02:00
try:
2022-06-01 14:53:05 +02:00
async with connect(
self.ws_url,
extra_headers=[("Authorization", self.auth["Authorization"])],
) as ws:
2022-05-05 16:44:34 +02:00
while True:
2022-04-29 12:39:27 +02:00
message = await ws.recv()
2022-05-05 16:44:34 +02:00
message = json.loads(message)
2021-06-25 11:15:50 +02:00
2022-05-05 16:44:34 +02:00
if message and message["type"] == "payment-received":
2021-06-23 19:50:01 +02:00
yield message["paymentHash"]
2022-06-01 14:53:05 +02:00
except (
OSError,
ConnectionClosedOK,
ConnectionClosedError,
ConnectionClosed,
) as ose:
logger.error("OSE", ose)
2022-05-05 16:44:34 +02:00
pass
2021-06-23 19:50:01 +02:00
logger.error("lost connection to eclair's websocket, retrying in 5 seconds")
2022-04-29 12:39:27 +02:00
await asyncio.sleep(5)