refactor: lightning system decoupling

This commit is contained in:
fusion44 2022-10-03 10:44:34 +02:00
parent 799eb8deef
commit df93cbdcee
No known key found for this signature in database
5 changed files with 1858 additions and 1789 deletions

View file

@ -28,11 +28,13 @@ PLATFORM = config("platform", cast=str)
ln_node = config("ln_node")
if ln_node == "lnd_grpc":
import app.repositories.ln_impl.lnd_grpc as ln
from app.repositories.ln_impl.lnd_grpc import LnNodeLNDgRPC as LnNode
elif ln_node == "cln_grpc" and PLATFORM != APIPlatform.RASPIBLITZ:
import app.repositories.ln_impl.cln_grpc as ln
from app.repositories.ln_impl.cln_grpc import LnNodeCLNgRPC as LnNode
elif ln_node == "cln_grpc" and PLATFORM == APIPlatform.RASPIBLITZ:
import app.repositories.ln_impl.specializations.cln_grpc_blitz as ln
from app.repositories.ln_impl.specializations.cln_grpc_blitz import (
LnNodeCLNgRPCBlitz as LnNode,
)
elif ln_node == "none":
logging.info(f"lightning was explicitly turned off")
elif ln_node == "":
@ -54,31 +56,33 @@ FWD_GATHER_INTERVAL = config("forwards_gather_interval", default=2.0, cast=float
if FWD_GATHER_INTERVAL < 0.3:
raise RuntimeError("forwards_gather_interval cannot be less than 0.3 seconds")
ln = LnNode()
async def initialize_ln_repo() -> AsyncGenerator[InitLnRepoUpdate, None]:
async for u in ln.initialize_impl():
async for u in ln.initialize():
yield u
async def get_ln_info_lite() -> LightningInfoLite:
ln_info = await ln.get_ln_info_impl()
ln_info = await ln.get_ln_info()
return LightningInfoLite.from_lninfo(ln_info)
async def get_wallet_balance():
return await ln.get_wallet_balance_impl()
return await ln.get_wallet_balance()
async def list_all_tx(
successful_only: bool, index_offset: int, max_tx: int, reversed: bool
) -> List[GenericTx]:
return await ln.list_all_tx_impl(successful_only, index_offset, max_tx, reversed)
return await ln.list_all_tx(successful_only, index_offset, max_tx, reversed)
async def list_invoices(
pending_only: bool, index_offset: int, num_max_invoices: int, reversed: bool
) -> List[Invoice]:
return await ln.list_invoices_impl(
return await ln.list_invoices(
pending_only,
index_offset,
num_max_invoices,
@ -87,13 +91,13 @@ async def list_invoices(
async def list_on_chain_tx() -> List[OnChainTransaction]:
return await ln.list_on_chain_tx_impl()
return await ln.list_on_chain_tx()
async def list_payments(
include_incomplete: bool, index_offset: int, max_payments: int, reversed: bool
) -> List[Payment]:
return await ln.list_payments_impl(
return await ln.list_payments(
include_incomplete, index_offset, max_payments, reversed
)
@ -101,19 +105,19 @@ async def list_payments(
async def add_invoice(
value_msat: int, memo: str = "", expiry: int = 3600, is_keysend: bool = False
) -> Invoice:
return await ln.add_invoice_impl(memo, value_msat, expiry, is_keysend)
return await ln.add_invoice(memo, value_msat, expiry, is_keysend)
async def decode_pay_request(pay_req: str) -> PaymentRequest:
return await ln.decode_pay_request_impl(pay_req)
return await ln.decode_pay_request(pay_req)
async def new_address(input: NewAddressInput) -> str:
return await ln.new_address_impl(input)
return await ln.new_address(input)
async def send_coins(input: SendCoinsInput) -> SendCoinsResponse:
res = await ln.send_coins_impl(input)
res = await ln.send_coins(input)
_schedule_wallet_balance_update()
return res
@ -124,9 +128,7 @@ async def send_payment(
fee_limit_msat: int,
amount_msat: Optional[int] = None,
) -> Payment:
res = await ln.send_payment_impl(
pay_req, timeout_seconds, fee_limit_msat, amount_msat
)
res = await ln.send_payment(pay_req, timeout_seconds, fee_limit_msat, amount_msat)
_schedule_wallet_balance_update()
return res
@ -147,41 +149,41 @@ async def channel_open(
if not "@" in node_URI:
raise ValueError("node_URI must contain @ with node physical address")
res = await ln.channel_open_impl(local_funding_amount, node_URI, target_confs)
res = await ln.channel_open(local_funding_amount, node_URI, target_confs)
return res
async def channel_list() -> List[Channel]:
res = await ln.channel_list_impl()
res = await ln.channel_list()
return res
async def channel_close(channel_id: int, force_close: bool) -> str:
res = await ln.channel_close_impl(channel_id, force_close)
res = await ln.channel_close(channel_id, force_close)
return res
async def get_ln_info() -> LnInfo:
ln_info = await ln.get_ln_info_impl()
ln_info = await ln.get_ln_info()
if PLATFORM == APIPlatform.RASPIBLITZ:
ln_info.identity_uri = await redis_get("ln_default_address")
return ln_info
async def unlock_wallet(password: str) -> bool:
res = await ln.unlock_wallet_impl(password)
res = await ln.unlock_wallet(password)
return res
async def get_fee_revenue() -> FeeRevenue:
return await ln.get_fee_revenue_impl()
return await ln.get_fee_revenue()
async def register_lightning_listener():
"""
Registers all lightning listeners
By calling get_ln_info_impl() once, we ensure that wallet is unlocked.
By calling get_ln_info() once, we ensure that wallet is unlocked.
Implementation will throw HTTPException with status_code 423_LOCKED if otherwise.
It is the task of the caller to call register_lightning_listener() again
"""
@ -194,7 +196,7 @@ async def register_lightning_listener():
)
return
await ln.get_ln_info_impl()
await ln.get_ln_info()
loop = asyncio.get_event_loop()
loop.create_task(_handle_info_listener())
@ -208,7 +210,7 @@ async def _handle_info_listener():
last_info = None
last_info_lite = None
while True:
info = await ln.get_ln_info_impl()
info = await ln.get_ln_info()
if last_info != info:
await broadcast_sse_msg(SSE.LN_INFO, info.dict())
@ -270,7 +272,7 @@ def _schedule_wallet_balance_update():
global _wallet_balance_update_scheduled
_wallet_balance_update_scheduled = True
await asyncio.sleep(1.1)
wb = await ln.get_wallet_balance_impl()
wb = await ln.get_wallet_balance()
if _CACHE["wallet_balance"] != wb:
await broadcast_sse_msg(SSE.WALLET_BALANCE, wb.dict())
_CACHE["wallet_balance"] = wb

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,133 @@
from abc import abstractmethod
from typing import AsyncGenerator, List, Optional
from app.models.lightning import (
Channel,
FeeRevenue,
ForwardSuccessEvent,
GenericTx,
InitLnRepoUpdate,
Invoice,
LnInfo,
NewAddressInput,
OnChainTransaction,
Payment,
PaymentRequest,
SendCoinsInput,
SendCoinsResponse,
WalletBalance,
)
class LightningNodeBase:
@abstractmethod
def get_implementation_name(self) -> str:
raise NotImplementedError()
@abstractmethod
async def initialize(self) -> AsyncGenerator[InitLnRepoUpdate, None]:
raise NotImplementedError()
@abstractmethod
async def get_wallet_balance(self) -> WalletBalance:
raise NotImplementedError()
@abstractmethod
async def list_all_tx(
self, successful_only: bool, index_offset: int, max_tx: int, reversed: bool
) -> List[GenericTx]:
raise NotImplementedError()
@abstractmethod
async def list_invoices(
self,
pending_only: bool,
index_offset: int,
num_max_invoices: int,
reversed: bool,
):
raise NotImplementedError()
@abstractmethod
async def list_on_chain_tx(self) -> List[OnChainTransaction]:
raise NotImplementedError()
@abstractmethod
async def list_payments(
self,
include_incomplete: bool,
index_offset: int,
max_payments: int,
reversed: bool,
):
raise NotImplementedError()
@abstractmethod
async def add_invoice(
self,
value_msat: int,
memo: str = "",
expiry: int = 3600,
is_keysend: bool = False,
) -> Invoice:
raise NotImplementedError()
@abstractmethod
async def decode_pay_request(self, pay_req: str) -> PaymentRequest:
raise NotImplementedError()
@abstractmethod
async def get_fee_revenue(self) -> FeeRevenue:
raise NotImplementedError()
@abstractmethod
async def new_address(self, input: NewAddressInput) -> str:
raise NotImplementedError()
@abstractmethod
async def send_coins(self, input: SendCoinsInput) -> SendCoinsResponse:
raise NotImplementedError()
@abstractmethod
async def send_payment(
self,
pay_req: str,
timeout_seconds: int,
fee_limit_msat: int,
amount_msat: Optional[int] = None,
) -> Payment:
raise NotImplementedError()
@abstractmethod
async def get_ln_info(self) -> LnInfo:
raise NotImplementedError()
@abstractmethod
async def unlock_wallet(self, password: str) -> bool:
raise NotImplementedError()
@abstractmethod
async def listen_invoices(self) -> AsyncGenerator[Invoice, None]:
raise NotImplementedError()
@abstractmethod
async def listen_forward_events(self) -> ForwardSuccessEvent:
raise NotImplementedError()
@abstractmethod
async def channel_open(
self, local_funding_amount: int, node_URI: str, target_confs: int
) -> str:
raise NotImplementedError()
@abstractmethod
async def peer_resolve_alias(self, node_pub: str) -> str:
raise NotImplementedError()
@abstractmethod
async def channel_list(self) -> List[Channel]:
raise NotImplementedError()
@abstractmethod
async def channel_close(self, channel_id: int, force_close: bool) -> str:
raise NotImplementedError()

File diff suppressed because it is too large Load diff

View file

@ -6,7 +6,6 @@ from decouple import config
from fastapi.exceptions import HTTPException
from starlette import status
import app.repositories.ln_impl.cln_grpc as cln_main
from app.core_utils import call_script2, redis_get
from app.models.lightning import (
Channel,
@ -25,307 +24,240 @@ from app.models.lightning import (
SendCoinsResponse,
WalletBalance,
)
# RaspiBlitz implements a lock function on top of CLN, so we need to implement this on Blitz only.
from app.repositories.ln_impl.cln_grpc import LnNodeCLNgRPC
_unlocked = False
class LnNodeCLNgRPCBlitz(LnNodeCLNgRPC):
# RaspiBlitz implements a lock function on top of CLN, so we need to implement this on Blitz only.
_NETWORK = config("network", default="mainnet")
_unlocked = False
_NETWORK = config("network", default="mainnet")
def get_implementation_name() -> str:
return "CLN_GRPC_BLITZ"
def get_implementation_name(self) -> str:
return "CLN_GRPC_BLITZ"
async def initialize(self) -> AsyncGenerator[InitLnRepoUpdate, None]:
logging.debug("CLN_GRPC_BLITZ: RaspiBlitz is locked, waiting for unlock...")
async def initialize_impl() -> AsyncGenerator[InitLnRepoUpdate, None]:
logging.debug("CLN_GRPC_BLITZ: RaspiBlitz is locked, waiting for unlock...")
global _unlocked
while not _unlocked:
key = f"ln_cl_{_NETWORK}_locked"
res = await redis_get(key)
if res == "0":
logging.debug(
f"CLN_GRPC_BLITZ: Redis key {key} indicates that RaspiBlitz has been unlocked"
)
_unlocked = True
yield InitLnRepoUpdate(state=LnInitState.BOOTSTRAPPING_AFTER_UNLOCK)
break
elif res == "1":
logging.debug(
f"CLN_GRPC_BLITZ: Redis key {key} indicates that RaspiBlitz is still locked"
)
yield InitLnRepoUpdate(
state=LnInitState.LOCKED,
msg="Wallet locked, unlock it to enable full RPC access",
)
else:
logging.error(
f"CLN_GRPC_BLITZ: Redis key {key} returns an unexpected value: {res}"
)
raise HTTPException(
status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Unknown lock status: {res}",
)
await asyncio.sleep(2)
async for u in cln_main.initialize_impl():
yield u
if u.state == LnInitState.DONE:
break
logging.info("CLN_GRPC_BLITZ: Initialization complete.")
async def get_wallet_balance_impl() -> WalletBalance:
try:
return await cln_main.get_wallet_balance_impl()
except:
_check_if_locked()
raise
async def list_all_tx_impl(
successful_only: bool, index_offset: int, max_tx: int, reversed: bool
) -> List[GenericTx]:
try:
return await cln_main.list_all_tx_impl(
successful_only, index_offset, max_tx, reversed
)
except:
_check_if_locked()
raise
async def list_invoices_impl(
pending_only: bool, index_offset: int, num_max_invoices: int, reversed: bool
) -> List[Invoice]:
try:
return await cln_main.list_invoices_impl(
pending_only, index_offset, num_max_invoices, reversed
)
except:
_check_if_locked()
raise
async def list_on_chain_tx_impl() -> List[OnChainTransaction]:
try:
return await cln_main.list_on_chain_tx_impl()
except:
_check_if_locked()
raise
async def list_payments_impl(
include_incomplete: bool, index_offset: int, max_payments: int, reversed: bool
):
try:
return await cln_main.list_payments_impl(
include_incomplete, index_offset, max_payments, reversed
)
except:
_check_if_locked()
raise
async def add_invoice_impl(
value_msat: int, memo: str = "", expiry: int = 3600, is_keysend: bool = False
) -> Invoice:
try:
return await cln_main.add_invoice_impl(value_msat, memo, expiry, is_keysend)
except:
_check_if_locked()
raise
async def decode_pay_request_impl(pay_req: str) -> PaymentRequest:
try:
return await cln_main.decode_pay_request_impl(pay_req)
except:
_check_if_locked()
raise
async def get_fee_revenue_impl() -> FeeRevenue:
try:
return await cln_main.get_fee_revenue_impl()
except:
_check_if_locked()
raise
async def new_address_impl(input: NewAddressInput) -> str:
try:
return await cln_main.new_address_impl(input)
except:
_check_if_locked()
raise
async def send_coins_impl(input: SendCoinsInput) -> SendCoinsResponse:
try:
return await cln_main.send_coins_impl(input)
except:
_check_if_locked()
raise
async def send_payment_impl(
pay_req: str,
timeout_seconds: int,
fee_limit_msat: int,
amount_msat: Optional[int] = None,
) -> Payment:
try:
return await cln_main.send_payment_impl(
pay_req, timeout_seconds, fee_limit_msat, amount_msat
)
except:
_check_if_locked()
raise
async def get_ln_info_impl() -> LnInfo:
try:
# This will return "CLN_GRPC" and not "CLN_GRPC_BLITZ" to
# not to complicate things further.
# res.implementation = get_implementation_name()
return await cln_main.get_ln_info_impl()
except:
_check_if_locked()
raise
async def unlock_wallet_impl(password: str) -> bool:
# RaspiBlitz implements a wallet lock functionality on top of CLN,
# so we need to implement this on Blitz only
# /home/admin/config.scripts/cl.hsmtool.sh unlock mainnet PASSWORD_C
# cl.hsmtool.sh [unlock] <mainnet|testnet|signet> <password>
global _unlocked
key = f"ln_cl_{_NETWORK}_locked"
res = await redis_get(key)
if res == "0":
raise HTTPException(
status.HTTP_412_PRECONDITION_FAILED, detail="wallet already unlocked"
)
res = await call_script2(
f"/home/admin/config.scripts/cl.hsmtool.sh unlock {_NETWORK} {password}"
)
if res.return_code == 0:
logging.debug(
f"CLN_GRPC_BLITZ: Unlock script successfully called via API. Waiting for Redis {key} to be set."
)
# success: exit 0
INTERVAL = 1
total_wait_time = 0
while total_wait_time < 60:
while not self._unlocked:
key = f"ln_cl_{self._NETWORK}_locked"
res = await redis_get(key)
if res == "0":
_unlocked = True
return True
logging.debug(
f"CLN_GRPC_BLITZ: Redis key {key} indicates that RaspiBlitz has been unlocked"
)
await asyncio.sleep(INTERVAL)
total_wait_time += INTERVAL
self._unlocked = True
yield InitLnRepoUpdate(state=LnInitState.BOOTSTRAPPING_AFTER_UNLOCK)
break
elif res == "1":
logging.debug(
f"CLN_GRPC_BLITZ: Redis key {key} indicates that RaspiBlitz is still locked"
)
logging.debug(
f"CLN_GRPC_BLITZ: Unlock script called successfully but redis key {key} indicates that RaspiBlitz is still locked. Stopped watching after polling for 60s for an unlock signal."
yield InitLnRepoUpdate(
state=LnInitState.LOCKED,
msg="Wallet locked, unlock it to enable full RPC access",
)
else:
logging.error(
f"CLN_GRPC_BLITZ: Redis key {key} returns an unexpected value: {res}"
)
raise HTTPException(
status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Unknown lock status: {res}",
)
await asyncio.sleep(2)
async for u in super().initialize_impl():
yield u
if u.state == LnInitState.DONE:
break
logging.info("CLN_GRPC_BLITZ: Initialization complete.")
async def get_wallet_balance(self) -> WalletBalance:
self._check_if_locked()
return await super().get_wallet_balance()
async def list_all_tx(
self, successful_only: bool, index_offset: int, max_tx: int, reversed: bool
) -> List[GenericTx]:
self._check_if_locked()
return await super().list_all_tx(
successful_only, index_offset, max_tx, reversed
)
async def list_invoices(
self,
pending_only: bool,
index_offset: int,
num_max_invoices: int,
reversed: bool,
):
self._check_if_locked()
return await super().list_invoices(
pending_only, index_offset, num_max_invoices, reversed
)
async def list_on_chain_tx(self) -> List[OnChainTransaction]:
self._check_if_locked()
return await super().list_on_chain_tx()
async def list_payments(
self,
include_incomplete: bool,
index_offset: int,
max_payments: int,
reversed: bool,
):
self._check_if_locked()
return await super().list_payments(
include_incomplete, index_offset, max_payments, reversed
)
async def add_invoice(
self,
value_msat: int,
memo: str = "",
expiry: int = 3600,
is_keysend: bool = False,
) -> Invoice:
self._check_if_locked()
return await super().add_invoice(value_msat, memo, expiry, is_keysend)
async def decode_pay_request(self, pay_req: str) -> PaymentRequest:
self._check_if_locked()
return await super().decode_pay_request(pay_req)
async def get_fee_revenue(self) -> FeeRevenue:
self._check_if_locked()
return await super().get_fee_revenue()
async def new_address(self, input: NewAddressInput) -> str:
self._check_if_locked()
return await super().new_address(input)
async def send_coins(self, input: SendCoinsInput) -> SendCoinsResponse:
self._check_if_locked()
return await super().send_coins(input)
async def send_payment(
self,
pay_req: str,
timeout_seconds: int,
fee_limit_msat: int,
amount_msat: Optional[int] = None,
) -> Payment:
self._check_if_locked()
return await super().send_payment(
pay_req, timeout_seconds, fee_limit_msat, amount_msat
)
async def get_ln_info(self) -> LnInfo:
self._check_if_locked()
return await super().get_ln_info()
async def unlock_wallet(self, password: str) -> bool:
# RaspiBlitz implements a wallet lock functionality on top of CLN,
# so we need to implement this on Blitz only
# /home/admin/config.scripts/cl.hsmtool.sh unlock mainnet PASSWORD_C
# cl.hsmtool.sh [unlock] <mainnet|testnet|signet> <password>
key = f"ln_cl_{self._NETWORK}_locked"
res = await redis_get(key)
if res == "0":
raise HTTPException(
status.HTTP_412_PRECONDITION_FAILED, detail="wallet already unlocked"
)
res = await call_script2(
f"/home/admin/config.scripts/cl.hsmtool.sh unlock {self._NETWORK} {password}"
)
if res.return_code == 0:
logging.debug(
f"CLN_GRPC_BLITZ: Unlock script successfully called via API. Waiting for Redis {key} to be set."
)
# success: exit 0
INTERVAL = 1
total_wait_time = 0
while total_wait_time < 60:
res = await redis_get(key)
if res == "0":
_unlocked = True
return True
await asyncio.sleep(INTERVAL)
total_wait_time += INTERVAL
logging.debug(
f"CLN_GRPC_BLITZ: Unlock script called successfully but redis key {key} indicates that RaspiBlitz is still locked. Stopped watching after polling for 60s for an unlock signal."
)
raise HTTPException(
status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Unknown error while trying to unlock.",
)
elif res.return_code == 1:
logging.error("CLN_GRPC_BLITZ: Unknown error while trying to unlock.")
logging.error(f"CLN_GRPC_BLITZ: {res.__str__()}")
raise HTTPException(
status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Unknown error while trying to unlock. See the API logs for more info.",
)
elif res.return_code == 2:
# wrong password: exit 2
raise HTTPException(
status.HTTP_401_UNAUTHORIZED, detail="invalid passphrase"
)
elif res.return_code == 3:
# fail to unlock after 1 minute + show logs: exit 3
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, detail=res)
raise HTTPException(
status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Unknown error while trying to unlock.",
detail=f"Unknown error while trying to unlock.\n{res}",
)
elif res.return_code == 1:
logging.error("CLN_GRPC_BLITZ: Unknown error while trying to unlock.")
logging.error(f"CLN_GRPC_BLITZ: {res.__str__()}")
raise HTTPException(
status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Unknown error while trying to unlock. See the API logs for more info.",
)
elif res.return_code == 2:
# wrong password: exit 2
raise HTTPException(status.HTTP_401_UNAUTHORIZED, detail="invalid passphrase")
elif res.return_code == 3:
# fail to unlock after 1 minute + show logs: exit 3
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, detail=res)
raise HTTPException(
status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Unknown error while trying to unlock.\n{res}",
)
async def listen_invoices() -> AsyncGenerator[Invoice, None]:
try:
async for i in cln_main.listen_invoices():
async def listen_invoices(self) -> AsyncGenerator[Invoice, None]:
self._check_if_locked()
async for i in super().listen_invoices():
yield i
except:
_check_if_locked()
raise
async def listen_forward_events(self) -> ForwardSuccessEvent:
self._check_if_locked()
async for i in super().listen_forward_events():
yield i
async def listen_forward_events() -> ForwardSuccessEvent:
try:
async for e in cln_main.listen_forward_events():
yield e
except:
_check_if_locked()
raise
async def channel_open(
self, local_funding_amount: int, node_URI: str, target_confs: int
) -> str:
self._check_if_locked()
return await super().channel_open(local_funding_amount, node_URI, target_confs)
async def peer_resolve_alias(self, node_pub: str) -> str:
self._check_if_locked()
return await super().peer_resolve_alias(node_pub)
async def connect_peer_impl(node_URI: str) -> bool:
try:
return await cln_main.connect_peer_impl(node_URI)
except:
_check_if_locked()
raise
async def channel_list(self) -> List[Channel]:
self._check_if_locked()
return await super().channel_list()
async def channel_close(self, channel_id: int, force_close: bool) -> str:
self._check_if_locked()
return await super().channel_close(channel_id, force_close)
async def channel_open_impl(
local_funding_amount: int, node_URI: str, target_confs: int
) -> str:
try:
return await cln_main.channel_open_impl(
local_funding_amount, node_URI, target_confs
)
except:
_check_if_locked()
raise
def _check_if_locked(self):
logging.debug(f"CLN_GRPC_BLITZ: _check_if_locked()")
async def channel_list_impl() -> List[Channel]:
try:
return await cln_main.channel_list_impl()
except:
_check_if_locked()
raise
async def channel_close_impl(channel_id: int, force_close: bool) -> str:
try:
return await cln_main.channel_close_impl(channel_id, force_close)
except:
_check_if_locked()
raise
def _check_if_locked():
logging.debug(f"CLN_GRPC_BLITZ: _check_if_locked()")
if not _unlocked:
raise HTTPException(
status.HTTP_423_LOCKED,
detail="Wallet is locked. Unlock via /lightning/unlock-wallet",
)
if not self._unlocked:
raise HTTPException(
status.HTTP_423_LOCKED,
detail="Wallet is locked. Unlock via /lightning/unlock-wallet",
)