[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:
dni ⚡ 2023-09-25 15:04:44 +02:00 committed by GitHub
parent c536df0dae
commit eb73daffe9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 2996 additions and 7 deletions

View file

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

View 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

View file

@ -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])