mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2025-02-25 07:07:48 +01:00
* [FEAT] Node Managment feat: node dashboard channels and transactions fix: update channel variables better types refactor ui add onchain balances and backend_name mock values for fake wallet remove app tab start implementing peers and channel management peer and channel management implement channel closing add channel states, better errors seperate payments and invoices on transactions tab display total channel balance feat: optional public page feat: show node address fix: port conversion feat: details dialog on transactions fix: peer info without alias fix: rename channel balances small improvements to channels tab feat: pagination on transactions tab test caching transactions refactor: move WALLET into wallets module fix: backwards compatibility refactor: move get_node_class to nodes modules post merge bundle fundle feat: disconnect peer feat: initial lnd support only use filtered channels for total balance adjust closing logic add basic node tests add setting for disabling transactions tab revert unnecessary changes add tests for invoices and payments improve payment and invoice implementations the previously used invoice fixture has a session scope, but a new invoice is required tests and bug fixes for channels api use query instead of body in channel delete delete requests should generally not use a body take node id through path instead of body for delete endpoint add peer management tests more tests for errors improve error handling rename id and pubkey to peer_id for consistency remove dead code fix http status codes make cache keys safer cache node public info comments for node settings rename node prop in frontend adjust tests to new status codes cln: use amount_msat instead of value for onchain balance turn transactions tab off by default enable transactions in tests only allow super user to create or delete fix prop name in admin navbar --------- Co-authored-by: jacksn <jkranawetter05@gmail.com>
188 lines
5.5 KiB
Python
188 lines
5.5 KiB
Python
from http import HTTPStatus
|
|
from typing import List, Optional
|
|
|
|
import httpx
|
|
from fastapi import APIRouter, Body, Depends, HTTPException
|
|
from pydantic import BaseModel
|
|
from starlette.status import HTTP_503_SERVICE_UNAVAILABLE
|
|
|
|
from lnbits.decorators import check_admin, check_super_user, parse_filters
|
|
from lnbits.nodes import get_node_class
|
|
from lnbits.settings import settings
|
|
|
|
from ...db import Filters, Page
|
|
from ...nodes.base import (
|
|
ChannelPoint,
|
|
Node,
|
|
NodeChannel,
|
|
NodeInfoResponse,
|
|
NodeInvoice,
|
|
NodeInvoiceFilters,
|
|
NodePayment,
|
|
NodePaymentsFilters,
|
|
NodePeerInfo,
|
|
PublicNodeInfo,
|
|
)
|
|
from ...utils.cache import cache
|
|
|
|
|
|
def require_node():
|
|
NODE = get_node_class()
|
|
if not NODE:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.NOT_IMPLEMENTED,
|
|
detail="Active backend does not implement Node API",
|
|
)
|
|
if not settings.lnbits_node_ui:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
|
|
detail="Not enabled",
|
|
)
|
|
return NODE
|
|
|
|
|
|
def check_public():
|
|
if not (settings.lnbits_node_ui and settings.lnbits_public_node_ui):
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
|
|
detail="Not enabled",
|
|
)
|
|
|
|
|
|
node_router = APIRouter(prefix="/node/api/v1", dependencies=[Depends(check_admin)])
|
|
super_node_router = APIRouter(
|
|
prefix="/node/api/v1", dependencies=[Depends(check_super_user)]
|
|
)
|
|
public_node_router = APIRouter(
|
|
prefix="/node/public/api/v1", dependencies=[Depends(check_public)]
|
|
)
|
|
|
|
|
|
@node_router.get(
|
|
"/ok",
|
|
description="Check if node api can be enabled",
|
|
status_code=200,
|
|
dependencies=[Depends(require_node)],
|
|
)
|
|
async def api_get_ok():
|
|
pass
|
|
|
|
|
|
@public_node_router.get("/info", response_model=PublicNodeInfo)
|
|
async def api_get_public_info(node: Node = Depends(require_node)) -> PublicNodeInfo:
|
|
return await cache.save_result(node.get_public_info, key="node:public_info")
|
|
|
|
|
|
@node_router.get("/info")
|
|
async def api_get_info(
|
|
node: Node = Depends(require_node),
|
|
) -> Optional[NodeInfoResponse]:
|
|
return await node.get_info()
|
|
|
|
|
|
@node_router.get("/channels")
|
|
async def api_get_channels(
|
|
node: Node = Depends(require_node),
|
|
) -> Optional[List[NodeChannel]]:
|
|
return await node.get_channels()
|
|
|
|
|
|
@super_node_router.post("/channels", response_model=ChannelPoint)
|
|
async def api_create_channel(
|
|
node: Node = Depends(require_node),
|
|
peer_id: str = Body(),
|
|
funding_amount: int = Body(),
|
|
push_amount: Optional[int] = Body(None),
|
|
fee_rate: Optional[int] = Body(None),
|
|
):
|
|
return await node.open_channel(peer_id, funding_amount, push_amount, fee_rate)
|
|
|
|
|
|
@super_node_router.delete("/channels")
|
|
async def api_delete_channel(
|
|
short_id: Optional[str],
|
|
funding_txid: Optional[str],
|
|
output_index: Optional[int],
|
|
force: bool = False,
|
|
node: Node = Depends(require_node),
|
|
) -> Optional[List[NodeChannel]]:
|
|
return await node.close_channel(
|
|
short_id,
|
|
ChannelPoint(funding_txid=funding_txid, output_index=output_index)
|
|
if funding_txid is not None and output_index is not None
|
|
else None,
|
|
force,
|
|
)
|
|
|
|
|
|
@node_router.get("/payments", response_model=Page[NodePayment])
|
|
async def api_get_payments(
|
|
node: Node = Depends(require_node),
|
|
filters: Filters = Depends(parse_filters(NodePaymentsFilters)),
|
|
) -> Optional[Page[NodePayment]]:
|
|
if not settings.lnbits_node_ui_transactions:
|
|
raise HTTPException(
|
|
HTTP_503_SERVICE_UNAVAILABLE,
|
|
detail="You can enable node transactions in the Admin UI",
|
|
)
|
|
return await node.get_payments(filters)
|
|
|
|
|
|
@node_router.get("/invoices", response_model=Page[NodeInvoice])
|
|
async def api_get_invoices(
|
|
node: Node = Depends(require_node),
|
|
filters: Filters = Depends(parse_filters(NodeInvoiceFilters)),
|
|
) -> Optional[Page[NodeInvoice]]:
|
|
if not settings.lnbits_node_ui_transactions:
|
|
raise HTTPException(
|
|
HTTP_503_SERVICE_UNAVAILABLE,
|
|
detail="You can enable node transactions in the Admin UI",
|
|
)
|
|
return await node.get_invoices(filters)
|
|
|
|
|
|
@node_router.get("/peers", response_model=List[NodePeerInfo])
|
|
async def api_get_peers(node: Node = Depends(require_node)) -> List[NodePeerInfo]:
|
|
return await node.get_peers()
|
|
|
|
|
|
@super_node_router.post("/peers")
|
|
async def api_connect_peer(
|
|
uri: str = Body(embed=True), node: Node = Depends(require_node)
|
|
):
|
|
return await node.connect_peer(uri)
|
|
|
|
|
|
@super_node_router.delete("/peers/{peer_id}")
|
|
async def api_disconnect_peer(peer_id: str, node: Node = Depends(require_node)):
|
|
return await node.disconnect_peer(peer_id)
|
|
|
|
|
|
class NodeRank(BaseModel):
|
|
capacity: Optional[int]
|
|
channelcount: Optional[int]
|
|
age: Optional[int]
|
|
growth: Optional[int]
|
|
availability: Optional[int]
|
|
|
|
|
|
# Same for public and private api
|
|
@node_router.get(
|
|
"/rank",
|
|
description="Retrieve node ranks from https://1ml.com",
|
|
response_model=Optional[NodeRank],
|
|
)
|
|
@public_node_router.get(
|
|
"/rank",
|
|
description="Retrieve node ranks from https://1ml.com",
|
|
response_model=Optional[NodeRank],
|
|
)
|
|
async def api_get_1ml_stats(node: Node = Depends(require_node)) -> Optional[NodeRank]:
|
|
node_id = await node.get_id()
|
|
async with httpx.AsyncClient() as client:
|
|
r = await client.get(url=f"https://1ml.com/node/{node_id}/json", timeout=15)
|
|
try:
|
|
r.raise_for_status()
|
|
return r.json()["noderank"]
|
|
except httpx.HTTPStatusError:
|
|
raise HTTPException(status_code=404, detail="Node not found on 1ml.com")
|