Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39b1f540b5 | ||
|
|
34bf834dc6 | ||
|
|
7db5c986b3 | ||
|
|
26c31b626d | ||
|
|
e39cb20525 | ||
|
|
3cea9a90d1 | ||
|
|
3224e2e774 |
@@ -51,7 +51,7 @@ nav_order: 1
|
|||||||
sudo apt-get install jq libfuse2
|
sudo apt-get install jq libfuse2
|
||||||
wget $(curl -s https://api.github.com/repos/lnbits/lnbits/releases/latest | jq -r '.assets[] | select(.name | endswith(".AppImage")) | .browser_download_url') -O LNbits-latest.AppImage
|
wget $(curl -s https://api.github.com/repos/lnbits/lnbits/releases/latest | jq -r '.assets[] | select(.name | endswith(".AppImage")) | .browser_download_url') -O LNbits-latest.AppImage
|
||||||
chmod +x LNbits-latest.AppImage
|
chmod +x LNbits-latest.AppImage
|
||||||
LNBITS_ADMIN_UI=true HOST=0.0.0.0 PORT=5000 ./LNbits-latest.AppImage # most system settings are now in the admin UI, but pass additional .env variables here
|
LNBITS_ADMIN_UI=true HOST=0.0.0.0 PORT=5000 AUTH_HTTPS_ONLY=false ./LNbits-latest.AppImage # most system settings are now in the admin UI, but pass additional .env variables here
|
||||||
```
|
```
|
||||||
|
|
||||||
- LNbits will create a folder for DB and extension files **in the same directory** as the AppImage.
|
- LNbits will create a folder for DB and extension files **in the same directory** as the AppImage.
|
||||||
@@ -285,10 +285,7 @@ but you can also set the env variables or pass command line arguments:
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
# .env variables are currently passed when running, but LNbits can be managed with the admin UI.
|
# .env variables are currently passed when running, but LNbits can be managed with the admin UI.
|
||||||
LNBITS_ADMIN_UI=true ./result/bin/lnbits --port 9000 --host 0.0.0.0
|
LNBITS_ADMIN_UI=true AUTH_HTTPS_ONLY=false ./result/bin/lnbits --port 9000 --host 0.0.0.0
|
||||||
|
|
||||||
# Once you have created a user, you can set as the super_user
|
|
||||||
SUPER_USER=be54db7f245346c8833eaa430e1e0405 LNBITS_ADMIN_UI=true ./result/bin/lnbits --port 9000
|
|
||||||
```
|
```
|
||||||
|
|
||||||
> 
|
> 
|
||||||
|
|||||||
@@ -290,6 +290,7 @@ async def create_payment(
|
|||||||
webhook=data.webhook,
|
webhook=data.webhook,
|
||||||
fee=-abs(data.fee),
|
fee=-abs(data.fee),
|
||||||
tag=extra.get("tag", None),
|
tag=extra.get("tag", None),
|
||||||
|
extension=data.extension,
|
||||||
extra=extra,
|
extra=extra,
|
||||||
labels=data.labels or [],
|
labels=data.labels or [],
|
||||||
external_id=data.external_id,
|
external_id=data.external_id,
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ class CreatePayment(BaseModel):
|
|||||||
amount_msat: int
|
amount_msat: int
|
||||||
memo: str
|
memo: str
|
||||||
extra: dict | None = {}
|
extra: dict | None = {}
|
||||||
|
extension: str | None = None
|
||||||
preimage: str | None = None
|
preimage: str | None = None
|
||||||
expiry: datetime | None = None
|
expiry: datetime | None = None
|
||||||
webhook: str | None = None
|
webhook: str | None = None
|
||||||
@@ -259,6 +260,7 @@ class CreateInvoice(BaseModel):
|
|||||||
)
|
)
|
||||||
expiry: int | None = None
|
expiry: int | None = None
|
||||||
extra: dict | None = None
|
extra: dict | None = None
|
||||||
|
extension: str | None = None
|
||||||
webhook: str | None = None
|
webhook: str | None = None
|
||||||
bolt11: str | None = None
|
bolt11: str | None = None
|
||||||
lnurl_withdraw: LnurlWithdrawResponse | None = None
|
lnurl_withdraw: LnurlWithdrawResponse | None = None
|
||||||
|
|||||||
@@ -214,6 +214,7 @@ async def create_wallet_invoice(wallet_id: str, data: CreateInvoice) -> Payment:
|
|||||||
unhashed_description=unhashed_description,
|
unhashed_description=unhashed_description,
|
||||||
expiry=data.expiry,
|
expiry=data.expiry,
|
||||||
extra=data.extra,
|
extra=data.extra,
|
||||||
|
extension=data.extension,
|
||||||
webhook=data.webhook,
|
webhook=data.webhook,
|
||||||
internal=data.internal,
|
internal=data.internal,
|
||||||
payment_hash=data.payment_hash,
|
payment_hash=data.payment_hash,
|
||||||
@@ -259,6 +260,7 @@ async def create_invoice(
|
|||||||
webhook: str | None = None,
|
webhook: str | None = None,
|
||||||
internal: bool | None = False,
|
internal: bool | None = False,
|
||||||
payment_hash: str | None = None,
|
payment_hash: str | None = None,
|
||||||
|
extension: str | None = None,
|
||||||
labels: list[str] | None = None,
|
labels: list[str] | None = None,
|
||||||
external_id: str | None = None,
|
external_id: str | None = None,
|
||||||
conn: Connection | None = None,
|
conn: Connection | None = None,
|
||||||
@@ -342,6 +344,7 @@ async def create_invoice(
|
|||||||
expiry=invoice.expiry_date,
|
expiry=invoice.expiry_date,
|
||||||
memo=memo,
|
memo=memo,
|
||||||
extra=extra,
|
extra=extra,
|
||||||
|
extension=extension,
|
||||||
webhook=webhook,
|
webhook=webhook,
|
||||||
fee=invoice_response.fee_msat or 0,
|
fee=invoice_response.fee_msat or 0,
|
||||||
labels=labels,
|
labels=labels,
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ from lnbits.settings import set_cli_settings, settings
|
|||||||
@click.option(
|
@click.option(
|
||||||
"--reload", is_flag=True, default=False, help="Enable auto-reload for development"
|
"--reload", is_flag=True, default=False, help="Enable auto-reload for development"
|
||||||
)
|
)
|
||||||
|
@click.option("--ws-max-queue", default=128, help="Websocket max queue size")
|
||||||
|
@click.option("--ws-ping-timeout", default=900.0, help="Websocket ping timeout")
|
||||||
def main(
|
def main(
|
||||||
port: int,
|
port: int,
|
||||||
host: str,
|
host: str,
|
||||||
@@ -34,6 +36,8 @@ def main(
|
|||||||
ssl_keyfile: str,
|
ssl_keyfile: str,
|
||||||
ssl_certfile: str,
|
ssl_certfile: str,
|
||||||
reload: bool,
|
reload: bool,
|
||||||
|
ws_max_queue: int,
|
||||||
|
ws_ping_timeout: float
|
||||||
):
|
):
|
||||||
"""Launched with `uv run lnbits` at root level"""
|
"""Launched with `uv run lnbits` at root level"""
|
||||||
|
|
||||||
@@ -58,6 +62,8 @@ def main(
|
|||||||
ssl_keyfile=ssl_keyfile,
|
ssl_keyfile=ssl_keyfile,
|
||||||
ssl_certfile=ssl_certfile,
|
ssl_certfile=ssl_certfile,
|
||||||
reload=reload or False,
|
reload=reload or False,
|
||||||
|
ws_ping_timeout=ws_ping_timeout,
|
||||||
|
ws_max_queue=ws_max_queue,
|
||||||
)
|
)
|
||||||
|
|
||||||
server = uvicorn.Server(config=config)
|
server = uvicorn.Server(config=config)
|
||||||
|
|||||||
+921
-138
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "lnbits"
|
name = "lnbits"
|
||||||
version = "1.5.5-rc3"
|
version = "1.5.5"
|
||||||
requires-python = ">=3.10,<3.13"
|
requires-python = ">=3.10,<3.13"
|
||||||
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
||||||
authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
|
authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import asyncio
|
||||||
import base64
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
@@ -12,7 +13,7 @@ from Cryptodome.Util.Padding import pad, unpad
|
|||||||
from websockets import ServerConnection
|
from websockets import ServerConnection
|
||||||
from websockets import serve as ws_serve
|
from websockets import serve as ws_serve
|
||||||
|
|
||||||
from lnbits.wallets.nwc import NWCWallet
|
from lnbits.wallets.nwc import NWCConnection, NWCWallet
|
||||||
from tests.wallets.helpers import (
|
from tests.wallets.helpers import (
|
||||||
WalletTest,
|
WalletTest,
|
||||||
build_test_id,
|
build_test_id,
|
||||||
@@ -99,6 +100,8 @@ async def handle( # noqa: C901
|
|||||||
event,
|
event,
|
||||||
)
|
)
|
||||||
await websocket.send(json.dumps(["EVENT", sub_id, event]))
|
await websocket.send(json.dumps(["EVENT", sub_id, event]))
|
||||||
|
elif 23195 in kinds:
|
||||||
|
assert sub_filter["authors"] == [mock_settings["service_public_key"]]
|
||||||
elif msg[0] == "EVENT":
|
elif msg[0] == "EVENT":
|
||||||
event = msg[1]
|
event = msg[1]
|
||||||
decrypted_content = decrypt_content(
|
decrypted_content = decrypt_content(
|
||||||
@@ -177,6 +180,129 @@ async def run(data: WalletTest):
|
|||||||
await nwcwallet.cleanup()
|
await nwcwallet.cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_nwc_rejects_event_from_unexpected_pubkey(mocker):
|
||||||
|
async def _noop(*args, **kwargs):
|
||||||
|
return None
|
||||||
|
|
||||||
|
mocker.patch("lnbits.wallets.nwc.NWCConnection._connect_to_relay", new=_noop)
|
||||||
|
mocker.patch("lnbits.wallets.nwc.NWCConnection._handle_timeouts", new=_noop)
|
||||||
|
|
||||||
|
service_private_key = PrivateKey()
|
||||||
|
service_public_key = service_private_key.public_key.format().hex()[2:]
|
||||||
|
attacker_private_key = PrivateKey()
|
||||||
|
attacker_public_key = attacker_private_key.public_key.format().hex()[2:]
|
||||||
|
account_private_key = PrivateKey()
|
||||||
|
|
||||||
|
conn = NWCConnection(
|
||||||
|
service_public_key,
|
||||||
|
account_private_key.secret.hex(),
|
||||||
|
"ws://127.0.0.1:8555",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
event = {
|
||||||
|
"kind": 23195,
|
||||||
|
"content": "{}",
|
||||||
|
"created_at": int(time.time()),
|
||||||
|
"tags": [["e", "request-event-id"]],
|
||||||
|
}
|
||||||
|
sign_event(attacker_public_key, attacker_private_key.secret.hex(), event)
|
||||||
|
|
||||||
|
with pytest.raises(Exception, match="Invalid event signature"):
|
||||||
|
await conn._on_event_message(["EVENT", "subid", event])
|
||||||
|
finally:
|
||||||
|
await conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_nwc_marks_pending_invoice_settled_only_once():
|
||||||
|
wallet = NWCWallet.__new__(NWCWallet)
|
||||||
|
wallet.pending_invoice_details = {"checking-id": {"checking_id": "checking-id"}}
|
||||||
|
wallet.pending_invoices = ["checking-id"]
|
||||||
|
wallet.paid_invoices_queue = asyncio.Queue(0)
|
||||||
|
|
||||||
|
wallet._mark_invoice_settled("checking-id", source="notification")
|
||||||
|
wallet._mark_invoice_settled("checking-id", source="notification")
|
||||||
|
|
||||||
|
assert wallet.paid_invoices_queue.qsize() == 1
|
||||||
|
assert await wallet.paid_invoices_queue.get() == "checking-id"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_nwc_registers_notification_subscriptions(mocker):
|
||||||
|
async def _noop(*args, **kwargs):
|
||||||
|
return None
|
||||||
|
|
||||||
|
mocker.patch("lnbits.wallets.nwc.NWCConnection._connect_to_relay", new=_noop)
|
||||||
|
mocker.patch("lnbits.wallets.nwc.NWCConnection._handle_timeouts", new=_noop)
|
||||||
|
|
||||||
|
service_private_key = PrivateKey()
|
||||||
|
service_public_key = service_private_key.public_key.format().hex()[2:]
|
||||||
|
account_private_key = PrivateKey()
|
||||||
|
|
||||||
|
conn = NWCConnection(
|
||||||
|
service_public_key,
|
||||||
|
account_private_key.secret.hex(),
|
||||||
|
"ws://127.0.0.1:8555",
|
||||||
|
)
|
||||||
|
send_mock = mocker.patch.object(conn, "_send", mocker.AsyncMock())
|
||||||
|
|
||||||
|
try:
|
||||||
|
await conn._subscribe_to_notifications()
|
||||||
|
|
||||||
|
assert len(conn.notification_subscription_ids) == 2
|
||||||
|
assert len(conn.subscriptions) == 2
|
||||||
|
assert set(conn.subscriptions.keys()) == conn.notification_subscription_ids
|
||||||
|
assert all(
|
||||||
|
subscription["method"] == "notification_sub"
|
||||||
|
and subscription["event_id"] == subscription["sub_id"]
|
||||||
|
for subscription in conn.subscriptions.values()
|
||||||
|
)
|
||||||
|
assert send_mock.await_count == 2
|
||||||
|
finally:
|
||||||
|
await conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_nwc_spreads_fallback_lookups_with_cooldown(mocker):
|
||||||
|
def _schedule_next_lookup(
|
||||||
|
invoice: dict[str, object], now: float | None = None
|
||||||
|
) -> None:
|
||||||
|
assert now is not None
|
||||||
|
invoice["next_lookup_at"] = now + 1
|
||||||
|
|
||||||
|
wallet = NWCWallet.__new__(NWCWallet)
|
||||||
|
wallet.shutdown = False
|
||||||
|
wallet.pending_invoices = ["checking-1", "checking-2"]
|
||||||
|
wallet.pending_invoice_details = {
|
||||||
|
"checking-1": {
|
||||||
|
"checking_id": "checking-1",
|
||||||
|
"next_lookup_at": 0.0,
|
||||||
|
"lookup_attempts": 0,
|
||||||
|
},
|
||||||
|
"checking-2": {
|
||||||
|
"checking_id": "checking-2",
|
||||||
|
"next_lookup_at": 0.0,
|
||||||
|
"lookup_attempts": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
wallet.pending_invoices_lookup_cooldown = 1.0
|
||||||
|
wallet._is_shutting_down = lambda: False
|
||||||
|
wallet._payment_data_is_settled = lambda payment_data: False
|
||||||
|
wallet._cache_payment_data = lambda *args, **kwargs: None
|
||||||
|
wallet._schedule_next_lookup = _schedule_next_lookup
|
||||||
|
wallet.conn = mocker.Mock()
|
||||||
|
wallet.conn.get_info = mocker.AsyncMock()
|
||||||
|
wallet.conn.supports_method = mocker.Mock(return_value=True)
|
||||||
|
wallet.conn.call = mocker.AsyncMock(return_value={"settled_at": None})
|
||||||
|
sleep_mock = mocker.patch("lnbits.wallets.nwc.asyncio.sleep", mocker.AsyncMock())
|
||||||
|
|
||||||
|
await wallet._run_fallback_lookups(100.0)
|
||||||
|
|
||||||
|
assert wallet.conn.call.await_count == 2
|
||||||
|
sleep_mock.assert_awaited_once_with(1.0)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"test_data",
|
"test_data",
|
||||||
|
|||||||
Reference in New Issue
Block a user