2021-08-30 19:55:02 +02:00
|
|
|
import asyncio
|
2020-05-02 19:00:01 +02:00
|
|
|
import random
|
2023-08-28 11:59:56 +02:00
|
|
|
from typing import Any, AsyncGenerator, Optional
|
2022-07-16 14:23:03 +02:00
|
|
|
|
2023-09-25 12:06:54 +02:00
|
|
|
from bolt11.decode import decode as bolt11_decode
|
|
|
|
from bolt11.exceptions import Bolt11Exception
|
2022-08-01 16:41:50 +02:00
|
|
|
from loguru import logger
|
2023-08-16 15:54:06 +02:00
|
|
|
from pyln.client import LightningRpc, RpcError
|
2022-08-01 16:41:50 +02:00
|
|
|
|
2023-09-25 15:04:44 +02:00
|
|
|
from lnbits.nodes.cln import CoreLightningNode
|
2022-10-05 13:01:41 +02:00
|
|
|
from lnbits.settings import settings
|
2020-10-13 03:25:55 +02:00
|
|
|
|
2021-03-24 04:40:32 +01:00
|
|
|
from .base import (
|
|
|
|
InvoiceResponse,
|
|
|
|
PaymentResponse,
|
|
|
|
PaymentStatus,
|
2021-11-04 19:49:48 +01:00
|
|
|
StatusResponse,
|
2021-03-24 04:40:32 +01:00
|
|
|
Unsupported,
|
2021-11-04 19:49:48 +01:00
|
|
|
Wallet,
|
2021-03-24 04:40:32 +01:00
|
|
|
)
|
2020-05-02 19:00:01 +02:00
|
|
|
|
2020-04-03 12:21:10 +02:00
|
|
|
|
2023-08-28 11:59:56 +02:00
|
|
|
async def run_sync(func) -> Any:
|
|
|
|
loop = asyncio.get_event_loop()
|
|
|
|
return await loop.run_in_executor(None, func)
|
2021-11-04 19:49:48 +01:00
|
|
|
|
|
|
|
|
2022-08-01 16:41:50 +02:00
|
|
|
class CoreLightningWallet(Wallet):
|
2023-09-25 15:04:44 +02:00
|
|
|
__node_cls__ = CoreLightningNode
|
|
|
|
|
2020-04-03 12:21:10 +02:00
|
|
|
def __init__(self):
|
2022-10-05 13:01:41 +02:00
|
|
|
self.rpc = settings.corelightning_rpc or settings.clightning_rpc
|
2020-10-03 22:27:55 +02:00
|
|
|
self.ln = LightningRpc(self.rpc)
|
2020-10-02 22:13:33 +02:00
|
|
|
|
2023-08-23 08:59:39 +02:00
|
|
|
# check if description_hash is supported (from corelightning>=v0.11.0)
|
2023-08-24 11:26:09 +02:00
|
|
|
command = self.ln.help("invoice")["help"][0]["command"] # type: ignore
|
|
|
|
self.supports_description_hash = "deschashonly" in command
|
2020-10-02 22:13:33 +02:00
|
|
|
|
|
|
|
# check last payindex so we can listen from that point on
|
|
|
|
self.last_pay_index = 0
|
2023-08-23 08:59:39 +02:00
|
|
|
invoices: dict = self.ln.listinvoices() # type: ignore
|
2020-10-03 22:27:55 +02:00
|
|
|
for inv in invoices["invoices"][::-1]:
|
|
|
|
if "pay_index" in inv:
|
|
|
|
self.last_pay_index = inv["pay_index"]
|
|
|
|
break
|
2020-04-26 13:28:19 +02:00
|
|
|
|
2021-03-24 05:01:09 +01:00
|
|
|
async def status(self) -> StatusResponse:
|
2020-10-13 03:25:55 +02:00
|
|
|
try:
|
2023-08-23 08:59:39 +02:00
|
|
|
funds: dict = self.ln.listfunds() # type: ignore
|
2020-10-13 03:25:55 +02:00
|
|
|
return StatusResponse(
|
2023-05-22 13:03:37 +02:00
|
|
|
None, sum([int(ch["our_amount_msat"]) for ch in funds["channels"]])
|
2020-10-13 03:25:55 +02:00
|
|
|
)
|
|
|
|
except RpcError as exc:
|
|
|
|
error_message = f"lightningd '{exc.method}' failed with '{exc.error}'."
|
|
|
|
return StatusResponse(error_message, 0)
|
|
|
|
|
2021-03-24 05:01:09 +01:00
|
|
|
async def create_invoice(
|
2021-03-24 04:40:32 +01:00
|
|
|
self,
|
|
|
|
amount: int,
|
|
|
|
memo: Optional[str] = None,
|
|
|
|
description_hash: Optional[bytes] = None,
|
2022-08-13 14:29:04 +02:00
|
|
|
unhashed_description: Optional[bytes] = None,
|
2023-01-26 11:08:40 +01:00
|
|
|
**kwargs,
|
2020-08-31 04:48:46 +02:00
|
|
|
) -> InvoiceResponse:
|
2023-01-21 13:38:17 +01:00
|
|
|
label = f"lbl{random.random()}"
|
2022-08-04 17:40:04 +02:00
|
|
|
msat: int = int(amount * 1000)
|
2020-10-02 22:13:33 +02:00
|
|
|
try:
|
2022-08-14 21:59:36 +02:00
|
|
|
if description_hash and not unhashed_description:
|
|
|
|
raise Unsupported(
|
2023-08-24 11:26:09 +02:00
|
|
|
"'description_hash' unsupported by CoreLightning, provide"
|
|
|
|
" 'unhashed_description'"
|
2022-08-14 21:59:36 +02:00
|
|
|
)
|
2022-08-13 14:29:04 +02:00
|
|
|
if unhashed_description and not self.supports_description_hash:
|
|
|
|
raise Unsupported("unhashed_description")
|
2023-08-23 08:59:39 +02:00
|
|
|
r: dict = self.ln.invoice( # type: ignore
|
2022-08-01 16:41:50 +02:00
|
|
|
msatoshi=msat,
|
|
|
|
label=label,
|
2023-08-24 11:26:09 +02:00
|
|
|
description=(
|
|
|
|
unhashed_description.decode() if unhashed_description else memo
|
|
|
|
),
|
2022-08-01 16:41:50 +02:00
|
|
|
exposeprivatechannels=True,
|
2023-08-24 11:26:09 +02:00
|
|
|
deschashonly=(
|
|
|
|
True if unhashed_description else False
|
|
|
|
), # we can't pass None here
|
2023-01-26 11:08:40 +01:00
|
|
|
expiry=kwargs.get("expiry"),
|
2022-08-01 16:41:50 +02:00
|
|
|
)
|
|
|
|
|
2023-08-23 08:59:39 +02:00
|
|
|
if r.get("code") and r.get("code") < 0: # type: ignore
|
2022-08-01 16:41:50 +02:00
|
|
|
raise Exception(r.get("message"))
|
|
|
|
|
|
|
|
return InvoiceResponse(True, r["payment_hash"], r["bolt11"], "")
|
2020-10-02 22:13:33 +02:00
|
|
|
except RpcError as exc:
|
2023-08-24 11:26:09 +02:00
|
|
|
error_message = (
|
|
|
|
f"CoreLightning method '{exc.method}' failed with"
|
|
|
|
f" '{exc.error.get('message') or exc.error}'." # type: ignore
|
|
|
|
)
|
2022-08-01 16:41:50 +02:00
|
|
|
return InvoiceResponse(False, None, None, error_message)
|
|
|
|
except Exception as e:
|
|
|
|
return InvoiceResponse(False, None, None, str(e))
|
2020-04-03 12:21:10 +02:00
|
|
|
|
2022-03-16 07:20:15 +01:00
|
|
|
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
2023-09-25 12:06:54 +02:00
|
|
|
try:
|
|
|
|
invoice = bolt11_decode(bolt11)
|
|
|
|
except Bolt11Exception as exc:
|
|
|
|
return PaymentResponse(False, None, None, None, str(exc))
|
2022-08-30 13:28:58 +02:00
|
|
|
|
|
|
|
previous_payment = await self.get_payment_status(invoice.payment_hash)
|
|
|
|
if previous_payment.paid:
|
|
|
|
return PaymentResponse(False, None, None, None, "invoice already paid")
|
|
|
|
|
2023-09-25 12:06:54 +02:00
|
|
|
if not invoice.amount_msat or invoice.amount_msat <= 0:
|
|
|
|
return PaymentResponse(
|
|
|
|
False, None, None, None, "CLN 0 amount invoice not supported"
|
|
|
|
)
|
|
|
|
|
2022-03-16 07:20:15 +01:00
|
|
|
fee_limit_percent = fee_limit_msat / invoice.amount_msat * 100
|
2023-08-24 11:26:09 +02:00
|
|
|
# so fee_limit_percent is applied even on payments with fee < 5000 millisatoshi
|
|
|
|
# (which is default value of exemptfee)
|
2022-03-16 07:20:15 +01:00
|
|
|
payload = {
|
|
|
|
"bolt11": bolt11,
|
2023-01-21 13:38:17 +01:00
|
|
|
"maxfeepercent": f"{fee_limit_percent:.11}",
|
2023-08-24 11:26:09 +02:00
|
|
|
"exemptfee": 0,
|
2023-09-25 15:04:44 +02:00
|
|
|
# so fee_limit_percent is applied even on payments with fee < 5000
|
|
|
|
# millisatoshi (which is default value of exemptfee)
|
|
|
|
"description": invoice.description,
|
2022-03-16 07:20:15 +01:00
|
|
|
}
|
2020-10-13 04:18:37 +02:00
|
|
|
try:
|
2023-08-28 11:59:56 +02:00
|
|
|
r = await run_sync(lambda: self.ln.call("pay", payload))
|
2022-08-30 13:28:58 +02:00
|
|
|
except RpcError as exc:
|
|
|
|
try:
|
2023-08-23 08:59:39 +02:00
|
|
|
error_message = exc.error["attempts"][-1]["fail_reason"] # type: ignore
|
2023-08-16 12:17:54 +02:00
|
|
|
except Exception:
|
2023-08-24 11:26:09 +02:00
|
|
|
error_message = (
|
|
|
|
f"CoreLightning method '{exc.method}' failed with"
|
|
|
|
f" '{exc.error.get('message') or exc.error}'." # type: ignore
|
|
|
|
)
|
2022-08-30 13:28:58 +02:00
|
|
|
return PaymentResponse(False, None, None, None, error_message)
|
2022-08-01 16:41:50 +02:00
|
|
|
except Exception as exc:
|
2022-08-30 13:28:58 +02:00
|
|
|
return PaymentResponse(False, None, None, None, str(exc))
|
2020-10-13 04:18:37 +02:00
|
|
|
|
2023-05-22 13:03:37 +02:00
|
|
|
fee_msat = -int(r["amount_sent_msat"] - r["amount_msat"])
|
2022-08-01 16:41:50 +02:00
|
|
|
return PaymentResponse(
|
|
|
|
True, r["payment_hash"], fee_msat, r["payment_preimage"], None
|
|
|
|
)
|
2020-04-03 12:21:10 +02:00
|
|
|
|
2021-03-24 05:01:09 +01:00
|
|
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
2022-08-01 16:51:19 +02:00
|
|
|
try:
|
2023-08-23 08:59:39 +02:00
|
|
|
r: dict = self.ln.listinvoices(payment_hash=checking_id) # type: ignore
|
|
|
|
except RpcError:
|
2022-08-01 16:51:19 +02:00
|
|
|
return PaymentStatus(None)
|
2020-08-29 17:23:01 +02:00
|
|
|
if not r["invoices"]:
|
2022-08-01 16:51:19 +02:00
|
|
|
return PaymentStatus(None)
|
2022-08-30 13:28:58 +02:00
|
|
|
|
|
|
|
invoice_resp = r["invoices"][-1]
|
|
|
|
|
|
|
|
if invoice_resp["payment_hash"] == checking_id:
|
|
|
|
if invoice_resp["status"] == "paid":
|
|
|
|
return PaymentStatus(True)
|
|
|
|
elif invoice_resp["status"] == "unpaid":
|
|
|
|
return PaymentStatus(None)
|
2023-07-24 12:00:41 +02:00
|
|
|
elif invoice_resp["status"] == "expired":
|
|
|
|
return PaymentStatus(False)
|
|
|
|
else:
|
|
|
|
logger.warning(f"supplied an invalid checking_id: {checking_id}")
|
2022-08-30 13:28:58 +02:00
|
|
|
return PaymentStatus(None)
|
2020-04-03 12:21:10 +02:00
|
|
|
|
2021-03-24 05:01:09 +01:00
|
|
|
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
2022-08-01 16:51:19 +02:00
|
|
|
try:
|
2023-08-23 08:59:39 +02:00
|
|
|
r: dict = self.ln.listpays(payment_hash=checking_id) # type: ignore
|
2023-08-16 12:17:54 +02:00
|
|
|
except Exception:
|
2022-08-01 16:51:19 +02:00
|
|
|
return PaymentStatus(None)
|
2023-11-21 12:54:55 +01:00
|
|
|
if "pays" not in r:
|
2022-08-01 16:51:19 +02:00
|
|
|
return PaymentStatus(None)
|
2023-11-21 12:54:55 +01:00
|
|
|
if not r["pays"]:
|
|
|
|
# no payment with this payment_hash is found
|
|
|
|
return PaymentStatus(False)
|
|
|
|
|
2022-08-30 13:28:58 +02:00
|
|
|
payment_resp = r["pays"][-1]
|
|
|
|
|
|
|
|
if payment_resp["payment_hash"] == checking_id:
|
|
|
|
status = payment_resp["status"]
|
2020-08-29 17:23:01 +02:00
|
|
|
if status == "complete":
|
2022-08-30 13:28:58 +02:00
|
|
|
fee_msat = -int(
|
|
|
|
payment_resp["amount_sent_msat"] - payment_resp["amount_msat"]
|
|
|
|
)
|
|
|
|
|
|
|
|
return PaymentStatus(True, fee_msat, payment_resp["preimage"])
|
2020-08-29 17:23:01 +02:00
|
|
|
elif status == "failed":
|
|
|
|
return PaymentStatus(False)
|
2023-07-24 12:00:41 +02:00
|
|
|
else:
|
|
|
|
return PaymentStatus(None)
|
|
|
|
else:
|
|
|
|
logger.warning(f"supplied an invalid checking_id: {checking_id}")
|
2022-08-30 13:28:58 +02:00
|
|
|
return PaymentStatus(None)
|
2020-10-02 22:13:33 +02:00
|
|
|
|
|
|
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
|
|
|
while True:
|
2022-08-01 16:41:50 +02:00
|
|
|
try:
|
2023-08-28 11:59:56 +02:00
|
|
|
paid = await run_sync(
|
|
|
|
lambda: self.ln.waitanyinvoice(self.last_pay_index, timeout=2)
|
|
|
|
)
|
2022-08-01 16:41:50 +02:00
|
|
|
self.last_pay_index = paid["pay_index"]
|
|
|
|
yield paid["payment_hash"]
|
2023-08-28 11:59:56 +02:00
|
|
|
except RpcError as exc:
|
|
|
|
# only raise if not a timeout
|
|
|
|
if exc.error["code"] != 904: # type: ignore
|
|
|
|
raise
|
2022-08-01 16:41:50 +02:00
|
|
|
except Exception as exc:
|
|
|
|
logger.error(
|
2023-08-24 11:26:09 +02:00
|
|
|
f"lost connection to corelightning invoices stream: '{exc}', "
|
|
|
|
"retrying in 5 seconds"
|
2022-08-01 16:41:50 +02:00
|
|
|
)
|
|
|
|
await asyncio.sleep(5)
|