from __future__ import annotations from abc import ABC, abstractmethod from enum import Enum from typing import TYPE_CHECKING, Optional from pydantic import BaseModel from lnbits.db import FilterModel, Filters, Page from lnbits.utils.cache import cache if TYPE_CHECKING: from lnbits.wallets.base import Wallet class NodePeerInfo(BaseModel): id: str alias: Optional[str] = None color: Optional[str] = None last_timestamp: Optional[int] = None addresses: Optional[list[str]] = None class ChannelState(Enum): ACTIVE = "active" PENDING = "pending" CLOSED = "closed" INACTIVE = "inactive" class ChannelBalance(BaseModel): local_msat: int remote_msat: int total_msat: int class ChannelPoint(BaseModel): funding_txid: str output_index: int class NodeChannel(BaseModel): short_id: Optional[str] = None point: Optional[ChannelPoint] = None peer_id: str balance: ChannelBalance state: ChannelState name: Optional[str] color: Optional[str] class ChannelStats(BaseModel): counts: dict[ChannelState, int] avg_size: int biggest_size: Optional[int] smallest_size: Optional[int] total_capacity: int @classmethod def from_list(cls, channels: list[NodeChannel]): counts: dict[ChannelState, int] = {} for channel in channels: counts[channel.state] = counts.get(channel.state, 0) + 1 active_channel_sizes = [ channel.balance.total_msat for channel in channels if channel.state == ChannelState.ACTIVE ] if len(active_channel_sizes) > 0: return cls( counts=counts, avg_size=int(sum(active_channel_sizes) / len(active_channel_sizes)), biggest_size=max(active_channel_sizes), smallest_size=min(active_channel_sizes), total_capacity=sum(active_channel_sizes), ) else: return cls( counts=counts, avg_size=0, biggest_size=0, smallest_size=0, total_capacity=0, ) class NodeFees(BaseModel): total_msat: int daily_msat: Optional[int] = None weekly_msat: Optional[int] = None monthly_msat: Optional[int] = None class PublicNodeInfo(BaseModel): id: str backend_name: str alias: str color: str num_peers: int blockheight: int channel_stats: ChannelStats addresses: list[str] class NodeInfoResponse(PublicNodeInfo): onchain_balance_sat: int onchain_confirmed_sat: int fees: NodeFees balance_msat: int class NodePayment(BaseModel): pending: bool amount: int fee: Optional[int] = None memo: Optional[str] = None time: int bolt11: Optional[str] = None preimage: Optional[str] payment_hash: str expiry: Optional[float] = None destination: Optional[NodePeerInfo] = None class NodeInvoice(BaseModel): pending: bool amount: int memo: Optional[str] bolt11: str preimage: Optional[str] payment_hash: str paid_at: Optional[int] = None expiry: Optional[int] = None class NodeInvoiceFilters(FilterModel): pass class NodePaymentsFilters(FilterModel): pass class Node(ABC): wallet: Wallet def __init__(self, wallet: Wallet): self.wallet = wallet self.id: Optional[str] = None @property def name(self): return self.__class__.__name__ async def get_id(self): if not self.id: self.id = await self._get_id() return self.id @abstractmethod async def _get_id(self) -> str: pass async def get_peers(self) -> list[NodePeerInfo]: peer_ids = await self.get_peer_ids() return [await self.get_peer_info(peer_id) for peer_id in peer_ids] @abstractmethod async def get_peer_ids(self) -> list[str]: pass @abstractmethod async def connect_peer(self, uri: str): pass @abstractmethod async def disconnect_peer(self, peer_id: str): pass @abstractmethod async def _get_peer_info(self, peer_id: str) -> NodePeerInfo: pass async def get_peer_info(self, peer_id: str) -> NodePeerInfo: key = f"node:peers:{peer_id}" info = cache.get(key) if not info: info = await self._get_peer_info(peer_id) if info.last_timestamp: cache.set(key, info) return info @abstractmethod async def open_channel( self, peer_id: str, local_amount: int, push_amount: Optional[int] = None, fee_rate: Optional[int] = None, ) -> ChannelPoint: pass @abstractmethod async def close_channel( self, short_id: Optional[str] = None, point: Optional[ChannelPoint] = None, force: bool = False, ): pass @abstractmethod async def get_channels(self) -> list[NodeChannel]: pass @abstractmethod async def get_info(self) -> NodeInfoResponse: pass async def get_public_info(self) -> PublicNodeInfo: info = await self.get_info() return PublicNodeInfo(**info.__dict__) @abstractmethod async def get_payments( self, filters: Filters[NodePaymentsFilters] ) -> Page[NodePayment]: pass @abstractmethod async def get_invoices( self, filters: Filters[NodeInvoiceFilters] ) -> Page[NodeInvoice]: pass