[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>
This commit is contained in:
parent
c536df0dae
commit
eb73daffe9
30 changed files with 2996 additions and 7 deletions
|
|
@ -102,6 +102,14 @@ async def to_user():
|
|||
yield user
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def from_super_user(from_user):
|
||||
prev = settings.super_user
|
||||
settings.super_user = from_user.id
|
||||
yield from_user
|
||||
settings.super_user = prev
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="session")
|
||||
async def superuser():
|
||||
user = await get_user(settings.super_user)
|
||||
|
|
|
|||
171
tests/core/views/test_node_api.py
Normal file
171
tests/core/views/test_node_api.py
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
import asyncio
|
||||
import random
|
||||
from http import HTTPStatus
|
||||
|
||||
import pytest
|
||||
from pydantic import parse_obj_as
|
||||
|
||||
from lnbits import bolt11
|
||||
from lnbits.nodes.base import ChannelPoint, ChannelState, NodeChannel
|
||||
from tests.conftest import pytest_asyncio, settings
|
||||
|
||||
from ...helpers import (
|
||||
WALLET,
|
||||
get_random_invoice_data,
|
||||
get_unconnected_node_uri,
|
||||
mine_blocks,
|
||||
)
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
WALLET.__node_cls__ is None, reason="Cant test if node implementation isnt avilable"
|
||||
)
|
||||
|
||||
|
||||
@pytest_asyncio.fixture()
|
||||
async def node_client(client, from_super_user):
|
||||
settings.lnbits_node_ui = True
|
||||
settings.lnbits_public_node_ui = False
|
||||
settings.lnbits_node_ui_transactions = True
|
||||
params = client.params
|
||||
client.params = {"usr": from_super_user.id}
|
||||
yield client
|
||||
client.params = params
|
||||
settings.lnbits_node_ui = False
|
||||
|
||||
|
||||
@pytest_asyncio.fixture()
|
||||
async def public_node_client(node_client):
|
||||
settings.lnbits_public_node_ui = True
|
||||
yield node_client
|
||||
settings.lnbits_public_node_ui = False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_node_info_not_found(client):
|
||||
response = await client.get("/node/api/v1/info")
|
||||
assert response.status_code == HTTPStatus.SERVICE_UNAVAILABLE
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_public_node_info_not_found(node_client):
|
||||
response = await node_client.get("/node/public/api/v1/info")
|
||||
assert response.status_code == HTTPStatus.SERVICE_UNAVAILABLE
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_public_node_info(public_node_client):
|
||||
response = await public_node_client.get("/node/public/api/v1/info")
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_node_info(node_client, from_super_user):
|
||||
response = await node_client.get("/node/api/v1/info")
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_node_invoices(inkey_headers_from, node_client):
|
||||
data = await get_random_invoice_data()
|
||||
response = await node_client.post(
|
||||
"/api/v1/payments", json=data, headers=inkey_headers_from
|
||||
)
|
||||
invoice = response.json()
|
||||
|
||||
response = await node_client.get("/node/api/v1/invoices", params={"limit": 1})
|
||||
assert response.status_code == 200
|
||||
invoices = response.json()["data"]
|
||||
assert len(invoices) == 1
|
||||
assert invoices[0]["payment_hash"] == invoice["payment_hash"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_node_payments(node_client, real_invoice, adminkey_headers_from):
|
||||
response = await node_client.post(
|
||||
"/api/v1/payments", json=real_invoice, headers=adminkey_headers_from
|
||||
)
|
||||
assert response.status_code < 300
|
||||
|
||||
response = await node_client.get("/node/api/v1/payments", params={"limit": 1})
|
||||
assert response.status_code == 200
|
||||
payments = response.json()["data"]
|
||||
assert len(payments) == 1
|
||||
assert (
|
||||
payments[0]["payment_hash"]
|
||||
== bolt11.decode(real_invoice["bolt11"]).payment_hash
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_channel_management(node_client):
|
||||
async def get_channels():
|
||||
response = await node_client.get("/node/api/v1/channels")
|
||||
assert response.status_code == 200
|
||||
return parse_obj_as(list[NodeChannel], response.json())
|
||||
|
||||
data = await get_channels()
|
||||
close = random.choice(
|
||||
[channel for channel in data if channel.state == ChannelState.ACTIVE]
|
||||
)
|
||||
assert close, "No active channel found"
|
||||
|
||||
response = await node_client.delete(
|
||||
"/node/api/v1/channels",
|
||||
params={"short_id": close.short_id, **close.point.dict()},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = await get_channels()
|
||||
assert any(
|
||||
channel.point == close.point and channel.state == ChannelState.CLOSED
|
||||
for channel in data
|
||||
)
|
||||
|
||||
response = await node_client.post(
|
||||
"/node/api/v1/channels",
|
||||
json={
|
||||
"peer_id": close.peer_id,
|
||||
"funding_amount": 100000,
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
created = ChannelPoint(**response.json())
|
||||
data = await get_channels()
|
||||
assert any(
|
||||
channel.point == created and channel.state == ChannelState.PENDING
|
||||
for channel in data
|
||||
)
|
||||
|
||||
# mine some blocks so that the newly created channel eventually
|
||||
# gets confirmed to avoid a situation where no channels are
|
||||
# left for testing
|
||||
mine_blocks(5)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_peer_management(node_client):
|
||||
connect_uri = get_unconnected_node_uri()
|
||||
id = connect_uri.split("@")[0]
|
||||
response = await node_client.post("/node/api/v1/peers", json={"uri": connect_uri})
|
||||
assert response.status_code == 200
|
||||
|
||||
response = await node_client.get("/node/api/v1/peers")
|
||||
assert response.status_code == 200
|
||||
assert any(peer["id"] == id for peer in response.json())
|
||||
|
||||
response = await node_client.delete(f"/node/api/v1/peers/{id}")
|
||||
assert response.status_code == 200
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
response = await node_client.get("/node/api/v1/peers")
|
||||
assert response.status_code == 200
|
||||
assert not any(peer["id"] == id for peer in response.json())
|
||||
|
||||
response = await node_client.delete(f"/node/api/v1/peers/{id}")
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_connect_invalid_uri(node_client):
|
||||
response = await node_client.post("/node/api/v1/peers", json={"uri": "invalid"})
|
||||
assert response.status_code == 400
|
||||
|
|
@ -53,6 +53,17 @@ docker_bitcoin_cli = [
|
|||
]
|
||||
|
||||
|
||||
docker_lightning_unconnected_cli = [
|
||||
"docker",
|
||||
"exec",
|
||||
"lnbits-legend-lnd-2-1",
|
||||
"lncli",
|
||||
"--network",
|
||||
"regtest",
|
||||
"--rpcserver=lnd-2",
|
||||
]
|
||||
|
||||
|
||||
def run_cmd(cmd: list) -> str:
|
||||
timeout = 20
|
||||
process = Popen(cmd, stdout=PIPE, stderr=PIPE)
|
||||
|
|
@ -124,6 +135,14 @@ def mine_blocks(blocks: int = 1) -> str:
|
|||
return run_cmd(cmd)
|
||||
|
||||
|
||||
def get_unconnected_node_uri() -> str:
|
||||
cmd = docker_lightning_unconnected_cli.copy()
|
||||
cmd.append("getinfo")
|
||||
info = run_cmd_json(cmd)
|
||||
pubkey = info["identity_pubkey"]
|
||||
return f"{pubkey}@lnd-2:9735"
|
||||
|
||||
|
||||
def create_onchain_address(address_type: str = "bech32") -> str:
|
||||
cmd = docker_bitcoin_cli.copy()
|
||||
cmd.extend(["getnewaddress", address_type])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue