lnbits-legend/lnbits/core/views/node_api.py
dni ⚡ eb73daffe9
[FEAT] Node Managment (#1895)
* [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>
2023-09-25 15:04:44 +02:00

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")