diff --git a/app/bitcoind/models.py b/app/bitcoind/models.py index 8d8f96d..9d6dc96 100644 --- a/app/bitcoind/models.py +++ b/app/bitcoind/models.py @@ -140,10 +140,10 @@ class NetworkInfo(BaseModel): connections_out: int = Query(..., description="The number of outbound connections") network_active: bool = Query(..., description="Whether p2p networking is enabled") networks: List[BtcNetwork] = Query(..., description="Information per network") - relay_fee: int = Query( + relay_fee: float = Query( ..., description="Minimum relay fee for transactions in BTC/kB" ) - incremental_fee: int = Query( + incremental_fee: float = Query( ..., description=( "Minimum fee increment for mempool limiting or BIP 125 " @@ -228,7 +228,7 @@ class Bip9Data(BaseModel): ..., description='One of "defined", "started", "locked_in", "active", "failed"', ) - bit: int = Query( + bit: int | None = Query( None, description=( "the bit(0-28) in the block version field used to signal this " @@ -255,21 +255,21 @@ class Bip9Data(BaseModel): min_activation_height: int = Query( ..., description="Minimum height of blocks for which the rules may be enforced" ) - statistics: Bip9Statistics = Query( + statistics: Bip9Statistics | None = Query( None, description=( "numeric statistics about BIP9 signalling for a " "softfork(only for `started` status)" ), ) - height: int = Query( + height: int | None = Query( None, description=( "Height of the first block which the rules are or will be " "enforced(only for `buried` type, or `bip9` type with `active` status)" ), ) - active: bool = Query( + active: bool | None = Query( None, description="True if the rules are enforced for the mempool and the next block", ) @@ -300,10 +300,10 @@ class SoftFork(BaseModel): "True **if** the rules are enforced for the mempool and the next block" ), ) - bip9: Bip9Data = Query( + bip9: Bip9Data | None = Query( None, description='Status of bip9 softforks(only for "bip9" type)' ) - height: int = Query( + height: int | None = Query( None, description=( "Height of the first block which the rules are or will be enforced " @@ -337,7 +337,7 @@ class BlockchainInfo(BaseModel): best_block_hash: str = Query( ..., description="The hash of the currently best block" ) - difficulty: int = Query(..., description="The current difficulty") + difficulty: float = Query(..., description="The current difficulty") mediantime: int = Query(..., description="Median time for the current best block") verification_progress: float = Query( ..., description="Estimate of verification progress[0..1]" @@ -353,19 +353,19 @@ class BlockchainInfo(BaseModel): ..., description="The estimated size of the block and undo files on disk" ) pruned: bool = Query(..., description="If the blocks are subject to pruning") - prune_height: int = Query( + prune_height: int | None = Query( None, description=( "Lowest-height complete block stored(only present if pruning is enabled)" ), ) - automatic_pruning: bool = Query( + automatic_pruning: bool | None = Query( None, description=( "Whether automatic pruning is enabled(only present if pruning is enabled)" ), ) - prune_target_size: int = Query( + prune_target_size: int | None = Query( None, description=( "The target size used by pruning(only present if automatic pruning is " @@ -395,12 +395,12 @@ class BlockchainInfo(BaseModel): chainwork=r["chainwork"], size_on_disk=r["size_on_disk"], pruned=r["pruned"], - pruned_height=None if "pruneheight" not in r else r["pruneheight"], + prune_height=None if "pruneheight" not in r else r["pruneheight"], automatic_pruning=( - None if "automatic_pruning" not in r else r["automatic_pruning"] + None if "automatic_pruning" not in r else bool(r["automatic_pruning"]) ), prune_target_size=( - None if "prune_target_size" not in r else r["prune_target_size"] + None if "prune_target_size" not in r else int(r["prune_target_size"]) ), warnings=r["warnings"], softforks=softforks, @@ -422,7 +422,7 @@ class BtcInfo(BaseModel): verification_progress: float = Query( ..., description="Estimate of verification progress[0..1]" ) - difficulty: int = Query(..., description="The current difficulty") + difficulty: float = Query(..., description="The current difficulty") size_on_disk: int = Query( ..., description="The estimated size of the block and undo files on disk" ) diff --git a/app/lightning/impl/cln_jrpc.py b/app/lightning/impl/cln_jrpc.py index 23e84ea..2f55785 100644 --- a/app/lightning/impl/cln_jrpc.py +++ b/app/lightning/impl/cln_jrpc.py @@ -48,10 +48,10 @@ class LnNodeCLNjRPC(LightningNodeBase): lastpay_index = 0 _current_id: int = _WAIT_ANY_INVOICE_ID + 1 _futures: dict[int, asyncio.Future] = {} - _socket_path: str = None - _reader: asyncio.StreamReader = None - _writer: asyncio.StreamWriter = None - _loop: asyncio.AbstractEventLoop = None + _socket_path: str | None = None + _reader: asyncio.StreamReader | None = None + _writer: asyncio.StreamWriter | None = None + _loop: asyncio.AbstractEventLoop | None = None _initialized: bool = False _invoice_queue = asyncio.Queue() @@ -78,7 +78,8 @@ class LnNodeCLNjRPC(LightningNodeBase): yield InitLnRepoUpdate(state=LnInitState.BOOTSTRAPPING) try: - self._socket_path = decouple.config("cln_jrpc_path") + self._socket_path = str(decouple.config("cln_jrpc_path")) + print(decouple.config("cln_jrpc_path")) except decouple.UndefinedValueError as e: logger.debug(e) logger.error( @@ -102,6 +103,13 @@ class LnNodeCLNjRPC(LightningNodeBase): while True: try: + if not isinstance(self._socket_path, str): + logger.trace( + f"self._socket_path is None, retrying {type(self._socket_path)}" + ) + await asyncio.sleep(10) + continue + self._reader, self._writer = await asyncio.open_unix_connection( path=self._socket_path, limit=_SOCKET_BUFFER_SIZE_LIMIT, @@ -145,7 +153,7 @@ class LnNodeCLNjRPC(LightningNodeBase): chan_local = chan_remote = chan_pending_local = chan_pending_remote = 0 for o in res["outputs"]: - sat = parse_cln_msat(o["amount_msat"]) / 1000 + sat = int(parse_cln_msat(o["amount_msat"]) / 1000) if o["status"] == "unconfirmed": onchain_unconfirmed += sat @@ -227,7 +235,7 @@ class LnNodeCLNjRPC(LightningNodeBase): tx = [] for invoice in res[0]: i = GenericTx.from_invoice(invoice) - if successful_only and i["status"] == "succeeded": + if successful_only and i.status == "succeeded": tx.append(i) continue tx.append(i) @@ -240,7 +248,11 @@ class LnNodeCLNjRPC(LightningNodeBase): tx.append(t) - for pay in res[2]: # type: Payment + for pay in res[2]: + if pay is not Payment: + logger.error("Payment is not a payment class.") + continue + comment = "" if pay.payment_request is not None and len(pay.payment_request) > 0: @@ -249,7 +261,7 @@ class LnNodeCLNjRPC(LightningNodeBase): p = GenericTx.from_payment(pay, comment) - if successful_only and p["status"] == "succeeded": + if successful_only and p.status == "succeeded": tx.append(p) continue @@ -524,7 +536,7 @@ class LnNodeCLNjRPC(LightningNodeBase): if "error" not in res: res = res["result"] r = SendCoinsResponse.from_cln_json(res, input) - await broadcast_sse_msg(SSE.LN_ONCHAIN_PAYMENT_STATUS, r.dict()) + await broadcast_sse_msg(SSE.LN_ONCHAIN_PAYMENT_STATUS, r.model_dump()) return r @@ -681,7 +693,7 @@ class LnNodeCLNjRPC(LightningNodeBase): await self._refresh_invoice_sub(True) @logger.catch(exclude=(HTTPException,)) - async def listen_forward_events(self) -> ForwardSuccessEvent: + async def listen_forward_events(self) -> AsyncGenerator[ForwardSuccessEvent, None]: logger.trace("listen_forward_events()") # CLN has no subscription to forwarded events. @@ -731,10 +743,18 @@ class LnNodeCLNjRPC(LightningNodeBase): if "error" in res: self._handle_open_channel_error(res["error"]) - res = res["result"] - if "txid" in res and "channel_id" in res: - return res["txid"] + res = res["result"] + if "txid" not in res and "channel_id" not in res: + self._handle_open_channel_error( + { + "message": ( + "Unable to find txid and channel_id in connect peer result" + ) + } + ) + + return res["txid"] def _handle_open_channel_error(self, error): logger.trace(f"_handle_open_channel_error({error})") @@ -903,6 +923,10 @@ class LnNodeCLNjRPC(LightningNodeBase): async def _read_loop(self): logger.trace("_read_loop()") + if self._writer is None or self._reader is None: + message = "Not initialized - _writer or _reader not yet available" + logger.error(message) + raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, detail=message) while not self._writer.is_closing(): try: diff --git a/app/lightning/impl/lnd_grpc.py b/app/lightning/impl/lnd_grpc.py index c33497e..e1e9060 100644 --- a/app/lightning/impl/lnd_grpc.py +++ b/app/lightning/impl/lnd_grpc.py @@ -479,7 +479,7 @@ This will show more debug information. expiry=expiry, r_hash=response.r_hash.hex(), payment_request=response.payment_request, - add_index=response.add_index, + add_index=str(response.add_index), payment_addr=response.payment_addr.hex(), state=InvoiceState.OPEN, is_keysend=is_keysend, diff --git a/app/lightning/models.py b/app/lightning/models.py index 951511d..4d97412 100644 --- a/app/lightning/models.py +++ b/app/lightning/models.py @@ -1,4 +1,5 @@ import logging +import time from enum import Enum from typing import List, Optional, Union @@ -491,7 +492,7 @@ class Channel(BaseModel): class Invoice(BaseModel): - memo: str = Query( + memo: str | None = Query( None, description=( "Optional memo to attach along with the invoice. " @@ -501,7 +502,7 @@ class Invoice(BaseModel): ), ) - r_preimage: str = Query( + r_preimage: str | None = Query( None, description=( "The hex-encoded preimage(32 byte) which will allow settling " @@ -509,7 +510,7 @@ class Invoice(BaseModel): ), ) - r_hash: str = Query(None, description="The hash of the preimage.") + r_hash: str | None = Query(None, description="The hash of the preimage.") value_msat: int = Query( ..., description="The value of this invoice in milli satoshis." @@ -517,21 +518,23 @@ class Invoice(BaseModel): settled: bool = Query(False, description="Whether this invoice has been fulfilled") - creation_date: int = Query( + creation_date: int | None = Query( None, description="When this invoice was created. Not available with CLN.", ) - settle_date: int = Query( + settle_date: int | None = Query( None, description=( "When this invoice was settled. " "Not available with pending invoices." ), ) - expiry_date: int = Query(None, description="The time at which this invoice expires") + expiry_date: int | None = Query( + None, description="The time at which this invoice expires" + ) - payment_request: str = Query( + payment_request: str | None = Query( None, description=( "A bare-bones invoice for a payment within the " @@ -540,7 +543,7 @@ class Invoice(BaseModel): ), ) - description_hash: str = Query( + description_hash: str | None = Query( None, description=( "Hash(SHA-256) of a description of the payment. Used if the description of " @@ -549,21 +552,21 @@ class Invoice(BaseModel): ), ) - expiry: int = Query( + expiry: int | None = Query( None, description="Payment request expiry time in seconds. Default is 3600 (1 hour).", ) - fallback_addr: str = Query(None, description="Fallback on-chain address.") + fallback_addr: str | None = Query(None, description="Fallback on-chain address.") - cltv_expiry: int = Query( + cltv_expiry: int | None = Query( None, description=( "Delta to use for the time-lock of the CLTV extended to the final hop." ), ) - route_hints: List[RouteHint] = Query( + route_hints: List[RouteHint] | None = Query( None, description=( "Route hints that can each be individually used to assist " @@ -571,7 +574,7 @@ class Invoice(BaseModel): ), ) - private: bool = Query( + private: bool | None = Query( None, description=( "Whether this invoice should include routing hints for private channels." @@ -598,7 +601,7 @@ class Invoice(BaseModel): ), ) - settle_index: int = Query( + settle_index: int | None = Query( None, description=( "The `settle` index of this invoice. Each newly settled invoice will " @@ -606,7 +609,7 @@ class Invoice(BaseModel): ), ) - amt_paid_sat: int = Query( + amt_paid_sat: int | None = Query( None, description=( "The amount that was accepted for this invoice, in satoshis. This " @@ -618,7 +621,7 @@ class Invoice(BaseModel): ), ) - amt_paid_msat: int = Query( + amt_paid_msat: int | None = Query( None, description=( "The amount that was accepted for this invoice, in millisatoshis. " @@ -632,15 +635,15 @@ class Invoice(BaseModel): state: InvoiceState = Query(..., description="The state the invoice is in.") - htlcs: List[InvoiceHTLC] = Query( + htlcs: List[InvoiceHTLC] | None = Query( None, description="List of HTLCs paying to this invoice[EXPERIMENTAL]." ) - features: List[FeaturesEntry] = Query( + features: List[FeaturesEntry] | None = Query( None, description="List of features advertised on the invoice." ) - is_keysend: bool = Query( + is_keysend: bool | None = Query( None, description=( "[LND only] Indicates if this invoice was a spontaneous payment " @@ -648,7 +651,7 @@ class Invoice(BaseModel): ), ) - payment_addr: str = Query( + payment_addr: str | None = Query( None, description=( "The payment address of this invoice. This value will be used " @@ -657,7 +660,7 @@ class Invoice(BaseModel): ), ) - is_amp: bool = Query( + is_amp: bool | None = Query( None, description="Signals whether or not this is an AMP invoice." ) @@ -676,7 +679,6 @@ class Invoice(BaseModel): memo=i.memo, r_preimage=i.r_preimage.hex(), r_hash=i.r_hash.hex(), - value=i.value, value_msat=i.value_msat, settled=i.settled, creation_date=i.creation_date, @@ -689,7 +691,7 @@ class Invoice(BaseModel): cltv_expiry=i.cltv_expiry, route_hints=_route_hints(i.route_hints), private=i.private, - add_index=i.add_index, + add_index=str(i.add_index), settle_index=i.settle_index, amt_paid_sat=i.amt_paid_sat, amt_paid_msat=i.amt_paid_msat, @@ -705,11 +707,10 @@ class Invoice(BaseModel): def from_cln_json(cls, i) -> "Invoice": amt = parse_cln_msat(i["amount_msat"]) return cls( - add_index=i["label"], + add_index=str(i["label"]), memo=i["description"], r_preimage=i["payment_preimage"] if "payment_preimage" in i else None, r_hash=i["payment_hash"], - value=amt / 1000, value_msat=amt, settled=True if i["status"] == "paid" else False, expiry_date=i["expires_at"], @@ -717,7 +718,7 @@ class Invoice(BaseModel): payment_request=i["bolt11"], settle_index=i["pay_index"] if "pay_index" in i else None, amt_paid_sat=( - parse_cln_msat(i["amount_received_msat"]) / 1000 + round(parse_cln_msat(i["amount_received_msat"]) / 1000) if "amount_received_msat" in i else None ), @@ -737,7 +738,6 @@ class Invoice(BaseModel): memo=i.description, r_preimage=i.payment_preimage.hex(), r_hash=i.payment_hash.hex(), - value=i.amount_msat.msat / 1000, value_msat=i.amount_msat.msat, settled=True if state == InvoiceState.SETTLED else False, expiry_date=i.expires_at, @@ -1208,6 +1208,7 @@ class Payment(BaseModel): def from_cln_jrpc(cls, p) -> "Payment": value = parse_cln_msat(p["amount_msat"]) total_sent = parse_cln_msat(p["amount_sent_msat"]) + ts = time.localtime(p["created_at"]) return cls( payment_hash=p["payment_hash"], @@ -1216,7 +1217,7 @@ class Payment(BaseModel): payment_request=p["bolt11"] if "bolt11" in p else "", status=PaymentStatus.from_cln_jrpc(p["status"]), fee_msat=total_sent - value, - creation_time_ns=p["created_at"], + creation_time_ns=ts.tm_sec, label=p["label"] if "label" in p else "", failure_reason=PaymentFailureReason.from_cln_jrpc(p), ) @@ -1623,10 +1624,10 @@ class LightningInfoLite(BaseModel): block_height: int = Query( ..., description="The node's current view of the height of the best block" ) - synced_to_chain: bool = Query( + synced_to_chain: bool | None = Query( None, description="Whether the wallet's view is synced to the main chain" ) - synced_to_graph: bool = Query( + synced_to_graph: bool | None = Query( None, description=( "Whether we consider ourselves synced with " "the public channel graph." @@ -1906,7 +1907,8 @@ class GenericTx(BaseModel): "category **onchain**." ), ) - num_confs: Union[int, None] = Query( + num_confs: int | None = Query( + None, ge=0, description=( "Number of confirmations. Only applicable for category **onchain**." @@ -1931,7 +1933,7 @@ class GenericTx(BaseModel): amount = i.value_msat return cls( - id=i.payment_request, + id=i.payment_request or "", category=TxCategory.LIGHTNING, type=TxType.RECEIVE, amount=amount, diff --git a/app/main.py b/app/main.py index dcf2133..96ee288 100644 --- a/app/main.py +++ b/app/main.py @@ -99,7 +99,7 @@ async def lifespan(app: FastAPI): remove_local_cookie() -app = FastAPI() +app = FastAPI(lifespan=lifespan) app.include_router(app_router) app.include_router(bitcoin_router) if node_type != "none": diff --git a/app/system/models.py b/app/system/models.py index b42a054..7a150a5 100644 --- a/app/system/models.py +++ b/app/system/models.py @@ -12,7 +12,7 @@ from app.system.docs import get_debug_data_sample_str class LoginInput(BaseModel): password: constr(min_length=8) one_time_password: Optional[ - constr(min_length=6, max_length=6, regex="^[0-9]+$") + constr(min_length=6, max_length=6, pattern="^[0-9]+$") ] = None @@ -45,7 +45,7 @@ class SystemInfo(BaseModel): "", description="The version of this platform", ) - code_version = Query( + code_version: str = Query( "", description="[RaspiBlitz only] The code version.", )