2022-06-04 14:10:44 +02:00
|
|
|
import asyncio
|
|
|
|
|
import json
|
2022-12-18 20:55:48 +01:00
|
|
|
import sys
|
2022-06-04 14:10:44 +02:00
|
|
|
from typing import AsyncGenerator, List, Optional
|
|
|
|
|
|
|
|
|
|
import grpc
|
|
|
|
|
from fastapi.exceptions import HTTPException
|
2022-12-18 20:55:48 +01:00
|
|
|
from loguru import logger
|
2022-06-04 14:10:44 +02:00
|
|
|
from starlette import status
|
|
|
|
|
|
2022-10-03 20:22:00 +02:00
|
|
|
import app.lightning.impl.protos.cln.node_pb2 as ln
|
|
|
|
|
import app.lightning.impl.protos.cln.node_pb2_grpc as clnrpc
|
|
|
|
|
import app.lightning.impl.protos.cln.primitives_pb2 as lnp
|
2025-02-16 20:35:21 +01:00
|
|
|
from app.api.config import config
|
2026-07-11 11:29:55 +02:00
|
|
|
from app.api.utils import Event, broadcast_msg, config_get_hex_str, next_push_id
|
2022-10-03 20:22:00 +02:00
|
|
|
from app.bitcoind.utils import bitcoin_rpc_async
|
2023-05-01 20:48:10 +02:00
|
|
|
from app.lightning.exceptions import NodeNotFoundError
|
2023-04-03 19:15:55 +02:00
|
|
|
from app.lightning.impl.cln_utils import cln_classify_fee_revenue, parse_cln_msat
|
2022-10-03 20:22:00 +02:00
|
|
|
from app.lightning.impl.ln_base import LightningNodeBase
|
|
|
|
|
from app.lightning.models import (
|
2022-06-04 14:10:44 +02:00
|
|
|
Channel,
|
|
|
|
|
FeeRevenue,
|
|
|
|
|
ForwardSuccessEvent,
|
|
|
|
|
GenericTx,
|
refactor: improve startup procedure
During startup the API will try to connect to Bitcoin Core and the
Lightning Node. If it can't connect it will check every "n" seconds
(currently 2s) and connect when available. A new SSE event
called "system_startup_info" is introduced. This event contains
all startup status information during the startup procedure.
The old wallet_locked event is obsolete.
Sample:
--------------------------
event: system_startup_info
data: {"bitcoin": "offline", "bitcoin_msg": "", "lightning": "offline", "lightning_msg": "Unable to connect to LND daemon, waiting..."}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "offline", "lightning_msg": "Unable to connect to LND daemon, waiting..."}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "locked", "lightning_msg": "Wallet locked, unlock it to enable full RPC access"}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "done", "lightning_msg": ""}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "bootstraping", "lightning_msg": "RPC not yet available"}
--------------------------
refs #97
2022-06-06 19:29:21 +02:00
|
|
|
InitLnRepoUpdate,
|
2022-06-04 14:10:44 +02:00
|
|
|
Invoice,
|
|
|
|
|
InvoiceState,
|
|
|
|
|
LnInfo,
|
2022-06-08 19:25:45 +02:00
|
|
|
LnInitState,
|
2022-06-04 14:10:44 +02:00
|
|
|
NewAddressInput,
|
|
|
|
|
OnChainTransaction,
|
|
|
|
|
Payment,
|
|
|
|
|
PaymentRequest,
|
|
|
|
|
SendCoinsInput,
|
|
|
|
|
SendCoinsResponse,
|
|
|
|
|
TxStatus,
|
|
|
|
|
WalletBalance,
|
|
|
|
|
)
|
2026-07-11 12:43:31 +02:00
|
|
|
from app.lightning.utils import (
|
|
|
|
|
alias_or_empty,
|
|
|
|
|
generic_grpc_error_handler,
|
|
|
|
|
raise_for_pay_req_decode_error,
|
|
|
|
|
)
|
2022-06-04 14:10:44 +02:00
|
|
|
|
|
|
|
|
|
2023-04-03 20:17:48 +02:00
|
|
|
@logger.catch(exclude=(HTTPException,))
|
2026-07-03 19:58:41 +02:00
|
|
|
async def _make_local_call(*args: str):
|
2022-06-04 14:10:44 +02:00
|
|
|
# FIXME: this is a hack because some of the commands are not exposed
|
|
|
|
|
# in the CLN grpc interface yet.
|
|
|
|
|
|
2026-07-03 19:58:41 +02:00
|
|
|
# Pass the command as a discrete argv list (create_subprocess_exec, not
|
|
|
|
|
# _shell) so user-controlled arguments such as the bolt11 in decodepay
|
|
|
|
|
# can never be interpreted as shell syntax.
|
2025-02-16 20:35:21 +01:00
|
|
|
testnet = config("BAPI_NETWORK") == "testnet"
|
2026-07-03 19:58:41 +02:00
|
|
|
argv = ["lightning-cli", "-k", *(["--testnet"] if testnet else []), *args]
|
|
|
|
|
proc = await asyncio.create_subprocess_exec(
|
|
|
|
|
*argv,
|
2022-06-04 14:10:44 +02:00
|
|
|
stdout=asyncio.subprocess.PIPE,
|
|
|
|
|
stderr=asyncio.subprocess.PIPE,
|
|
|
|
|
)
|
2022-07-23 16:10:48 +02:00
|
|
|
stdout, stderr = await proc.communicate()
|
|
|
|
|
|
2023-05-17 16:02:28 +02:00
|
|
|
if stderr is not None and stderr != b"":
|
2022-07-31 18:03:41 +02:00
|
|
|
err = stderr.decode()
|
|
|
|
|
if "lightning-cli: Connecting to 'lightning-rpc': Permission denied" in err:
|
2022-12-18 20:55:48 +01:00
|
|
|
logger.critical(
|
2023-05-17 16:02:28 +02:00
|
|
|
(
|
|
|
|
|
"Unable to connect to lightning-cli: Permission denied. "
|
|
|
|
|
"Is the lightning-rpc socket readable for the API user?"
|
|
|
|
|
)
|
2022-07-31 18:03:41 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status.HTTP_500_INTERNAL_SERVER_ERROR,
|
2023-04-03 20:17:48 +02:00
|
|
|
detail="Unable to connect to lightning-cli: Permission denied.",
|
2022-07-31 18:03:41 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if "lightning-cli: Moving into" in err and "No such file or directory" in err:
|
2022-12-18 20:55:48 +01:00
|
|
|
logger.critical(
|
2023-05-17 16:02:28 +02:00
|
|
|
(
|
|
|
|
|
"Unable to connect to lightning-cli: No such file or directory. "
|
|
|
|
|
"Is the lightning-rpc socket available to the API user?"
|
|
|
|
|
)
|
2022-07-31 18:03:41 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status.HTTP_500_INTERNAL_SERVER_ERROR,
|
2023-05-17 16:02:28 +02:00
|
|
|
detail=(
|
|
|
|
|
"Unable to connect to lightning-cli: "
|
|
|
|
|
"API Can't access lightning-cli.",
|
|
|
|
|
),
|
2022-07-31 18:03:41 +02:00
|
|
|
)
|
|
|
|
|
|
2023-04-03 20:17:48 +02:00
|
|
|
logger.critical(f"Unable to connect to lightning-cli: {err}")
|
2022-07-23 16:10:48 +02:00
|
|
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status.HTTP_500_INTERNAL_SERVER_ERROR,
|
2023-05-17 16:02:28 +02:00
|
|
|
detail=(
|
|
|
|
|
"Unable to connect to lightning-cli: "
|
|
|
|
|
"Unknown error. Please consult the logs."
|
|
|
|
|
),
|
2022-07-23 16:10:48 +02:00
|
|
|
)
|
|
|
|
|
return stdout, stderr
|
2022-06-04 14:10:44 +02:00
|
|
|
|
|
|
|
|
|
2023-04-03 20:17:48 +02:00
|
|
|
@logger.catch(exclude=(HTTPException,))
|
2023-03-13 21:51:38 +01:00
|
|
|
def _extract_message(details):
|
|
|
|
|
return details.split('message: "')[1].replace('" }', ".")
|
|
|
|
|
|
|
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
class LnNodeCLNgRPC(LightningNodeBase):
|
|
|
|
|
_initialized = False
|
|
|
|
|
_channel = None
|
|
|
|
|
_cln_stub: clnrpc.NodeStub = None
|
|
|
|
|
# Decoding the payment request take a long time,
|
|
|
|
|
# hence we build a simple cache here.
|
|
|
|
|
_memo_cache = {}
|
|
|
|
|
_block_cache = {}
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
def get_implementation_name(self) -> str:
|
|
|
|
|
return "CLN_GRPC"
|
2022-06-26 11:06:56 +02:00
|
|
|
|
2023-04-03 20:17:48 +02:00
|
|
|
@logger.catch(exclude=(HTTPException,))
|
2022-10-03 10:44:34 +02:00
|
|
|
async def initialize(self) -> AsyncGenerator[InitLnRepoUpdate, None]:
|
2023-04-03 20:17:48 +02:00
|
|
|
logger.info("Establishing a connection to the CLN daemon ...")
|
2022-10-03 10:44:34 +02:00
|
|
|
if self._initialized:
|
2022-12-18 20:55:48 +01:00
|
|
|
logger.warning(
|
2023-05-17 16:02:28 +02:00
|
|
|
(
|
|
|
|
|
"Connection already initialized. "
|
|
|
|
|
"This function must not be called twice."
|
|
|
|
|
)
|
2022-10-03 10:44:34 +02:00
|
|
|
)
|
|
|
|
|
yield InitLnRepoUpdate(state=LnInitState.DONE)
|
2022-06-08 19:25:45 +02:00
|
|
|
|
2022-12-18 20:55:48 +01:00
|
|
|
try:
|
|
|
|
|
cln_grpc_key = bytes.fromhex(
|
2025-02-16 20:35:21 +01:00
|
|
|
config_get_hex_str(
|
|
|
|
|
str(config("BAPI_CLN_GRPC_KEY")), name="cln_grpc_key"
|
|
|
|
|
)
|
2022-12-18 20:55:48 +01:00
|
|
|
)
|
|
|
|
|
cln_grpc_cert = bytes.fromhex(
|
2025-02-16 20:35:21 +01:00
|
|
|
config_get_hex_str(
|
|
|
|
|
str(config("BAPI_CLN_GRPC_CERT")), name="cln_grpc_cert"
|
|
|
|
|
)
|
2022-12-18 20:55:48 +01:00
|
|
|
)
|
|
|
|
|
cln_grpc_ca = bytes.fromhex(
|
2025-02-16 20:35:21 +01:00
|
|
|
config_get_hex_str(str(config("BAPI_CLN_GRPC_CA")), name="cln_grpc_ca")
|
|
|
|
|
)
|
|
|
|
|
cln_grpc_url = (
|
|
|
|
|
str(config("BAPI_CLN_GRPC_IP"))
|
|
|
|
|
+ ":"
|
|
|
|
|
+ str(config("BAPI_CLN_GRPC_PORT"))
|
2022-12-18 20:55:48 +01:00
|
|
|
)
|
|
|
|
|
except ValueError as e:
|
2025-02-16 20:35:21 +01:00
|
|
|
logger.critical(f"Unable to decode BAPI_CLN_GRPC_CERT: {e.args}.")
|
2022-12-18 20:55:48 +01:00
|
|
|
sys.exit(0)
|
|
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
self.creds = grpc.ssl_channel_credentials(
|
|
|
|
|
root_certificates=cln_grpc_ca,
|
|
|
|
|
private_key=cln_grpc_key,
|
|
|
|
|
certificate_chain=cln_grpc_cert,
|
2022-06-08 19:25:45 +02:00
|
|
|
)
|
|
|
|
|
|
2023-04-03 18:49:48 +02:00
|
|
|
opts = (
|
|
|
|
|
("grpc.ssl_target_name_override", "cln"),
|
|
|
|
|
("grpc.max_receive_message_length", 1024 * 1024 * 10),
|
|
|
|
|
)
|
2022-10-03 10:44:34 +02:00
|
|
|
|
|
|
|
|
while not self._initialized:
|
2023-04-03 20:17:48 +02:00
|
|
|
logger.trace("iterating ...")
|
2022-10-03 10:44:34 +02:00
|
|
|
try:
|
|
|
|
|
if self._channel is None:
|
|
|
|
|
self._channel = grpc.aio.secure_channel(
|
|
|
|
|
cln_grpc_url, self.creds, options=opts
|
|
|
|
|
)
|
|
|
|
|
self._cln_stub = clnrpc.NodeStub(self._channel)
|
|
|
|
|
|
|
|
|
|
await self._cln_stub.Getinfo(ln.GetinfoRequest())
|
|
|
|
|
self._initialized = True
|
|
|
|
|
yield InitLnRepoUpdate(state=LnInitState.DONE)
|
|
|
|
|
except grpc.aio._call.AioRpcError as error:
|
|
|
|
|
details = error.details()
|
2023-04-03 20:17:48 +02:00
|
|
|
logger.debug(f"Waiting for CLN daemon... Details {details}")
|
2022-10-03 10:44:34 +02:00
|
|
|
|
|
|
|
|
if "failed to connect to all addresses" in details:
|
|
|
|
|
yield InitLnRepoUpdate(
|
|
|
|
|
state=LnInitState.OFFLINE,
|
|
|
|
|
msg="Unable to connect to CLN daemon, waiting...",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await self._channel.close()
|
|
|
|
|
self._channel = self.cln_stub = None
|
|
|
|
|
else:
|
2023-04-03 20:17:48 +02:00
|
|
|
logger.error(f"Unknown error: {details}")
|
2022-10-03 10:44:34 +02:00
|
|
|
raise
|
2022-06-08 19:25:45 +02:00
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
await asyncio.sleep(2)
|
2022-12-18 20:55:48 +01:00
|
|
|
except Exception as e:
|
2023-04-03 20:17:48 +02:00
|
|
|
logger.error(f"Unknown error: {e}")
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2023-04-03 20:17:48 +02:00
|
|
|
logger.success("Initialization complete.")
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2023-04-03 20:17:48 +02:00
|
|
|
@logger.catch(exclude=(HTTPException,))
|
2022-10-03 10:44:34 +02:00
|
|
|
async def get_wallet_balance(self) -> WalletBalance:
|
2023-04-03 20:17:48 +02:00
|
|
|
logger.trace("get_wallet_balance() ")
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
req = ln.ListfundsRequest()
|
|
|
|
|
res = await self._cln_stub.ListFunds(req)
|
|
|
|
|
onchain_confirmed = onchain_unconfirmed = onchain_total = 0
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
for o in res.outputs:
|
|
|
|
|
sat = o.amount_msat.msat / 1000
|
2023-04-09 18:21:20 +02:00
|
|
|
if o.status == 0: # unconfirmed
|
2022-10-03 10:44:34 +02:00
|
|
|
onchain_unconfirmed += sat
|
2023-04-09 18:21:20 +02:00
|
|
|
elif o.status == 1 and not o.reserved: # confirmed
|
2022-10-03 10:44:34 +02:00
|
|
|
onchain_confirmed += sat
|
|
|
|
|
# 2 is spent => ignore
|
2023-04-09 18:21:20 +02:00
|
|
|
# 3 is immature => not sure what to do with this
|
|
|
|
|
|
|
|
|
|
onchain_total = onchain_confirmed + onchain_unconfirmed
|
2022-06-26 11:06:56 +02:00
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
chan_local = chan_remote = chan_pending_local = chan_pending_remote = 0
|
|
|
|
|
for c in res.channels:
|
|
|
|
|
our_msat = c.our_amount_msat.msat
|
|
|
|
|
their_msat = c.amount_msat.msat - our_msat
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
if c.state == 2: # ChanneldNormal
|
|
|
|
|
chan_local += our_msat
|
|
|
|
|
chan_remote += their_msat
|
|
|
|
|
else:
|
|
|
|
|
# treat everything else as pending for now
|
|
|
|
|
chan_pending_local += our_msat
|
|
|
|
|
chan_pending_remote += their_msat
|
|
|
|
|
|
|
|
|
|
return WalletBalance(
|
|
|
|
|
onchain_confirmed_balance=onchain_confirmed,
|
|
|
|
|
onchain_total_balance=onchain_total,
|
|
|
|
|
onchain_unconfirmed_balance=onchain_unconfirmed,
|
|
|
|
|
channel_local_balance=chan_local,
|
|
|
|
|
channel_remote_balance=chan_remote,
|
|
|
|
|
# TODO: find out how to get these values with CLN
|
|
|
|
|
channel_unsettled_local_balance=0,
|
|
|
|
|
channel_unsettled_remote_balance=0,
|
|
|
|
|
channel_pending_open_local_balance=chan_pending_local,
|
|
|
|
|
channel_pending_open_remote_balance=chan_pending_remote,
|
|
|
|
|
)
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2023-04-03 20:17:48 +02:00
|
|
|
@logger.catch(exclude=(HTTPException,))
|
2022-10-03 10:44:34 +02:00
|
|
|
async def _get_block_time(self, block_height: int) -> tuple:
|
2023-04-03 20:17:48 +02:00
|
|
|
logger.trace(f"_get_block_time(block_height={block_height}) ")
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
if block_height is None or block_height < 0:
|
|
|
|
|
raise ValueError("block_height cannot be None or negative")
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
if block_height in self._block_cache:
|
|
|
|
|
return self._block_cache[block_height]
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
res = await bitcoin_rpc_async("getblockstats", params=[block_height])
|
|
|
|
|
hash = res["result"]["blockhash"]
|
|
|
|
|
block = await bitcoin_rpc_async("getblock", params=[hash])
|
|
|
|
|
self._block_cache[block_height] = (
|
|
|
|
|
block["result"]["time"],
|
|
|
|
|
block["result"]["mediantime"],
|
|
|
|
|
)
|
|
|
|
|
return self._block_cache[block_height]
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2023-04-03 20:17:48 +02:00
|
|
|
@logger.catch(exclude=(HTTPException,))
|
2022-10-03 10:44:34 +02:00
|
|
|
async def list_all_tx(
|
|
|
|
|
self, successful_only: bool, index_offset: int, max_tx: int, reversed: bool
|
|
|
|
|
) -> List[GenericTx]:
|
2023-04-03 20:17:48 +02:00
|
|
|
logger.trace(
|
2023-05-17 16:02:28 +02:00
|
|
|
(
|
|
|
|
|
f"list_all_tx(successful_only={successful_only}, "
|
|
|
|
|
f"index_offset={index_offset}, max_tx={max_tx}, reversed={reversed})"
|
|
|
|
|
)
|
2022-10-03 10:44:34 +02:00
|
|
|
)
|
2022-06-26 11:06:56 +02:00
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
list_invoice_req = ln.ListinvoicesRequest()
|
|
|
|
|
list_payments_req = ln.ListpaysRequest()
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
try:
|
|
|
|
|
res = await asyncio.gather(
|
|
|
|
|
*[
|
|
|
|
|
self._cln_stub.ListInvoices(list_invoice_req),
|
|
|
|
|
self.list_on_chain_tx(),
|
|
|
|
|
self._cln_stub.ListPays(list_payments_req),
|
|
|
|
|
self.get_ln_info(),
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
tx = []
|
|
|
|
|
for invoice in res[0].invoices:
|
|
|
|
|
i = GenericTx.from_cln_grpc_invoice(invoice)
|
|
|
|
|
if successful_only and i.status == TxStatus.SUCCEEDED:
|
|
|
|
|
tx.append(i)
|
|
|
|
|
continue
|
2022-06-04 14:10:44 +02:00
|
|
|
tx.append(i)
|
|
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
for transaction in res[1]:
|
2023-04-03 19:15:55 +02:00
|
|
|
t = GenericTx.from_onchain_tx(transaction, res[3].block_height)
|
2022-10-03 10:44:34 +02:00
|
|
|
if successful_only and t.status == TxStatus.SUCCEEDED:
|
|
|
|
|
tx.append(t)
|
|
|
|
|
continue
|
|
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
tx.append(t)
|
|
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
for pay in res[2].pays:
|
2023-04-03 19:15:55 +02:00
|
|
|
decoded_bolt11: PaymentRequest = None
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
if pay.bolt11 is not None and len(pay.bolt11) > 0:
|
|
|
|
|
if pay.bolt11 in self._memo_cache:
|
2023-04-03 19:15:55 +02:00
|
|
|
decoded_bolt11 = self._memo_cache[pay.bolt11]
|
2022-10-03 10:44:34 +02:00
|
|
|
else:
|
2023-04-03 19:15:55 +02:00
|
|
|
decoded_bolt11 = await self.decode_pay_request(pay.bolt11)
|
|
|
|
|
self._memo_cache[pay.bolt11] = decoded_bolt11
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2023-04-03 19:15:55 +02:00
|
|
|
p = GenericTx.from_cln_grpc_payment(
|
|
|
|
|
pay, decoded_bolt11.description, decoded_bolt11.num_msat
|
|
|
|
|
)
|
2022-07-01 21:12:54 +02:00
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
if successful_only and p.status == TxStatus.SUCCEEDED:
|
|
|
|
|
tx.append(p)
|
|
|
|
|
continue
|
2022-06-04 14:10:44 +02:00
|
|
|
|
|
|
|
|
tx.append(p)
|
|
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
def sortKey(e: GenericTx):
|
|
|
|
|
return e.time_stamp
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
tx.sort(key=sortKey)
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
if reversed:
|
|
|
|
|
tx.reverse()
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2023-05-17 16:02:28 +02:00
|
|
|
tx_length = len(tx)
|
|
|
|
|
for invoice in range(tx_length):
|
2022-10-03 10:44:34 +02:00
|
|
|
tx[invoice].index = invoice
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
if max_tx == 0:
|
2023-05-17 16:02:28 +02:00
|
|
|
max_tx = tx_length
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
return tx[index_offset : index_offset + max_tx]
|
|
|
|
|
except grpc.aio._call.AioRpcError as error:
|
2023-04-03 20:17:48 +02:00
|
|
|
generic_grpc_error_handler(error)
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2023-04-03 20:17:48 +02:00
|
|
|
@logger.catch(exclude=(HTTPException,))
|
2022-10-03 10:44:34 +02:00
|
|
|
async def list_invoices(
|
|
|
|
|
self,
|
|
|
|
|
pending_only: bool,
|
|
|
|
|
index_offset: int,
|
|
|
|
|
num_max_invoices: int,
|
|
|
|
|
reversed: bool,
|
|
|
|
|
) -> List[Invoice]:
|
2023-04-03 20:17:48 +02:00
|
|
|
logger.trace("list_invoices() ")
|
2022-06-26 11:06:56 +02:00
|
|
|
|
2023-04-03 19:15:55 +02:00
|
|
|
try:
|
|
|
|
|
req = ln.ListinvoicesRequest()
|
|
|
|
|
res = await self._cln_stub.ListInvoices(req)
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2023-04-03 19:15:55 +02:00
|
|
|
tx = []
|
|
|
|
|
for i in res.invoices:
|
|
|
|
|
if pending_only:
|
|
|
|
|
if i.status == 0:
|
|
|
|
|
tx.append(Invoice.from_cln_grpc(i))
|
|
|
|
|
else:
|
2022-10-03 10:44:34 +02:00
|
|
|
tx.append(Invoice.from_cln_grpc(i))
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2023-04-03 19:15:55 +02:00
|
|
|
if reversed:
|
|
|
|
|
tx.reverse()
|
2022-07-31 18:01:02 +02:00
|
|
|
|
2023-04-03 19:15:55 +02:00
|
|
|
if num_max_invoices == 0 or num_max_invoices is None:
|
|
|
|
|
return tx
|
2022-10-03 10:44:34 +02:00
|
|
|
|
2023-04-03 19:15:55 +02:00
|
|
|
return tx[index_offset : index_offset + num_max_invoices]
|
|
|
|
|
|
|
|
|
|
except grpc.aio._call.AioRpcError as error:
|
2023-04-03 20:17:48 +02:00
|
|
|
generic_grpc_error_handler(error)
|
2022-10-03 10:44:34 +02:00
|
|
|
|
2023-04-03 20:17:48 +02:00
|
|
|
@logger.catch(exclude=(HTTPException,))
|
2022-10-03 10:44:34 +02:00
|
|
|
async def list_on_chain_tx(self) -> List[OnChainTransaction]:
|
2023-04-03 20:17:48 +02:00
|
|
|
logger.trace("list_on_chain_tx() ")
|
2022-11-05 22:35:01 +01:00
|
|
|
info = await self.get_ln_info() # for current block height
|
|
|
|
|
res = await _make_local_call("bkpr-listincome")
|
2022-10-03 10:44:34 +02:00
|
|
|
|
2022-11-05 22:35:01 +01:00
|
|
|
if not res:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
|
|
|
detail="Unknown CLN error while listing account income events",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if len(res) == 0:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status.HTTP_500_INTERNAL_SERVER_ERROR,
|
2023-05-17 16:02:28 +02:00
|
|
|
detail=(
|
|
|
|
|
"No response from CLN while trying to list account income events"
|
|
|
|
|
),
|
2022-06-04 14:10:44 +02:00
|
|
|
)
|
|
|
|
|
|
2022-11-05 22:35:01 +01:00
|
|
|
decoded = res[0].decode()
|
|
|
|
|
js = json.loads(decoded)
|
|
|
|
|
|
|
|
|
|
txs = {}
|
|
|
|
|
num_events = len(js["income_events"])
|
|
|
|
|
for i in range(0, num_events):
|
|
|
|
|
e = js["income_events"][i]
|
|
|
|
|
if e["account"] != "wallet":
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
if e["tag"] == "deposit" or e["tag"] == "withdrawal":
|
|
|
|
|
tx = OnChainTransaction.from_cln_bkpr(e)
|
|
|
|
|
txs[tx.tx_hash] = tx
|
|
|
|
|
elif e["tag"] == "onchain_fee":
|
|
|
|
|
if e["txid"] in txs:
|
|
|
|
|
txs[e["txid"]].total_fees = parse_cln_msat(e["debit_msat"]) / 1000
|
|
|
|
|
|
|
|
|
|
# TODO: Improve this once CLN reports the block height in bkpr-listincome
|
|
|
|
|
# see https://github.com/ElementsProject/lightning/issues/5694
|
2022-11-28 19:07:25 +01:00
|
|
|
|
2022-11-05 22:35:01 +01:00
|
|
|
# now get the block height for each tx ...
|
|
|
|
|
res = await _make_local_call("bkpr-listaccountevents")
|
|
|
|
|
if not res:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
|
|
|
detail="Unknown CLN error while listing account events",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if len(res) == 0:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
|
|
|
detail="No response from CLN while trying to list account events",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
decoded = res[0].decode()
|
|
|
|
|
js = json.loads(decoded)
|
|
|
|
|
num_events = len(js["events"])
|
|
|
|
|
for i in range(0, num_events):
|
|
|
|
|
e = js["events"][i]
|
|
|
|
|
if e["account"] != "wallet" or e["type"] != "chain":
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
txid = ""
|
|
|
|
|
if e["tag"] == "deposit":
|
|
|
|
|
txid = e["outpoint"].split(":")[0]
|
|
|
|
|
elif e["tag"] == "withdrawal":
|
|
|
|
|
txid = e["txid"]
|
|
|
|
|
|
|
|
|
|
if len(txid) == 0:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
if txid in txs:
|
|
|
|
|
txs[txid].block_height = e["blockheight"]
|
|
|
|
|
txs[txid].num_confirmations = info.block_height - txs[txid].block_height
|
|
|
|
|
|
|
|
|
|
return [txs[k] for k in txs.keys()]
|
2022-10-03 10:44:34 +02:00
|
|
|
|
2023-04-03 20:17:48 +02:00
|
|
|
@logger.catch(exclude=(HTTPException,))
|
2022-10-03 10:44:34 +02:00
|
|
|
async def list_payments(
|
|
|
|
|
self,
|
|
|
|
|
include_incomplete: bool,
|
|
|
|
|
index_offset: int,
|
|
|
|
|
max_payments: int,
|
|
|
|
|
reversed: bool,
|
|
|
|
|
):
|
2023-04-03 20:17:48 +02:00
|
|
|
logger.trace(
|
2023-05-17 16:02:28 +02:00
|
|
|
(
|
|
|
|
|
f"list_payments(include_incomplete={include_incomplete}, "
|
|
|
|
|
f"index_offset{index_offset}, max_payments={max_payments}, "
|
|
|
|
|
f"reversed={reversed})"
|
|
|
|
|
)
|
2022-10-03 10:44:34 +02:00
|
|
|
)
|
2023-04-03 19:15:55 +02:00
|
|
|
try:
|
|
|
|
|
req = ln.ListpaysRequest()
|
|
|
|
|
res = await self._cln_stub.ListPays(req)
|
|
|
|
|
|
|
|
|
|
pays = []
|
|
|
|
|
for p in res.pays:
|
|
|
|
|
if p.status == 2:
|
|
|
|
|
# always include completed payments
|
|
|
|
|
pays.append(Payment.from_cln_grpc(p))
|
|
|
|
|
continue
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2023-04-03 19:15:55 +02:00
|
|
|
if include_incomplete:
|
|
|
|
|
pays.append(Payment.from_cln_grpc(p))
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2023-04-03 19:15:55 +02:00
|
|
|
if reversed:
|
|
|
|
|
pays.reverse()
|
2022-10-03 10:44:34 +02:00
|
|
|
|
2023-04-03 19:15:55 +02:00
|
|
|
if max_payments == 0 or max_payments is None:
|
|
|
|
|
return pays
|
2022-10-03 10:44:34 +02:00
|
|
|
|
2023-04-03 19:15:55 +02:00
|
|
|
return pays[index_offset : index_offset + max_payments]
|
|
|
|
|
except grpc.aio._call.AioRpcError as error:
|
2023-04-03 20:17:48 +02:00
|
|
|
generic_grpc_error_handler(error)
|
2022-10-03 10:44:34 +02:00
|
|
|
|
2023-04-03 20:17:48 +02:00
|
|
|
@logger.catch(exclude=(HTTPException,))
|
2022-10-03 10:44:34 +02:00
|
|
|
async def add_invoice(
|
|
|
|
|
self,
|
|
|
|
|
value_msat: int,
|
|
|
|
|
memo: str = "",
|
|
|
|
|
expiry: int = 3600,
|
|
|
|
|
is_keysend: bool = False,
|
|
|
|
|
) -> Invoice:
|
2023-04-03 20:17:48 +02:00
|
|
|
logger.trace(
|
2023-05-17 16:02:28 +02:00
|
|
|
(
|
|
|
|
|
f"add_invoice(value_msat={value_msat}, memo={memo}, "
|
|
|
|
|
f"expiry={expiry}, is_keysend={is_keysend})"
|
|
|
|
|
)
|
2022-06-04 14:10:44 +02:00
|
|
|
)
|
|
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
if value_msat < 0:
|
|
|
|
|
raise ValueError("value_msat cannot be negative")
|
|
|
|
|
|
|
|
|
|
msat = None
|
|
|
|
|
if value_msat == 0:
|
|
|
|
|
msat = lnp.AmountOrAny(any=True)
|
|
|
|
|
elif value_msat > 0:
|
|
|
|
|
msat = lnp.AmountOrAny(amount=lnp.Amount(msat=value_msat))
|
|
|
|
|
|
|
|
|
|
id = next_push_id()
|
|
|
|
|
req = ln.InvoiceRequest(
|
2022-12-01 19:08:29 +01:00
|
|
|
amount_msat=msat,
|
2022-10-03 10:44:34 +02:00
|
|
|
description=memo,
|
|
|
|
|
label=id,
|
|
|
|
|
expiry=expiry,
|
2022-06-04 14:10:44 +02:00
|
|
|
)
|
|
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
try:
|
|
|
|
|
res = await self._cln_stub.Invoice(req)
|
2022-12-01 19:08:29 +01:00
|
|
|
return Invoice(
|
|
|
|
|
payment_request=res.bolt11,
|
|
|
|
|
memo=memo,
|
|
|
|
|
value_msat=value_msat,
|
|
|
|
|
expiry_date=res.expires_at,
|
|
|
|
|
add_index=id,
|
|
|
|
|
state=InvoiceState.OPEN,
|
|
|
|
|
)
|
|
|
|
|
except grpc.aio._call.AioRpcError as error:
|
|
|
|
|
details = error.details()
|
2023-04-03 20:17:48 +02:00
|
|
|
logger.debug(details)
|
2022-12-01 19:08:29 +01:00
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
self._handle_base_cln_error(error)
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
|
|
|
detail=f"Unknown CLN error while adding invoice: {details}",
|
|
|
|
|
)
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2023-04-03 20:17:48 +02:00
|
|
|
@logger.catch(exclude=(HTTPException,))
|
2022-10-03 10:44:34 +02:00
|
|
|
async def decode_pay_request(self, pay_req: str) -> PaymentRequest:
|
2023-04-03 20:17:48 +02:00
|
|
|
logger.trace(f"decode_pay_request(pay_req={pay_req})")
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2026-07-03 19:58:41 +02:00
|
|
|
res = await _make_local_call("decodepay", f"bolt11={pay_req}")
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2023-05-17 16:02:28 +02:00
|
|
|
if not res:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
|
|
|
detail="Unknown CLN error decoding pay request",
|
|
|
|
|
)
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2023-05-17 16:02:28 +02:00
|
|
|
if len(res) == 0:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
|
|
|
detail="No response from CLN decoding pay request",
|
|
|
|
|
)
|
2022-10-03 10:44:34 +02:00
|
|
|
|
2023-05-17 16:02:28 +02:00
|
|
|
decoded = res[0].decode()
|
2022-10-03 10:44:34 +02:00
|
|
|
|
2026-07-11 12:43:31 +02:00
|
|
|
raise_for_pay_req_decode_error(decoded)
|
2023-05-17 16:02:28 +02:00
|
|
|
|
|
|
|
|
return PaymentRequest.from_cln_json(json.loads(decoded))
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2023-04-03 20:17:48 +02:00
|
|
|
@logger.catch(exclude=(HTTPException,))
|
2022-10-03 10:44:34 +02:00
|
|
|
async def get_fee_revenue(self) -> FeeRevenue:
|
2023-05-17 16:02:28 +02:00
|
|
|
logger.trace("get_fee_revenue()")
|
2023-04-03 19:15:55 +02:00
|
|
|
try:
|
|
|
|
|
# status 1 == "settled"
|
|
|
|
|
req = ln.ListforwardsRequest(status=1)
|
|
|
|
|
res = await self._cln_stub.ListForwards(req)
|
|
|
|
|
day, week, month, year, total = cln_classify_fee_revenue(res.forwards)
|
2022-10-03 10:44:34 +02:00
|
|
|
|
2023-04-03 19:15:55 +02:00
|
|
|
return FeeRevenue(day=day, week=week, month=month, year=year, total=total)
|
|
|
|
|
except grpc.aio._call.AioRpcError as error:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status.HTTP_500_INTERNAL_SERVER_ERROR, detail=error.details()
|
|
|
|
|
)
|
2022-10-03 10:44:34 +02:00
|
|
|
|
2023-04-03 20:17:48 +02:00
|
|
|
@logger.catch(exclude=(HTTPException,))
|
2022-10-03 10:44:34 +02:00
|
|
|
async def new_address(self, input: NewAddressInput) -> str:
|
2023-04-03 20:17:48 +02:00
|
|
|
logger.trace(f"new_address(input={input})")
|
2022-10-03 10:44:34 +02:00
|
|
|
|
2023-04-03 19:15:55 +02:00
|
|
|
try:
|
|
|
|
|
req = ln.NewaddrRequest()
|
|
|
|
|
res = await self._cln_stub.NewAddr(req)
|
|
|
|
|
|
|
|
|
|
return res.bech32
|
|
|
|
|
except grpc.aio._call.AioRpcError as error:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(error)
|
|
|
|
|
)
|
2022-10-03 10:44:34 +02:00
|
|
|
|
2023-04-03 20:17:48 +02:00
|
|
|
return res.bech32
|
|
|
|
|
except grpc.aio._call.AioRpcError as error:
|
|
|
|
|
generic_grpc_error_handler(error)
|
|
|
|
|
|
|
|
|
|
@logger.catch(exclude=(HTTPException,))
|
2022-10-03 10:44:34 +02:00
|
|
|
async def send_coins(self, input: SendCoinsInput) -> SendCoinsResponse:
|
2023-04-03 20:17:48 +02:00
|
|
|
logger.trace(f"send_coins(input={input})")
|
2022-10-03 10:44:34 +02:00
|
|
|
|
|
|
|
|
fee_rate: lnp.Feerate = None
|
2023-05-17 16:02:28 +02:00
|
|
|
if input.sat_per_vbyte is not None and input.sat_per_vbyte > 0:
|
2022-10-03 10:44:34 +02:00
|
|
|
fee_rate = lnp.Feerate(perkw=input.sat_per_vbyte)
|
2023-05-17 16:02:28 +02:00
|
|
|
elif input.target_conf is not None and input.target_conf == 1:
|
2022-10-03 10:44:34 +02:00
|
|
|
fee_rate = lnp.Feerate(urgent=True)
|
2023-05-17 16:02:28 +02:00
|
|
|
elif input.target_conf is not None and input.target_conf >= 2:
|
2022-10-03 10:44:34 +02:00
|
|
|
fee_rate = lnp.Feerate(normal=True)
|
2023-05-17 16:02:28 +02:00
|
|
|
elif input.target_conf is not None and input.target_conf >= 10:
|
2022-10-03 10:44:34 +02:00
|
|
|
fee_rate = lnp.Feerate(slow=True)
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
try:
|
|
|
|
|
funds = await self._cln_stub.ListFunds(ln.ListfundsRequest())
|
|
|
|
|
if len(funds.outputs) == 0:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status.HTTP_412_PRECONDITION_FAILED,
|
2023-05-17 16:02:28 +02:00
|
|
|
detail=(
|
|
|
|
|
f"Could not afford {input.amount}sat. No UTXOs available at all"
|
|
|
|
|
),
|
2022-10-03 10:44:34 +02:00
|
|
|
)
|
2022-06-26 11:06:56 +02:00
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
utxos = []
|
|
|
|
|
max_amt = 0
|
|
|
|
|
for o in funds.outputs:
|
|
|
|
|
utxos.append(lnp.Outpoint(txid=o.txid, outnum=o.output))
|
2023-05-01 13:28:58 +02:00
|
|
|
max_amt += o.amount_msat.msat / 1000
|
2022-07-04 21:06:11 +02:00
|
|
|
|
2022-11-28 19:07:25 +01:00
|
|
|
if not input.send_all and max_amt <= input.amount:
|
2022-10-03 10:44:34 +02:00
|
|
|
raise HTTPException(
|
|
|
|
|
status.HTTP_412_PRECONDITION_FAILED,
|
2023-05-17 16:02:28 +02:00
|
|
|
detail=(
|
2025-03-25 09:48:40 +01:00
|
|
|
f"Could not afford {input.amount}sat. "
|
|
|
|
|
"Not enough funds available"
|
2023-05-17 16:02:28 +02:00
|
|
|
),
|
2022-10-03 10:44:34 +02:00
|
|
|
)
|
2022-07-04 21:06:11 +02:00
|
|
|
|
2023-05-01 13:28:58 +02:00
|
|
|
amt = lnp.AmountOrAll(amount=lnp.Amount(msat=input.amount * 1000))
|
|
|
|
|
if input.send_all:
|
|
|
|
|
amt = lnp.AmountOrAll(all=True)
|
|
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
req = ln.WithdrawRequest(
|
|
|
|
|
destination=input.address,
|
2023-05-01 13:28:58 +02:00
|
|
|
satoshi=amt,
|
2022-10-03 10:44:34 +02:00
|
|
|
minconf=input.min_confs,
|
|
|
|
|
feerate=fee_rate,
|
|
|
|
|
utxos=utxos,
|
2022-07-04 21:06:11 +02:00
|
|
|
)
|
2022-11-28 19:07:25 +01:00
|
|
|
response = await self._cln_stub.Withdraw(req)
|
|
|
|
|
r = SendCoinsResponse.from_cln_grpc(response, input)
|
2026-07-11 11:29:55 +02:00
|
|
|
await broadcast_msg(Event.LN_ONCHAIN_PAYMENT_STATUS, r.model_dump())
|
2022-11-28 19:07:25 +01:00
|
|
|
return r
|
2022-10-03 10:44:34 +02:00
|
|
|
except grpc.aio._call.AioRpcError as error:
|
|
|
|
|
details = error.details()
|
2023-04-03 20:17:48 +02:00
|
|
|
logger.debug(details)
|
|
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
if details and details.find("Could not parse destination address") > -1:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status.HTTP_400_BAD_REQUEST,
|
2023-05-17 16:02:28 +02:00
|
|
|
detail=(
|
|
|
|
|
"Could not parse destination address, "
|
|
|
|
|
" destination should be a valid address."
|
|
|
|
|
),
|
2022-10-03 10:44:34 +02:00
|
|
|
)
|
|
|
|
|
elif (
|
|
|
|
|
details
|
|
|
|
|
and details.find("UTXO") > -1
|
|
|
|
|
and details.find("already reserved") > -1
|
|
|
|
|
):
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status.HTTP_500_INTERNAL_SERVER_ERROR,
|
2023-05-17 16:02:28 +02:00
|
|
|
detail=(
|
|
|
|
|
"Server tried to use a reserved UTXO. "
|
|
|
|
|
"Please submit an issue to the BlitzAPI repository."
|
|
|
|
|
),
|
2022-10-03 10:44:34 +02:00
|
|
|
)
|
|
|
|
|
elif details and details.find("insufficient funds available") > -1:
|
|
|
|
|
raise HTTPException(status.HTTP_412_PRECONDITION_FAILED, detail=details)
|
|
|
|
|
else:
|
2023-04-03 20:17:48 +02:00
|
|
|
generic_grpc_error_handler(error)
|
2022-07-04 21:06:11 +02:00
|
|
|
|
2023-04-03 20:17:48 +02:00
|
|
|
@logger.catch(exclude=(HTTPException,))
|
2022-10-03 10:44:34 +02:00
|
|
|
async def send_payment(
|
|
|
|
|
self,
|
|
|
|
|
pay_req: str,
|
|
|
|
|
timeout_seconds: int,
|
|
|
|
|
fee_limit_msat: int,
|
|
|
|
|
amount_msat: Optional[int] = None,
|
|
|
|
|
) -> Payment:
|
2023-04-03 20:17:48 +02:00
|
|
|
logger.trace(
|
2023-05-17 16:02:28 +02:00
|
|
|
(
|
|
|
|
|
f"send_payment(pay_req={pay_req}, timeout_seconds={timeout_seconds}, "
|
|
|
|
|
f"fee_limit_msat={fee_limit_msat}, amount_msat={amount_msat})"
|
|
|
|
|
)
|
2022-10-03 10:44:34 +02:00
|
|
|
)
|
2022-07-04 21:06:11 +02:00
|
|
|
|
2023-05-17 16:02:28 +02:00
|
|
|
amt = lnp.Amount(msat=amount_msat) if amount_msat is not None else None
|
2022-10-03 10:44:34 +02:00
|
|
|
fee_limit = lnp.Amount(msat=fee_limit_msat)
|
|
|
|
|
req = ln.PayRequest(
|
|
|
|
|
bolt11=pay_req,
|
2022-12-01 19:08:29 +01:00
|
|
|
amount_msat=amt,
|
2022-10-03 10:44:34 +02:00
|
|
|
maxfee=fee_limit,
|
|
|
|
|
retry_for=timeout_seconds,
|
|
|
|
|
)
|
2022-07-04 21:06:11 +02:00
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
try:
|
|
|
|
|
res = await self._cln_stub.Pay(req)
|
|
|
|
|
except grpc.aio._call.AioRpcError as error:
|
|
|
|
|
details = error.details()
|
2023-04-03 20:17:48 +02:00
|
|
|
logger.debug(details)
|
2022-07-04 21:06:11 +02:00
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
if "Ran out of routes to try after" in details:
|
|
|
|
|
attempts = details.split("Ran out of routes to try after ")[1]
|
|
|
|
|
attempts = attempts.split(" attempts")[0]
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
|
|
|
detail=f"Ran out of routes to try after {attempts} attempts.",
|
|
|
|
|
)
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2022-12-01 19:08:29 +01:00
|
|
|
if "Invalid bolt11: " in details:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status.HTTP_400_BAD_REQUEST,
|
|
|
|
|
detail="invalid bech32 string",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if "amount_msat parameter required" in details:
|
2022-10-03 10:44:34 +02:00
|
|
|
raise HTTPException(
|
|
|
|
|
status.HTTP_400_BAD_REQUEST,
|
|
|
|
|
detail="amount must be specified when paying a zero amount invoice",
|
|
|
|
|
)
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2022-12-01 19:08:29 +01:00
|
|
|
if "amount_msat parameter unnecessary" in details:
|
2022-10-03 10:44:34 +02:00
|
|
|
raise HTTPException(
|
|
|
|
|
status.HTTP_400_BAD_REQUEST,
|
2023-05-17 16:02:28 +02:00
|
|
|
detail=(
|
2025-03-25 09:48:40 +01:00
|
|
|
"amount must not be specified when paying "
|
|
|
|
|
"a non-zero amount invoice"
|
2023-05-17 16:02:28 +02:00
|
|
|
),
|
2022-10-03 10:44:34 +02:00
|
|
|
)
|
2022-06-26 11:06:56 +02:00
|
|
|
|
2023-04-03 20:17:48 +02:00
|
|
|
generic_grpc_error_handler(error)
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
return Payment.from_cln_grpc(res)
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2023-04-03 20:17:48 +02:00
|
|
|
@logger.catch(exclude=(HTTPException,))
|
2022-10-03 10:44:34 +02:00
|
|
|
async def get_ln_info(self) -> LnInfo:
|
2023-05-17 16:02:28 +02:00
|
|
|
logger.trace("get_ln_info()")
|
2022-06-26 11:06:56 +02:00
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
req = ln.GetinfoRequest()
|
2022-12-01 19:08:29 +01:00
|
|
|
try:
|
|
|
|
|
res = await self._cln_stub.Getinfo(req)
|
|
|
|
|
return LnInfo.from_cln_grpc(self.get_implementation_name(), res)
|
|
|
|
|
except grpc.aio._call.AioRpcError as error:
|
|
|
|
|
details = error.details()
|
2023-04-03 20:17:48 +02:00
|
|
|
logger.debug(details)
|
2022-12-01 19:08:29 +01:00
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
self._handle_base_cln_error(error)
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status.HTTP_500_INTERNAL_SERVER_ERROR,
|
2023-04-03 20:17:48 +02:00
|
|
|
detail=f"Unknown CLN error while getting lightning info: {details}",
|
2022-12-01 19:08:29 +01:00
|
|
|
)
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2023-04-03 20:17:48 +02:00
|
|
|
@logger.catch(exclude=(HTTPException,))
|
2022-10-03 10:44:34 +02:00
|
|
|
async def unlock_wallet(self, password: str) -> bool:
|
2023-05-17 16:02:28 +02:00
|
|
|
logger.trace("unlock_wallet(password=wedontlogpasswords)")
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
# Core Lightning doesn't lock wallets,
|
|
|
|
|
# so we don't need to do anything here
|
|
|
|
|
return True
|
2022-06-26 11:06:56 +02:00
|
|
|
|
2023-04-03 20:17:48 +02:00
|
|
|
@logger.catch(exclude=(HTTPException,))
|
2022-10-03 10:44:34 +02:00
|
|
|
async def listen_invoices(self) -> AsyncGenerator[Invoice, None]:
|
2023-05-17 16:02:28 +02:00
|
|
|
logger.trace("listen_invoices()")
|
2023-04-03 19:15:55 +02:00
|
|
|
try:
|
|
|
|
|
lastpay_index = 0
|
|
|
|
|
invoices = await self.list_invoices(
|
|
|
|
|
pending_only=False,
|
|
|
|
|
index_offset=0,
|
|
|
|
|
num_max_invoices=9999999999999,
|
|
|
|
|
reversed=False,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
for i in invoices: # type Invoice
|
|
|
|
|
if i.state == InvoiceState.SETTLED and i.settle_index > lastpay_index:
|
|
|
|
|
lastpay_index = i.settle_index
|
2022-10-03 10:44:34 +02:00
|
|
|
|
2023-04-03 19:15:55 +02:00
|
|
|
while True:
|
|
|
|
|
req = ln.WaitanyinvoiceRequest(lastpay_index=lastpay_index)
|
|
|
|
|
i = await self._cln_stub.WaitAnyInvoice(req)
|
|
|
|
|
i = Invoice.from_cln_grpc(i)
|
2022-10-03 10:44:34 +02:00
|
|
|
lastpay_index = i.settle_index
|
2023-04-03 19:15:55 +02:00
|
|
|
yield i
|
|
|
|
|
except grpc.aio._call.AioRpcError as error:
|
|
|
|
|
details = error.details()
|
2023-04-03 20:17:48 +02:00
|
|
|
logger.debug(details)
|
2022-10-03 10:44:34 +02:00
|
|
|
|
2023-04-03 19:15:55 +02:00
|
|
|
try:
|
|
|
|
|
self._handle_base_cln_error(error)
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
|
|
|
detail=f"Unknown CLN error while listening for invoices: {details}",
|
|
|
|
|
)
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2023-04-03 20:17:48 +02:00
|
|
|
@logger.catch(exclude=(HTTPException,))
|
2022-10-03 10:44:34 +02:00
|
|
|
async def listen_forward_events(self) -> ForwardSuccessEvent:
|
2023-05-17 16:02:28 +02:00
|
|
|
logger.trace("listen_forward_events()")
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
# CLN has no subscription to forwarded events.
|
|
|
|
|
# We must poll instead.
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2025-02-16 20:35:21 +01:00
|
|
|
interval = config("BAPI_GATHER_LN_INFO_INTERVAL", default=2, cast=float)
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
# make sure we know how many forwards we have
|
|
|
|
|
# we need to calculate the difference between each iteration
|
|
|
|
|
# status=1 == "settled"
|
|
|
|
|
req = ln.ListforwardsRequest(status=1)
|
|
|
|
|
res = await self._cln_stub.ListForwards(req)
|
|
|
|
|
num_fwd_last_poll = len(res.forwards)
|
|
|
|
|
while True:
|
|
|
|
|
res = await self._cln_stub.ListForwards(req)
|
|
|
|
|
if len(res.forwards) > num_fwd_last_poll:
|
|
|
|
|
fwds = res.forwards[num_fwd_last_poll:]
|
|
|
|
|
for fwd in fwds:
|
|
|
|
|
yield ForwardSuccessEvent.from_cln_grpc(fwd)
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
num_fwd_last_poll = len(res.forwards)
|
|
|
|
|
await asyncio.sleep(interval - 0.1)
|
2022-08-03 20:43:25 +02:00
|
|
|
|
2023-04-03 20:17:48 +02:00
|
|
|
@logger.catch(exclude=(HTTPException,))
|
2023-03-13 21:51:38 +01:00
|
|
|
async def connect_peer(self, uri: str) -> bool:
|
2023-04-03 20:17:48 +02:00
|
|
|
logger.trace(f"connect_peer(node_URI={uri})")
|
2022-08-03 20:43:25 +02:00
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
try:
|
2023-03-13 21:51:38 +01:00
|
|
|
req = ln.ConnectRequest(id=uri)
|
2023-05-17 16:02:28 +02:00
|
|
|
await self._cln_stub.ConnectPeer(req)
|
2022-08-03 20:43:25 +02:00
|
|
|
|
2023-03-13 21:51:38 +01:00
|
|
|
return True
|
2022-10-03 10:44:34 +02:00
|
|
|
except grpc.aio._call.AioRpcError as error:
|
2023-03-13 21:51:38 +01:00
|
|
|
details = error.details()
|
|
|
|
|
logger.warning(details)
|
|
|
|
|
|
|
|
|
|
if "All addresses failed" in details:
|
|
|
|
|
m = details.split('message: "')[1]
|
|
|
|
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status.HTTP_400_BAD_REQUEST,
|
|
|
|
|
detail=m,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if "no address known for peer" in details:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status.HTTP_400_BAD_REQUEST,
|
|
|
|
|
detail="Connection establishment: No address known for peer",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if "Connection timed out" in details:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status.HTTP_504_GATEWAY_TIMEOUT,
|
|
|
|
|
detail="Connection establishment: Connection timed out.",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if "Connection refused" in details:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status.HTTP_504_GATEWAY_TIMEOUT,
|
|
|
|
|
detail="Connection establishment: Connection refused.",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
logger.exception(details)
|
|
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
raise HTTPException(
|
|
|
|
|
status.HTTP_500_INTERNAL_SERVER_ERROR, detail=error.details()
|
|
|
|
|
)
|
2022-08-03 20:43:25 +02:00
|
|
|
|
2023-05-01 20:48:10 +02:00
|
|
|
@logger.catch(exclude=(HTTPException, NodeNotFoundError))
|
|
|
|
|
async def peer_resolve_alias(self, node_pub: bytes) -> str:
|
2023-04-03 20:17:48 +02:00
|
|
|
logger.trace(f"peer_resolve_alias(node_pub={node_pub})")
|
2022-08-03 20:43:25 +02:00
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
try:
|
|
|
|
|
request = ln.ListnodesRequest(id=node_pub)
|
|
|
|
|
response = await self._cln_stub.ListNodes(request)
|
2022-08-03 20:43:25 +02:00
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
if len(response.nodes) == 0:
|
2023-05-01 20:48:10 +02:00
|
|
|
raise NodeNotFoundError(node_pub.hex())
|
2022-06-26 11:06:56 +02:00
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
return str(response.nodes[0].alias)
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
except grpc.aio._call.AioRpcError as error:
|
2023-05-01 20:48:10 +02:00
|
|
|
logger.error(error.details())
|
|
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
raise HTTPException(
|
2022-10-03 10:44:34 +02:00
|
|
|
status.HTTP_500_INTERNAL_SERVER_ERROR, detail=error.details()
|
2022-06-04 14:10:44 +02:00
|
|
|
)
|
|
|
|
|
|
2023-04-03 20:17:48 +02:00
|
|
|
@logger.catch(exclude=(HTTPException,))
|
2022-10-03 10:44:34 +02:00
|
|
|
async def channel_open(
|
|
|
|
|
self, local_funding_amount: int, node_URI: str, target_confs: int
|
|
|
|
|
) -> str:
|
2023-04-03 20:17:48 +02:00
|
|
|
logger.trace(
|
2023-05-17 16:02:28 +02:00
|
|
|
(
|
|
|
|
|
f"channel_open(local_funding_amount={local_funding_amount}, "
|
|
|
|
|
f"node_URI={node_URI}, target_confs={target_confs})"
|
|
|
|
|
)
|
2022-10-03 10:44:34 +02:00
|
|
|
)
|
|
|
|
|
|
2023-03-13 21:51:38 +01:00
|
|
|
await self.connect_peer(node_URI)
|
|
|
|
|
|
|
|
|
|
fee_rate: lnp.Feerate = None
|
2022-10-03 10:44:34 +02:00
|
|
|
if target_confs == 1:
|
2023-03-13 21:51:38 +01:00
|
|
|
fee_rate = lnp.Feerate(urgent=True)
|
2022-10-03 10:44:34 +02:00
|
|
|
elif target_confs >= 2 and target_confs <= 9:
|
2023-03-13 21:51:38 +01:00
|
|
|
fee_rate = lnp.Feerate(normal=True)
|
2022-10-03 10:44:34 +02:00
|
|
|
elif target_confs >= 10:
|
2023-03-13 21:51:38 +01:00
|
|
|
fee_rate = lnp.Feerate(slow=True)
|
2022-10-03 10:44:34 +02:00
|
|
|
|
|
|
|
|
try:
|
2023-03-13 21:51:38 +01:00
|
|
|
h = bytes.fromhex(node_URI.split("@")[0])
|
|
|
|
|
req = ln.FundchannelRequest(
|
|
|
|
|
id=h,
|
2023-04-07 20:59:44 +02:00
|
|
|
amount=lnp.AmountOrAll(amount=lnp.Amount(msat=local_funding_amount)),
|
2023-03-13 21:51:38 +01:00
|
|
|
feerate=fee_rate,
|
|
|
|
|
)
|
|
|
|
|
except TypeError as e:
|
2023-04-03 20:17:48 +02:00
|
|
|
logger.error(f"channel_open() failed at ln.FundchannelRequest(): {e}")
|
2023-03-13 21:51:38 +01:00
|
|
|
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
res = await self._cln_stub.FundChannel(req)
|
2023-04-07 20:59:44 +02:00
|
|
|
return res.txid.hex()
|
2023-03-13 21:51:38 +01:00
|
|
|
|
|
|
|
|
except grpc.aio._call.AioRpcError as error:
|
|
|
|
|
details = error.details()
|
|
|
|
|
logger.debug(details)
|
|
|
|
|
|
|
|
|
|
if "amount: should be a satoshi amount" in details:
|
2022-06-04 14:10:44 +02:00
|
|
|
raise HTTPException(
|
2023-03-13 21:51:38 +01:00
|
|
|
status.HTTP_400_BAD_REQUEST,
|
|
|
|
|
detail="The amount is not a valid satoshi amount.",
|
2022-06-04 14:10:44 +02:00
|
|
|
)
|
2022-10-03 10:44:34 +02:00
|
|
|
|
2023-03-13 21:51:38 +01:00
|
|
|
if "Unknown peer" in details:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status.HTTP_500_INTERNAL_SERVER_ERROR,
|
2023-05-17 16:02:28 +02:00
|
|
|
detail=(
|
|
|
|
|
"We where able to connect to the peer but CLN "
|
|
|
|
|
"can't find it when opening a channel."
|
|
|
|
|
),
|
2023-03-13 21:51:38 +01:00
|
|
|
)
|
2022-10-03 10:44:34 +02:00
|
|
|
|
2023-03-13 21:51:38 +01:00
|
|
|
if "Owning subdaemon openingd died" in details:
|
|
|
|
|
# https://github.com/ElementsProject/lightning/issues/2798#issuecomment-511205719
|
2022-06-04 14:10:44 +02:00
|
|
|
raise HTTPException(
|
2023-03-13 21:51:38 +01:00
|
|
|
status.HTTP_400_BAD_REQUEST,
|
2023-05-17 16:02:28 +02:00
|
|
|
detail=(
|
|
|
|
|
"Likely the peer didn't like our channel "
|
|
|
|
|
"opening proposal and disconnected from us. More info:"
|
|
|
|
|
"https://github.com/ElementsProject/lightning/issues/2798#issuecomment-511205719"
|
|
|
|
|
),
|
2022-06-04 14:10:44 +02:00
|
|
|
)
|
|
|
|
|
|
2023-03-13 21:51:38 +01:00
|
|
|
if (
|
|
|
|
|
"Number of pending channels exceed maximum" in details
|
|
|
|
|
or "exceeds maximum chan size of 10 BTC" in details
|
|
|
|
|
or "Could not afford all using all " in details
|
2023-04-07 20:59:44 +02:00
|
|
|
or "BTC is below min chan size of" in details
|
2023-03-13 21:51:38 +01:00
|
|
|
):
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status.HTTP_400_BAD_REQUEST, detail=_extract_message(details)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
logger.warning(f"UNHANDLED ERROR: {details}")
|
|
|
|
|
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, detail=details)
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2023-04-03 20:17:48 +02:00
|
|
|
@logger.catch(exclude=(HTTPException,))
|
2022-10-03 10:44:34 +02:00
|
|
|
async def channel_list(self) -> List[Channel]:
|
2023-05-17 16:02:28 +02:00
|
|
|
logger.trace("channel_list()")
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
try:
|
|
|
|
|
res = await self._cln_stub.ListFunds(ln.ListfundsRequest())
|
|
|
|
|
peer_ids = [c.peer_id for c in res.channels]
|
|
|
|
|
peer_res = await asyncio.gather(
|
2023-05-01 20:48:10 +02:00
|
|
|
*[alias_or_empty(self.peer_resolve_alias, p) for p in peer_ids],
|
|
|
|
|
return_exceptions=True,
|
2022-10-03 10:44:34 +02:00
|
|
|
)
|
2023-05-01 20:48:10 +02:00
|
|
|
|
|
|
|
|
channels = []
|
|
|
|
|
for c, p in zip(res.channels, peer_res):
|
|
|
|
|
channels.append(Channel.from_cln_grpc(c, p))
|
|
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
return channels
|
|
|
|
|
except grpc.aio._call.AioRpcError as error:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status.HTTP_500_INTERNAL_SERVER_ERROR, detail=error.details()
|
|
|
|
|
)
|
2022-06-26 11:06:56 +02:00
|
|
|
|
2023-04-03 20:17:48 +02:00
|
|
|
@logger.catch(exclude=(HTTPException,))
|
2022-10-03 10:44:34 +02:00
|
|
|
async def channel_close(self, channel_id: int, force_close: bool) -> str:
|
2023-04-03 20:17:48 +02:00
|
|
|
logger.trace(
|
|
|
|
|
f"channel_close(channel_id={channel_id}, force_close={force_close})"
|
2022-06-04 14:10:44 +02:00
|
|
|
)
|
|
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
try:
|
|
|
|
|
# on CLN we wait for 2 minutes to negotiate a channel close
|
|
|
|
|
# if peer doesn't respond we force close
|
|
|
|
|
wait_time_before_unilateral_close = 120 if force_close else 0
|
|
|
|
|
req = ln.CloseRequest(
|
|
|
|
|
id=channel_id,
|
|
|
|
|
unilateraltimeout=wait_time_before_unilateral_close,
|
|
|
|
|
feerange=[lnp.Feerate(slow=True), lnp.Feerate(urgent=True)],
|
|
|
|
|
)
|
|
|
|
|
res = await self._cln_stub.Close(req)
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2022-10-03 10:44:34 +02:00
|
|
|
# “mutual”, “unilateral”, “unopened”
|
|
|
|
|
t = res.item_type
|
|
|
|
|
if t == 0 or t == 1: # mutual, unilateral
|
|
|
|
|
return res.txid.hex()
|
|
|
|
|
elif t == 2: # unopened
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status.HTTP_400_BAD_REQUEST, detail="Channel is not open yet."
|
|
|
|
|
)
|
2022-06-04 14:10:44 +02:00
|
|
|
|
|
|
|
|
raise HTTPException(
|
2022-10-03 10:44:34 +02:00
|
|
|
status.HTTP_400_BAD_REQUEST,
|
|
|
|
|
detail=f"CLN returned unknown close type: {t}",
|
2022-06-04 14:10:44 +02:00
|
|
|
)
|
2022-10-03 10:44:34 +02:00
|
|
|
except grpc.aio._call.AioRpcError as error:
|
|
|
|
|
if "Channel is in state AWAITING_UNILATERAL" in error.details():
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status.HTTP_400_BAD_REQUEST,
|
|
|
|
|
detail="Channel is awaiting an unilateral close.",
|
|
|
|
|
)
|
2022-06-04 14:10:44 +02:00
|
|
|
|
|
|
|
|
raise HTTPException(
|
2022-10-03 10:44:34 +02:00
|
|
|
status.HTTP_500_INTERNAL_SERVER_ERROR, detail=error.details()
|
2022-06-04 14:10:44 +02:00
|
|
|
)
|
2022-12-01 19:08:29 +01:00
|
|
|
|
2023-04-03 20:17:48 +02:00
|
|
|
@logger.catch(exclude=(HTTPException,))
|
2022-12-01 19:08:29 +01:00
|
|
|
def _handle_base_cln_error(self, error: grpc.aio._call.AioRpcError) -> None:
|
|
|
|
|
# This method handles all errors common to all CLN calls
|
|
|
|
|
details = error.details()
|
|
|
|
|
|
|
|
|
|
if details and details.find("Received RST_STREAM with error code 8") > -1:
|
2022-12-18 20:55:48 +01:00
|
|
|
logger.error(details)
|
2022-12-01 19:08:29 +01:00
|
|
|
raise HTTPException(
|
|
|
|
|
status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
|
|
|
detail="CLN is responding with an error. Please check the logs.",
|
|
|
|
|
)
|