Merge pull request #616 from talvasconcelos/eclair-backend

Add Eclair backend/funding wallet
This commit is contained in:
Arc 2022-05-17 14:19:28 +01:00 committed by GitHub
commit 9d6382294a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 212 additions and 7 deletions

View file

@ -36,7 +36,7 @@ LNBITS_SITE_DESCRIPTION="Some description about your service, will display if ti
LNBITS_THEME_OPTIONS="classic, bitcoin, freedom, mint, autumn, monochrome, salvador"
# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet,
# LndRestWallet, CLightningWallet, LNbitsWallet, SparkWallet, FakeWallet
# LndRestWallet, CLightningWallet, LNbitsWallet, SparkWallet, FakeWallet, EclairWallet
LNBITS_BACKEND_WALLET_CLASS=VoidWallet
# VoidWallet is just a fallback that works without any actual Lightning capabilities,
# just so you can see the UI before dealing with this file.
@ -77,4 +77,8 @@ OPENNODE_KEY=OPENNODE_ADMIN_KEY
# FakeWallet
FAKE_WALLET_SECRET="ToTheMoon1"
LNBITS_DENOMINATION=sats
LNBITS_DENOMINATION=sats
# EclairWallet
ECLAIR_URL=http://127.0.0.1:8283
ECLAIR_PASS=eclairpw

View file

@ -1,11 +1,12 @@
# flake8: noqa
from .void import VoidWallet
from .clightning import CLightningWallet
from .lntxbot import LntxbotWallet
from .opennode import OpenNodeWallet
from .lnpay import LNPayWallet
from .eclair import EclairWallet
from .fake import FakeWallet
from .lnbits import LNbitsWallet
from .lndrest import LndRestWallet
from .lnpay import LNPayWallet
from .lntxbot import LntxbotWallet
from .opennode import OpenNodeWallet
from .spark import SparkWallet
from .fake import FakeWallet
from .void import VoidWallet

200
lnbits/wallets/eclair.py Normal file
View file

@ -0,0 +1,200 @@
import asyncio
import base64
import json
import urllib.parse
from os import getenv
from typing import AsyncGenerator, Dict, Optional
import httpx
from websockets import connect
from websockets.exceptions import (
ConnectionClosed,
ConnectionClosedError,
ConnectionClosedOK,
)
from .base import (
InvoiceResponse,
PaymentResponse,
PaymentStatus,
StatusResponse,
Wallet,
)
class EclairError(Exception):
pass
class UnknownError(Exception):
pass
class EclairWallet(Wallet):
def __init__(self):
url = getenv("ECLAIR_URL")
self.url = url[:-1] if url.endswith("/") else url
self.ws_url = f"ws://{urllib.parse.urlsplit(self.url).netloc}/ws"
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:
async with httpx.AsyncClient() as client:
r = await client.post(
f"{self.url}/usablebalances",
headers=self.auth,
timeout=40
)
try:
data = r.json()
except:
return StatusResponse(
f"Failed to connect to {self.url}, got: '{r.text[:200]}...'", 0
)
if r.is_error:
return StatusResponse(data["error"], 0)
return StatusResponse(None, data[0]["canSend"] * 1000)
async def create_invoice(
self,
amount: int,
memo: Optional[str] = None,
description_hash: Optional[bytes] = None,
) -> InvoiceResponse:
data: Dict = {"amountMsat": amount * 1000}
if description_hash:
data["description_hash"] = description_hash.hex()
else:
data["description"] = memo or ""
async with httpx.AsyncClient() as client:
r = await client.post(
f"{self.url}/createinvoice",
headers=self.auth,
data=data,
timeout=40
)
if r.is_error:
try:
data = r.json()
error_message = data["error"]
except:
error_message = r.text
pass
return InvoiceResponse(False, None, None, error_message)
data = r.json()
return InvoiceResponse(True, data["paymentHash"], data["serialized"], None)
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)
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
return PaymentResponse(True, checking_id, 0, preimage, error_message) ## ?? is this ok ??
data = r.json()
fees = [i["status"] for i in data]
fee_msat = sum([i["feesPaid"] for i in fees])
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
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,
data={"paymentHash": checking_id}
)
data = r.json()
if r.is_error or "error" in data:
return PaymentStatus(None)
if data["status"]["type"] != "received":
return PaymentStatus(False)
return PaymentStatus(True)
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
async with httpx.AsyncClient() as client:
r = await client.post(
url=f"{self.url}/getsentinfo",
headers=self.auth,
data={"paymentHash": checking_id}
)
data = r.json()[0]
if r.is_error:
return PaymentStatus(None)
if data["status"]["type"] != "sent":
return PaymentStatus(False)
return PaymentStatus(True)
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
try:
async with connect(self.ws_url, extra_headers=[('Authorization', self.auth["Authorization"])]) as ws:
while True:
message = await ws.recv()
message = json.loads(message)
if message and message["type"] == "payment-received":
yield message["paymentHash"]
except (OSError, ConnectionClosedOK, ConnectionClosedError, ConnectionClosed) as ose:
print('OSE', ose)
pass
print("lost connection to eclair's websocket, retrying in 5 seconds")
await asyncio.sleep(5)