fix: use lnbits linting, change to uv (#9)
Some checks failed
/ release (push) Has been cancelled
Tests / test (push) Has been cancelled
/ pullrequest (push) Has been cancelled

* fix: use lnbits linting, change to uv

- formatting
- prettier
- pyright

* dont ignore packagejson

* readd poetry lock for integration tests

* fix pyright

* fix mypy

* fix test?

* fix lnbits version

* fixup tests

* fixup!
This commit is contained in:
dni ⚡ 2025-09-12 09:30:16 +02:00 • committed by GitHub
commit 603b345073
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 5059 additions and 2199 deletions

10
.github/workflows/lint.yml vendored Normal file
View file

@ -0,0 +1,10 @@
name: lint
on:
push:
branches:
- main
pull_request:
jobs:
lint:
uses: lnbits/lnbits/.github/workflows/lint.yml@dev

View file

@ -1,52 +0,0 @@
on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+"
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Create github release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref_name }}
run: |
gh release create "$tag" --generate-notes
pullrequest:
needs: [release]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
token: ${{ secrets.EXT_GITHUB }}
repository: lnbits/lnbits-extensions
path: "./lnbits-extensions"
- name: setup git user
run: |
git config --global user.name "alan"
git config --global user.email "alan@lnbits.com"
- name: Create pull request in extensions repo
env:
GH_TOKEN: ${{ secrets.EXT_GITHUB }}
repo_name: "${{ github.event.repository.name }}"
tag: "${{ github.ref_name }}"
branch: "update-${{ github.event.repository.name }}-${{ github.ref_name }}"
title: "[UPDATE] ${{ github.event.repository.name }} to ${{ github.ref_name }}"
body: "https://github.com/lnbits/${{ github.event.repository.name }}/releases/${{ github.ref_name }}"
archive: "https://github.com/lnbits/${{ github.event.repository.name }}/archive/refs/tags/${{ github.ref_name }}.zip"
run: |
cd lnbits-extensions
git checkout -b $branch
# if there is another open PR
git pull origin $branch || echo "branch does not exist"
sh util.sh update_extension $repo_name $tag
git add -A
git commit -am "$title"
git push origin $branch
# check if pr exists before creating it
gh config set pager cat
check=$(gh pr list -H $branch | wc -l)
test $check -ne 0 || gh pr create --title "$title" --body "$body" --repo lnbits/lnbits-extensions

View file

@ -1,51 +1,57 @@
on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+"
- 'v[0-9]+.[0-9]+.[0-9]+'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Create github release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref_name }}
run: |
gh release create "$tag" --generate-notes
pullrequest:
needs: [release]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
token: ${{ secrets.EXT_GITHUB }}
repository: lnbits/lnbits-extensions
path: "./lnbits-extensions"
path: './lnbits-extensions'
- name: setup git user
run: |
git config --global user.name "alan"
git config --global user.email "alan@lnbits.com"
- name: Create pull request in extensions repo
env:
GH_TOKEN: ${{ secrets.EXT_GITHUB }}
repo_name: "${{ github.event.repository.name }}"
tag: "${{ github.ref_name }}"
branch: "update-${{ github.event.repository.name }}-${{ github.ref_name }}"
title: "[UPDATE] ${{ github.event.repository.name }} to ${{ github.ref_name }}"
body: "https://github.com/lnbits/${{ github.event.repository.name }}/releases/${{ github.ref_name }}"
archive: "https://github.com/lnbits/${{ github.event.repository.name }}/archive/refs/tags/${{ github.ref_name }}.zip"
repo_name: '${{ github.event.repository.name }}'
tag: '${{ github.ref_name }}'
branch: 'update-${{ github.event.repository.name }}-${{ github.ref_name }}'
title: '[UPDATE] ${{ github.event.repository.name }} to ${{ github.ref_name }}'
body: 'https://github.com/lnbits/${{ github.event.repository.name }}/releases/${{ github.ref_name }}'
archive: 'https://github.com/lnbits/${{ github.event.repository.name }}/archive/refs/tags/${{ github.ref_name }}.zip'
run: |
cd lnbits-extensions
git checkout -b $branch
# if there is another open PR
git pull origin $branch || echo "branch does not exist"
sh util.sh update_extension $repo_name $tag
git add -A
git commit -am "$title"
git push origin $branch
# check if pr exists before creating it
gh config set pager cat
check=$(gh pr list -H $branch | wc -l)

View file

@ -10,12 +10,12 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: "3.9"
python-version: '3.10'
- name: Install dependencies
run: |

3
.gitignore vendored
View file

@ -8,6 +8,5 @@ node_modules
.mypy_cache
data
.vscode
package.json
package-lock.json
dump
.venv

12
.prettierrc Normal file
View file

@ -0,0 +1,12 @@
{
"semi": false,
"arrowParens": "avoid",
"insertPragma": false,
"printWidth": 80,
"proseWrap": "preserve",
"singleQuote": true,
"trailingComma": "none",
"useTabs": false,
"bracketSameLine": false,
"bracketSpacing": false
}

View file

@ -5,27 +5,27 @@ format: prettier black ruff
check: mypy pyright checkblack checkruff checkprettier
prettier:
poetry run ./node_modules/.bin/prettier --write .
uv run ./node_modules/.bin/prettier --write .
pyright:
poetry run ./node_modules/.bin/pyright
uv run ./node_modules/.bin/pyright
mypy:
poetry run mypy .
uv run mypy .
black:
poetry run black .
uv run black .
ruff:
poetry run ruff check . --fix
uv run ruff check . --fix
checkruff:
poetry run ruff check .
uv run ruff check .
checkprettier:
poetry run ./node_modules/.bin/prettier --check .
uv run ./node_modules/.bin/prettier --check .
checkblack:
poetry run black --check .
uv run black --check .
checkeditorconfig:
editorconfig-checker
@ -33,14 +33,14 @@ checkeditorconfig:
test:
PYTHONUNBUFFERED=1 \
DEBUG=true \
poetry run pytest
uv run pytest
install-pre-commit-hook:
@echo "Installing pre-commit hook to git"
@echo "Uninstall the hook with poetry run pre-commit uninstall"
poetry run pre-commit install
@echo "Uninstall the hook with uv run pre-commit uninstall"
uv run pre-commit install
pre-commit:
poetry run pre-commit run --all-files
uv run pre-commit run --all-files
checkbundle:

View file

@ -45,7 +45,7 @@ def nwcprovider_start():
__all__ = [
"db",
"nwcprovider_ext",
"nwcprovider_static_files",
"nwcprovider_start",
"nwcprovider_static_files",
"nwcprovider_stop",
]

38
crud.py
View file

@ -1,5 +1,4 @@
import time
from typing import List, Optional
from lnbits.db import Database
@ -11,6 +10,7 @@ from .models import (
GetNWC,
GetWalletNWC,
NWCBudget,
NWCConfig,
NWCKey,
NWCNewBudget,
TrackedSpendNWC,
@ -71,7 +71,8 @@ async def delete_nwc(data: DeleteNWC) -> None:
# hardening #
assert_valid_pubkey(data.pubkey)
assert_valid_wallet_id(data.wallet)
if data.wallet:
assert_valid_wallet_id(data.wallet)
# ## #
await db.execute(
@ -80,9 +81,12 @@ async def delete_nwc(data: DeleteNWC) -> None:
)
async def get_wallet_nwcs(data: GetWalletNWC) -> List[NWCKey]:
async def get_wallet_nwcs(data: GetWalletNWC) -> list[NWCKey]:
expires = int(time.time()) if not data.include_expired else -1
if not data.wallet:
return []
# hardening #
assert_valid_wallet_id(data.wallet)
assert_valid_expiration_seconds(expires)
@ -101,7 +105,7 @@ async def get_wallet_nwcs(data: GetWalletNWC) -> List[NWCKey]:
)
async def get_nwc(data: GetNWC) -> Optional[NWCKey]:
async def get_nwc(data: GetNWC) -> NWCKey | None:
expires = int(time.time()) if not data.include_expired else -1
# hardening #
@ -150,17 +154,17 @@ async def get_nwc(data: GetNWC) -> Optional[NWCKey]:
return row
async def get_budgets_nwc(data: GetBudgetsNWC) -> Optional[NWCBudget]:
async def get_budgets_nwc(data: GetBudgetsNWC) -> list[NWCBudget]:
# hardening #
assert_valid_pubkey(data.pubkey)
# ## #
rows = await db.fetchall(
budgets = await db.fetchall(
"SELECT * FROM nwcprovider.budgets WHERE pubkey = :pubkey",
{"pubkey": data.pubkey},
model=NWCBudget,
)
budgets = [NWCBudget(**row) for row in rows]
if data.calculate_spent:
for budget in budgets:
last_cycle, next_cycle = budget.get_timestamp_range()
@ -170,7 +174,7 @@ async def get_budgets_nwc(data: GetBudgetsNWC) -> Optional[NWCBudget]:
assert_valid_timestamp_seconds(next_cycle)
# ## #
tot_spent_in_range_msats = await db.fetchone(
result: dict = await db.fetchone(
"""
SELECT SUM(amount_msats) FROM nwcprovider.spent
WHERE pubkey = :pubkey AND created_at >=
@ -182,9 +186,7 @@ async def get_budgets_nwc(data: GetBudgetsNWC) -> Optional[NWCBudget]:
"next_cycle": next_cycle,
},
)
tot_spent_in_range_msats = (
next(iter(tot_spent_in_range_msats.values())) or 0
)
tot_spent_in_range_msats = next(iter(result.values())) or 0
# hardening #
assert_valid_msats(tot_spent_in_range_msats)
@ -263,12 +265,14 @@ async def tracked_spend_nwc(data: TrackedSpendNWC, action):
async def get_config_nwc(key: str):
row = await db.fetchone(
"SELECT * FROM nwcprovider.config WHERE key = :key", {"key": key}
config = await db.fetchone(
"SELECT * FROM nwcprovider.config WHERE key = :key",
{"key": key},
model=NWCConfig,
)
if not row:
if not config:
return None
return row["value"]
return config.value
async def set_config_nwc(key: str, value: str):
@ -289,5 +293,5 @@ async def set_config_nwc(key: str, value: str):
async def get_all_config_nwc():
rows = await db.fetchall("SELECT * FROM nwcprovider.config")
return {row["key"]: row["value"] for row in rows}
rows = await db.fetchall("SELECT * FROM nwcprovider.config", model=NWCConfig)
return {row.key: row.value for row in rows}

View file

@ -1,7 +1,7 @@
import asyncio
from typing import Any, Dict
from typing import Any
execution_queue: asyncio.Queue[Dict[str, Any]] = asyncio.Queue()
execution_queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue()
async def enqueue(action):

View file

@ -75,7 +75,8 @@ async def m003_default_config(db):
new_private_key = bytes.hex(secp256k1._gen_private_key())
await db.execute(
"""
INSERT INTO nwcprovider.config (key, value) VALUES ('provider_key', :provider_key)
INSERT INTO nwcprovider.config (key, value)
VALUES ('provider_key', :provider_key)
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value;
""",
{"provider_key": new_private_key},
@ -87,12 +88,13 @@ async def m004_default_config2(db):
Default config
"""
await db.execute(
"""
"""
INSERT INTO nwcprovider.config (key, value) VALUES ('relay_alias', :value)
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value;
""",
{"value": ""},
)
{"value": ""},
)
async def m005_key_last_used(db):
"""
@ -111,7 +113,8 @@ async def m006_default_config3(db):
"""
await db.execute(
"""
INSERT INTO nwcprovider.config (key, value) VALUES ('handle_missed_events', :value)
INSERT INTO nwcprovider.config (key, value)
VALUES ('handle_missed_events', :value)
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value;
""",
{"value": "0"},

View file

@ -2,7 +2,7 @@
import time
from sqlite3 import Row
from typing import Any, Dict, List, Optional
from typing import Any
from pydantic import BaseModel
@ -16,14 +16,14 @@ class NWCKey(BaseModel):
created_at: int
last_used: int
def get_permissions(self) -> List[str]:
def get_permissions(self) -> list[str]:
try:
return self.permissions.split(" ")
except Exception:
return []
@classmethod
def from_row(cls, row: Dict[str, Any]) -> "NWCKey":
def from_row(cls, row: dict[str, Any]) -> "NWCKey":
return cls(**row)
@ -53,7 +53,7 @@ class NWCBudget(BaseModel):
class NWCNewBudget(BaseModel):
pubkey: Optional[str]
pubkey: str | None
budget_msats: int
refresh_window: int
created_at: int
@ -65,30 +65,30 @@ class CreateNWCKey(BaseModel):
wallet: str
description: str
expires_at: int
permissions: List[str]
budgets: Optional[List[NWCNewBudget]] = None
permissions: list[str]
budgets: list[NWCNewBudget] | None = None
class DeleteNWC(BaseModel):
pubkey: str
wallet: Optional[str] = None
wallet: str | None = None
class GetWalletNWC(BaseModel):
wallet: Optional[str] = None
include_expired: Optional[bool] = False
wallet: str | None = None
include_expired: bool | None = False
class GetNWC(BaseModel):
pubkey: str
wallet: Optional[str] = None
include_expired: Optional[bool] = False
refresh_last_used: Optional[bool] = False
wallet: str | None = None
include_expired: bool | None = False
refresh_last_used: bool | None = False
class GetBudgetsNWC(BaseModel):
pubkey: str
calculate_spent: Optional[bool] = False
calculate_spent: bool | None = False
class TrackedSpendNWC(BaseModel):
@ -98,12 +98,17 @@ class TrackedSpendNWC(BaseModel):
# API models
class NWCRegistrationRequest(BaseModel):
permissions: List[str]
permissions: list[str]
description: str
expires_at: int
budgets: List[NWCNewBudget]
budgets: list[NWCNewBudget]
class NWCGetResponse(BaseModel):
data: NWCKey
budgets: List[NWCBudget]
budgets: list[NWCBudget]
class NWCConfig(BaseModel):
key: str
value: str

61
nwcp.py
View file

@ -4,16 +4,17 @@ import hashlib
import json
import random
import time
from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple, Union
from collections.abc import Awaitable, Callable
from typing import Any, Union
import secp256k1
import websockets.client as websockets
from Cryptodome import Random
from Cryptodome.Cipher import AES
from Cryptodome.Util.Padding import pad, unpad
from lnbits.helpers import encrypt_internal_message
from lnbits.settings import settings
from loguru import logger
from websockets.legacy.client import connect
class RateLimit:
@ -23,14 +24,14 @@ class RateLimit:
class MainSubscription:
def __init__(self):
self.requests_sub_id: Optional[str] = None
self.responses_sub_id: Optional[str] = None
self.requests_sub_id: str | None = None
self.responses_sub_id: str | None = None
self.requests_eose = False
self.responses_eose = False
self.events: Dict[str, Dict] = {}
self.responses: List[str] = []
self.events: dict[str, dict] = {}
self.responses: list[str] = []
def get_stale(self) -> List[Dict]:
def get_stale(self) -> list[dict]:
"""
Get all the pending events that do not have a response yet.
"""
@ -47,7 +48,7 @@ class MainSubscription:
if event_id not in self.responses:
self.responses.append(event_id)
def gc(self, expire: Optional[int] = None):
def gc(self, expire: int | None = None):
"""
Garbage collection, remove all the events that have a response older
than expire seconds (defaults to 1 hour if 0 or None)
@ -74,8 +75,8 @@ class MainSubscription:
class NWCServiceProvider:
def __init__(
self,
private_key: Optional[str] = None,
relay: Optional[str] = None,
private_key: str | None = None,
relay: str | None = None,
handle_missed_events: int = 0,
):
if not relay: # Connect to nostrclient
@ -100,17 +101,17 @@ class NWCServiceProvider:
self.public_key_hex = self.public_key.serialize().hex()[2:]
# List of supported methods
self.supported_methods: List[str] = []
self.supported_methods: list[str] = []
# Keep track of the number of subscriptions (used for unique subid)
self.subscriptions_count: int = 0
# Request listeners, listen to specific methods
self.request_listeners: Dict[
self.request_listeners: dict[
str,
Callable[
[NWCServiceProvider, str, Dict],
Awaitable[List[Tuple[Optional[Dict], Optional[Dict], List]]],
[NWCServiceProvider, str, dict],
Awaitable[list[tuple[dict | None, dict | None, list]]],
],
] = {}
@ -122,7 +123,7 @@ class NWCServiceProvider:
# Subscription
self.sub = None
self.rate_limit: Dict[str, RateLimit] = {}
self.rate_limit: dict[str, RateLimit] = {}
# websocket connection
self.ws = None
@ -161,8 +162,8 @@ class NWCServiceProvider:
self,
method: str,
listener: Callable[
["NWCServiceProvider", str, Dict],
Awaitable[List[Tuple[Optional[Dict], Optional[Dict], List]]],
["NWCServiceProvider", str, dict],
Awaitable[list[tuple[dict | None, dict | None, list]]],
],
):
"""
@ -185,7 +186,7 @@ class NWCServiceProvider:
self.reconnect_task = asyncio.create_task(self._connect_to_relay())
self.gc_task = asyncio.create_task(self._gc_loop())
def _json_dumps(self, data: Union[Dict, list]) -> str:
def _json_dumps(self, data: Union[dict, list]) -> str:
"""
Converts a Python dictionary to a JSON string with compact encoding.
@ -195,7 +196,7 @@ class NWCServiceProvider:
Returns:
str: The compact JSON string.
"""
if isinstance(data, Dict):
if isinstance(data, dict):
data = {k: v for k, v in data.items() if v is not None}
return json.dumps(data, separators=(",", ":"), ensure_ascii=False)
@ -205,7 +206,7 @@ class NWCServiceProvider:
"""
return self.shutdown or not settings.lnbits_running
async def _send(self, data: List[Any]):
async def _send(self, data: list[Any]):
"""
Sends data to the relay.
@ -249,7 +250,7 @@ class NWCServiceProvider:
await asyncio.sleep(1)
async def _ratelimit(self, unit: str, max_sleep_time: int = 120) -> None:
limit: Optional[RateLimit] = self.rate_limit.get(unit)
limit: RateLimit | None = self.rate_limit.get(unit)
if not limit:
self.rate_limit[unit] = limit = RateLimit()
@ -289,7 +290,7 @@ class NWCServiceProvider:
await self._send(["REQ", self.sub.requests_sub_id, req_filter])
await self._send(["REQ", self.sub.responses_sub_id, res_filter])
async def _on_connection(self, ws):
async def _on_connection(self, _):
"""
On connection callback, announce the service provider
methods and subscribe to nip67 events.
@ -306,7 +307,7 @@ class NWCServiceProvider:
# Resubscribe to nwc events
await self._subscribe()
async def _handle_request(self, event: Dict) -> List[Dict]:
async def _handle_request(self, event: dict) -> list[dict]:
"""
Handle a nwc request
"""
@ -319,7 +320,7 @@ class NWCServiceProvider:
# Handle request
method = content["method"]
listener = self.request_listeners.get(method, None)
outs: List[Dict[str, Any]] = []
outs: list[dict[str, Any]] = []
if not listener:
outs.append(
{
@ -352,7 +353,7 @@ class NWCServiceProvider:
if "error" in out:
content["error"] = out["error"]
# Prepare response event
res: Dict = {
res: dict = {
"kind": 23195,
"created_at": int(time.time()),
"tags": out.get("tags", []),
@ -451,7 +452,7 @@ class NWCServiceProvider:
await self._ratelimit("subscribing")
await self._subscribe()
async def _on_message(self, ws, message: str):
async def _on_message(self, _, message: str):
"""
Handle incoming messages from the relay.
"""
@ -484,7 +485,7 @@ class NWCServiceProvider:
): # Reconnect until the instance is shutting down
logger.debug("Creating new connection...")
try:
async with websockets.connect(self.relay) as ws:
async with connect(self.relay) as ws:
self.ws = ws
self.connected = True
await self._on_connection(ws)
@ -513,7 +514,7 @@ class NWCServiceProvider:
await self._ratelimit("connecting")
def _encrypt_content(
self, content: str, pubkey_hex: str, iv_seed: Optional[int] = None
self, content: str, pubkey_hex: str, iv_seed: int | None = None
) -> str:
"""
Encrypts the content for the given public key
@ -571,7 +572,7 @@ class NWCServiceProvider:
decrypted = decrypted_bytes.decode("utf-8")
return decrypted
def _verify_event(self, event: Dict) -> bool:
def _verify_event(self, event: dict) -> bool:
"""
Verify the event signature
@ -602,7 +603,7 @@ class NWCServiceProvider:
return False
return True
def _sign_event(self, event: Dict) -> Dict:
def _sign_event(self, event: dict) -> dict:
"""
Signs the event (in place)

62
package-lock.json generated Normal file
View file

@ -0,0 +1,62 @@
{
"name": "nwcprovider",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "nwcprovider",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"prettier": "^3.2.5",
"pyright": "^1.1.358"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/prettier": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/pyright": {
"version": "1.1.405",
"resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.405.tgz",
"integrity": "sha512-hgy12kLZ1oAMtl9LTsByHftg3AD6Pouwu5rBsQlqYQqCCdGBgaQm9XDAPDap7ayWe9W+NWrUwO7Zy1K7uXoE2A==",
"license": "MIT",
"bin": {
"pyright": "index.js",
"pyright-langserver": "langserver.index.js"
},
"engines": {
"node": ">=14.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
}
}
}
}

15
package.json Normal file
View file

@ -0,0 +1,15 @@
{
"name": "nwcprovider",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"prettier": "^3.2.5",
"pyright": "^1.1.358"
}
}

3385
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,37 +1,41 @@
[tool.poetry]
[project]
name = "nwcprovider"
version = "0.0.0"
description = "A NWC service provider for LNbits."
authors = ["Riccardo Balbo <oc@rblb.it>"]
requires-python = ">=3.10,<3.13"
authors = [{ name = "Riccardo Balbo", email = "oc@rblb.it" }]
urls = { Homepage = "https://lnbits.com", Repository = "https://github.com/lnbits/nwcprovider" }
dependencies = [ "lnbits>1" ]
[tool.poetry.dependencies]
python = "^3.10 | ^3.9"
lnbits = {version = "*", allow-prereleases = true}
mypy = "^1.13.0"
[tool.poetry]
package-mode = false
[tool.poetry.group.dev.dependencies]
black = "^24.3.0"
pytest-asyncio = "^0.21.0"
pytest = "^7.3.2"
mypy = "^1.5.1"
pre-commit = "^3.2.2"
ruff = "^0.3.2"
pytest-md = "^0.2.0"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.uv]
dev-dependencies = [
"black",
"pytest-asyncio",
"pytest",
"mypy",
"pre-commit",
"ruff",
"pytest-md",
]
[tool.mypy]
plugins = ["pydantic.mypy"]
[[tool.mypy.overrides]]
module = [
"lnbits.*",
"loguru.*",
"fastapi.*",
"pydantic.*",
"secp256k1.*",
]
ignore_missing_imports = "True"
[tool.pydantic-mypy]
init_forbid_extra = true
init_typed = true
warn_required_dynamic_aliases = true
warn_untyped_fields = true
[tool.pytest.ini_options]
log_cli = false
testpaths = [

View file

@ -1,87 +1,87 @@
window.app = Vue.createApp({
el: "#vue",
el: '#vue',
mixins: [windowMixin],
delimiters: ["${", "}"],
delimiters: ['${', '}'],
data: function () {
return {
entries: [],
columns: [
{
name: "key",
name: 'key',
required: true,
label: "Key",
align: "left",
field: (row) => row.key,
sortable: true,
label: 'Key',
align: 'left',
field: row => row.key,
sortable: true
},
{
name: "value",
name: 'value',
required: true,
label: "Value",
align: "left",
field: (row) => row.value,
sortable: true,
},
],
};
label: 'Value',
align: 'left',
field: row => row.value,
sortable: true
}
]
}
},
methods: {
fetchConfig() {
this.entries = [];
this.entries = []
LNbits.api
.request(
"GET",
"/nwcprovider/api/v1/config",
this.g.user.wallets[0].adminkey,
'GET',
'/nwcprovider/api/v1/config',
this.g.user.wallets[0].adminkey
)
.then((response) => {
const newEntries = [];
.then(response => {
const newEntries = []
for (const [key, value] of Object.entries(response.data)) {
newEntries.push({
key: key,
value: value,
});
value: value
})
}
this.entries.length = 0;
this.entries.push(...newEntries);
this.entries.length = 0
this.entries.push(...newEntries)
})
.catch(function (error) {
console.error("Error fetching config:", error);
});
console.error('Error fetching config:', error)
})
},
async saveConfig() {
const data = {};
const data = {}
for (const entry of this.entries) {
data[entry.key] = entry.value;
data[entry.key] = entry.value
}
try {
const response = await LNbits.api.request(
"POST",
"/nwcprovider/api/v1/config",
'POST',
'/nwcprovider/api/v1/config',
this.g.user.wallets[0].adminkey,
data,
);
data
)
Quasar.Notify.create({
type: "positive",
message: "Config saved!",
});
type: 'positive',
message: 'Config saved!'
})
Quasar.Notify.create({
type: "warning",
type: 'warning',
message:
"You need to restart the server for the changes to take effect!",
});
'You need to restart the server for the changes to take effect!'
})
} catch (error) {
Quasar.Notify.create({
type: "negative",
message: "Error saving config: " + String(error),
});
console.error("Error saving config:", error);
type: 'negative',
message: 'Error saving config: ' + String(error)
})
console.error('Error saving config:', error)
}
},
}
},
created: function () {
this.fetchConfig();
},
});
this.fetchConfig()
}
})

View file

@ -1,7 +1,7 @@
window.app = Vue.createApp({
el: "#vue",
el: '#vue',
mixins: [windowMixin],
delimiters: ["${", "}"],
delimiters: ['${', '}'],
data: function () {
return {
selectedWallet: null,
@ -10,244 +10,244 @@ window.app = Vue.createApp({
nwcsTable: {
columns: [
{
name: "description",
align: "left",
label: "Description",
field: "description",
name: 'description',
align: 'left',
label: 'Description',
field: 'description'
},
{ name: "status", align: "left", label: "Status", field: "status" },
{name: 'status', align: 'left', label: 'Status', field: 'status'},
{
name: "last_used",
align: "left",
label: "Last used",
field: "last_used",
name: 'last_used',
align: 'left',
label: 'Last used',
field: 'last_used'
},
{
name: "created_at",
align: "left",
label: "Created",
field: "created_at",
name: 'created_at',
align: 'left',
label: 'Created',
field: 'created_at'
},
{
name: "expires_at",
align: "left",
label: "Expires",
field: "expires_at",
},
name: 'expires_at',
align: 'left',
label: 'Expires',
field: 'expires_at'
}
],
pagination: {
rowsPerPage: 10,
},
rowsPerPage: 10
}
},
connectDialog: {
show: false,
data: {},
data: {}
},
pairingDialog: {
show: false,
data: {
pairingUrl: "",
},
pairingUrl: ''
}
},
pairingQrDialog: {
show: false,
data: {
pairingUrl: "",
},
pairingUrl: ''
}
},
connectionInfoDialog: {
show: false,
data: {},
},
};
data: {}
}
}
},
methods: {
showConnectDialog() {
const wallet = this.getWallet();
const wallet = this.getWallet()
if (!wallet) {
Quasar.Notify.create({
type: "negative",
message: "Please select a wallet first",
});
return;
type: 'negative',
message: 'Please select a wallet first'
})
return
} else {
this.connectDialog.show = true;
this.connectDialog.show = true
}
},
openConnectionInfoDialog(data) {
this.connectionInfoDialog.data = data;
this.connectionInfoDialog.show = true;
this.connectionInfoDialog.data = data
this.connectionInfoDialog.show = true
},
closeConnectionInfoDialog() {
this.connectionInfoDialog.show = false;
this.connectionInfoDialog.show = false
},
openPairingUrl() {
const url = this.pairingDialog.data.pairingUrl;
if (url) window.open(url, "_blank");
const url = this.pairingDialog.data.pairingUrl
if (url) window.open(url, '_blank')
},
go(url) {
window.open(url, "_blank");
window.open(url, '_blank')
},
async copyPairingUrl() {
const url = this.pairingDialog.data.pairingUrl;
const url = this.pairingDialog.data.pairingUrl
if (url) {
try {
await navigator.clipboard.writeText(url);
await navigator.clipboard.writeText(url)
Quasar.Notify.create({
type: "positive",
message: "URL copied to clipboard",
});
type: 'positive',
message: 'URL copied to clipboard'
})
} catch (err) {
Quasar.Notify.create({
type: "negative",
message: "Failed to copy URL.",
});
type: 'negative',
message: 'Failed to copy URL.'
})
}
}
},
showPairingQR() {
this.pairingQrDialog.data.pairingUrl = this.pairingDialog.data.pairingUrl;
this.pairingQrDialog.show = true;
this.pairingQrDialog.data.pairingUrl = this.pairingDialog.data.pairingUrl
this.pairingQrDialog.show = true
},
closePairingQrDialog() {
this.pairingQrDialog.show = false;
this.pairingQrDialog.show = false
},
loadConnectDialogData() {
this.connectDialog.data = {
description: "",
description: '',
expires_at: Date.now() + 1000 * 60 * 60 * 24 * 7,
neverExpires: true,
permissions: [],
budgets: [],
};
budgets: []
}
for (const permission of this.nodePermissions) {
this.connectDialog.data.permissions.push({
key: permission.key,
name: permission.name,
value: permission.value,
});
value: permission.value
})
}
},
deleteBudget(index) {
this.connectDialog.data.budgets.splice(index, 1);
this.connectDialog.data.budgets.splice(index, 1)
},
addBudget() {
this.connectDialog.data.budgets.push({
budget_sats: 1000,
used_budget_sats: 0,
created_at: new Date(new Date().setHours(0, 0, 0, 0)).getTime() / 1000,
expiration: "never",
});
expiration: 'never'
})
},
closeConnectDialog() {
this.connectDialog.show = false;
this.loadConnectDialogData();
this.connectDialog.show = false
this.loadConnectDialogData()
},
getWallet: function () {
let wallet = undefined;
let wallet = undefined
for (let i = 0; i < this.g.user.wallets.length; i++) {
if (this.g.user.wallets[i].id == this.selectedWallet) {
wallet = this.g.user.wallets[i];
break;
wallet = this.g.user.wallets[i]
break
}
}
return wallet;
return wallet
},
async generateKeyPair() {
while (!window.NobleSecp256k1) {
await new Promise((resolve) => setTimeout(resolve, 1));
await new Promise(resolve => setTimeout(resolve, 1))
}
const privKeyBytes = window.NobleSecp256k1.utils.randomPrivateKey();
const pubKeyBytes = window.NobleSecp256k1.getPublicKey(privKeyBytes);
const privKeyBytes = window.NobleSecp256k1.utils.randomPrivateKey()
const pubKeyBytes = window.NobleSecp256k1.getPublicKey(privKeyBytes)
const out = {
privKeyBytes: privKeyBytes,
pubKeyBytes: pubKeyBytes,
privKey: window.NobleSecp256k1.etc.bytesToHex(privKeyBytes),
pubKey: window.NobleSecp256k1.etc.bytesToHex(pubKeyBytes.slice(1)),
};
return out;
pubKey: window.NobleSecp256k1.etc.bytesToHex(pubKeyBytes.slice(1))
}
return out
},
deleteNWC: async function (pubkey) {
Quasar.Dialog.create({
title: "Confirm Deletion",
message: "Are you sure you want to delete this connection?",
title: 'Confirm Deletion',
message: 'Are you sure you want to delete this connection?',
cancel: true,
persistent: true,
persistent: true
})
.onOk(async () => {
try {
const wallet = this.getWallet();
const wallet = this.getWallet()
const response = await LNbits.api.request(
"DELETE",
'DELETE',
`/nwcprovider/api/v1/nwc/${pubkey}`,
wallet.adminkey,
);
this.loadNwcs();
wallet.adminkey
)
this.loadNwcs()
Quasar.Notify.create({
type: "positive",
message: "Deleted successfully",
});
type: 'positive',
message: 'Deleted successfully'
})
} catch (error) {
LNbits.utils.notifyApiError(error);
LNbits.utils.notifyApiError(error)
}
})
.onCancel(() => {
// User canceled the operation
});
})
},
loadNwcs: async function () {
const wallet = this.getWallet();
const wallet = this.getWallet()
if (!wallet) {
this.nwcs = [];
return;
this.nwcs = []
return
}
try {
const response = await LNbits.api.request(
"GET",
"/nwcprovider/api/v1/nwc?include_expired=true&calculate_spent_budget=true",
wallet.adminkey,
);
this.nwcs = response.data;
'GET',
'/nwcprovider/api/v1/nwc?include_expired=true&calculate_spent_budget=true',
wallet.adminkey
)
this.nwcs = response.data
} catch (error) {
this.nwcs = [];
this.nwcs = []
}
try {
const response = await LNbits.api.request(
"GET",
"/nwcprovider/api/v1/permissions",
wallet.adminkey,
);
const permissions = [];
'GET',
'/nwcprovider/api/v1/permissions',
wallet.adminkey
)
const permissions = []
for (const [key, value] of Object.entries(response.data)) {
permissions.push({
key: key,
name: value.name,
value: value.default,
});
value: value.default
})
}
this.nodePermissions = permissions;
this.nodePermissions = permissions
} catch (error) {
Lnbits.utils.notifyApiError(error);
Lnbits.utils.notifyApiError(error)
}
this.loadConnectDialogData();
const newTableEntries = [];
this.loadConnectDialogData()
const newTableEntries = []
for (const nwc of this.nwcs) {
const t = Quasar.date.formatDate(
new Date(nwc.data.created_at * 1000),
"YYYY-MM-DD HH:mm",
);
'YYYY-MM-DD HH:mm'
)
const e =
nwc.data.expires_at > 0
? Quasar.date.formatDate(
new Date(nwc.data.expires_at * 1000),
"YYYY-MM-DD HH:mm",
'YYYY-MM-DD HH:mm'
)
: "Never";
: 'Never'
const l = Quasar.date.formatDate(
new Date(nwc.data.last_used * 1000),
"YYYY-MM-DD HH:mm",
);
'YYYY-MM-DD HH:mm'
)
const nwcTableEntry = {
description: nwc.data.description,
created_at: t,
@ -256,131 +256,130 @@ window.app = Vue.createApp({
pubkey: nwc.data.pubkey,
permissions: nwc.data.permissions,
budgets: [],
status: "Active",
};
status: 'Active'
}
if (
nwc.data.expires_at > 0 &&
nwc.data.expires_at < new Date().getTime() / 1000
) {
nwcTableEntry.status = "Expired";
nwcTableEntry.status = 'Expired'
}
for (const budget of nwc.budgets) {
const createdAt = Quasar.date.formatDate(
new Date(budget.created_at * 1000),
"YYYY-MM-DD HH:mm",
);
let refreshWindow = budget.refresh_window;
'YYYY-MM-DD HH:mm'
)
let refreshWindow = budget.refresh_window
if (refreshWindow <= 0) {
refreshWindow = "Never";
refreshWindow = 'Never'
} else if (refreshWindow == 60 * 60 * 24) {
refreshWindow = "Daily";
refreshWindow = 'Daily'
} else if (refreshWindow == 60 * 60 * 24 * 7) {
refreshWindow = "Weekly";
refreshWindow = 'Weekly'
} else if (refreshWindow == 60 * 60 * 24 * 30) {
refreshWindow = "Monthly";
refreshWindow = 'Monthly'
} else if (refreshWindow == 60 * 60 * 24 * 365) {
refreshWindow = "Yearly";
refreshWindow = 'Yearly'
}
nwcTableEntry.budgets.push({
budget_sats: budget.budget_msats / 1000,
used_budget_sats: budget.used_budget_msats / 1000,
created_at: createdAt,
refresh_window: refreshWindow,
});
refresh_window: refreshWindow
})
}
newTableEntries.push(nwcTableEntry);
newTableEntries.push(nwcTableEntry)
}
this.nwcEntries = newTableEntries;
this.nwcEntries = newTableEntries
},
closePairingDialog() {
this.pairingDialog.show = false;
this.pairingDialog.show = false
},
async showPairingDialog(secret) {
let response = await LNbits.api.request(
"GET",
"/nwcprovider/api/v1/pairing/{SECRET}",
);
response = response.data;
response = response.replace("{SECRET}", secret);
this.pairingDialog.data.pairingUrl = response;
this.pairingDialog.show = true;
'GET',
'/nwcprovider/api/v1/pairing/{SECRET}'
)
response = response.data
response = response.replace('{SECRET}', secret)
this.pairingDialog.data.pairingUrl = response
this.pairingDialog.show = true
},
async confirmConnectDialog() {
const keyPair = await this.generateKeyPair();
const keyPair = await this.generateKeyPair()
// timestamp
let expires_at = 0;
let expires_at = 0
if (!this.connectDialog.data.neverExpires) {
expires_at =
new Date(this.connectDialog.data.expires_at).getTime() / 1000;
new Date(this.connectDialog.data.expires_at).getTime() / 1000
}
const data = {
permissions: [],
description: this.connectDialog.data.description,
expires_at: expires_at,
budgets: [],
};
budgets: []
}
for (const permission of this.connectDialog.data.permissions) {
if (permission.value) data.permissions.push(permission.key);
if (permission.value) data.permissions.push(permission.key)
}
for (const budget of this.connectDialog.data.budgets) {
const budget_msats = budget.budget_sats * 1000;
let refresh_window = 0;
const budget_msats = budget.budget_sats * 1000
let refresh_window = 0
switch (budget.expiry) {
case "Daily":
refresh_window = 60 * 60 * 24;
break;
case "Weekly":
refresh_window = 60 * 60 * 24 * 7;
break;
case "Monthly":
refresh_window = 60 * 60 * 24 * 30;
break;
case "Yearly":
refresh_window = 60 * 60 * 24 * 365;
break;
case "Never":
refresh_window = 0;
break;
case 'Daily':
refresh_window = 60 * 60 * 24
break
case 'Weekly':
refresh_window = 60 * 60 * 24 * 7
break
case 'Monthly':
refresh_window = 60 * 60 * 24 * 30
break
case 'Yearly':
refresh_window = 60 * 60 * 24 * 365
break
case 'Never':
refresh_window = 0
break
}
data.budgets.push({
budget_msats: budget_msats,
refresh_window: refresh_window,
created_at:
new Date(new Date().setHours(0, 0, 0, 0)).getTime() / 1000,
});
created_at: new Date(new Date().setHours(0, 0, 0, 0)).getTime() / 1000
})
}
const wallet = this.getWallet();
const wallet = this.getWallet()
try {
const response = await LNbits.api.request(
"PUT",
"/nwcprovider/api/v1/nwc/" + keyPair.pubKey,
'PUT',
'/nwcprovider/api/v1/nwc/' + keyPair.pubKey,
wallet.adminkey,
data,
);
this.closeConnectDialog();
data
)
this.closeConnectDialog()
if (
!response.data ||
!response.data.data ||
!response.data.data.pubkey
) {
LNbits.utils.notifyApiError("Error creating nwc pairing");
return;
LNbits.utils.notifyApiError('Error creating nwc pairing')
return
}
this.showPairingDialog(keyPair.privKey);
this.showPairingDialog(keyPair.privKey)
} catch (error) {
LNbits.utils.notifyApiError(error);
LNbits.utils.notifyApiError(error)
}
this.loadNwcs();
},
this.loadNwcs()
}
},
created: function () {
this.loadNwcs();
this.loadNwcs()
},
watch: {
selectedWallet(newValue, oldValue) {
this.loadNwcs();
},
},
});
this.loadNwcs()
}
}
})

View file

@ -4,93 +4,93 @@ const B256 = 2n ** 256n,
N = B256 - 0x14551231950b75fc4402da1732fc9bebfn,
Gx = 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798n,
Gy = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8n,
CURVE = { p: P, n: N, a: 0n, b: 7n, Gx, Gy },
CURVE = {p: P, n: N, a: 0n, b: 7n, Gx, Gy},
fLen = 32,
crv = (t) => mod(mod(t * t) * t + CURVE.b),
err = (t = "") => {
throw new Error(t);
crv = t => mod(mod(t * t) * t + CURVE.b),
err = (t = '') => {
throw new Error(t)
},
big = (t) => "bigint" == typeof t,
str = (t) => "string" == typeof t,
fe = (t) => big(t) && 0n < t && t < P,
ge = (t) => big(t) && 0n < t && t < N,
isu8 = (t) =>
big = t => 'bigint' == typeof t,
str = t => 'string' == typeof t,
fe = t => big(t) && 0n < t && t < P,
ge = t => big(t) && 0n < t && t < N,
isu8 = t =>
t instanceof Uint8Array ||
(null != t && "object" == typeof t && "Uint8Array" === t.constructor.name),
(null != t && 'object' == typeof t && 'Uint8Array' === t.constructor.name),
au8 = (t, n) =>
!isu8(t) || ("number" == typeof n && n > 0 && t.length !== n)
? err("Uint8Array expected")
!isu8(t) || ('number' == typeof n && n > 0 && t.length !== n)
? err('Uint8Array expected')
: t,
u8n = (t) => new Uint8Array(t),
u8n = t => new Uint8Array(t),
toU8 = (t, n) => au8(str(t) ? h2b(t) : u8n(au8(t)), n),
mod = (t, n = P) => {
let e = t % n;
return e >= 0n ? e : n + e;
let e = t % n
return e >= 0n ? e : n + e
},
isPoint = (t) => (t instanceof Point ? t : err("Point expected"));
isPoint = t => (t instanceof Point ? t : err('Point expected'))
class Point {
constructor(t, n, e) {
(this.px = t), (this.py = n), (this.pz = e);
;((this.px = t), (this.py = n), (this.pz = e))
}
static fromAffine(t) {
return 0n === t.x && 0n === t.y ? Point.ZERO : new Point(t.x, t.y, 1n);
return 0n === t.x && 0n === t.y ? Point.ZERO : new Point(t.x, t.y, 1n)
}
static fromHex(t) {
let n;
let n
const e = (t = toU8(t))[0],
r = t.subarray(1),
o = slcNum(r, 0, 32),
i = t.length;
i = t.length
if (33 === i && [2, 3].includes(e)) {
fe(o) || err("Point hex invalid: x not FE");
let t = sqrt(crv(o));
!(1 & ~e) !== (1n === (1n & t)) && (t = mod(-t)),
(n = new Point(o, t, 1n));
fe(o) || err('Point hex invalid: x not FE')
let t = sqrt(crv(o))
;(!(1 & ~e) !== (1n === (1n & t)) && (t = mod(-t)),
(n = new Point(o, t, 1n)))
}
return (
65 === i && 4 === e && (n = new Point(o, slcNum(r, 32, 64), 1n)),
n ? n.ok() : err("Point is not on curve")
);
n ? n.ok() : err('Point is not on curve')
)
}
static fromPrivateKey(t) {
return G.mul(toPriv(t));
return G.mul(toPriv(t))
}
get x() {
return this.aff().x;
return this.aff().x
}
get y() {
return this.aff().y;
return this.aff().y
}
equals(t) {
const { px: n, py: e, pz: r } = this,
{ px: o, py: i, pz: s } = isPoint(t),
const {px: n, py: e, pz: r} = this,
{px: o, py: i, pz: s} = isPoint(t),
a = mod(n * s),
c = mod(o * r),
d = mod(e * s),
u = mod(i * r);
return a === c && d === u;
u = mod(i * r)
return a === c && d === u
}
negate() {
return new Point(this.px, mod(-this.py), this.pz);
return new Point(this.px, mod(-this.py), this.pz)
}
double() {
return this.add(this);
return this.add(this)
}
add(t) {
const { px: n, py: e, pz: r } = this,
{ px: o, py: i, pz: s } = isPoint(t),
{ a, b: c } = CURVE;
const {px: n, py: e, pz: r} = this,
{px: o, py: i, pz: s} = isPoint(t),
{a, b: c} = CURVE
let d = 0n,
u = 0n,
m = 0n;
const h = mod(3n * c);
m = 0n
const h = mod(3n * c)
let l = mod(n * o),
y = mod(e * i),
f = mod(r * s),
p = mod(n + e),
b = mod(o + i);
(p = mod(p * b)), (b = mod(l + y)), (p = mod(p - b)), (b = mod(n + r));
let g = mod(o + s);
b = mod(o + i)
;((p = mod(p * b)), (b = mod(l + y)), (p = mod(p - b)), (b = mod(n + r)))
let g = mod(o + s)
return (
(b = mod(b * g)),
(g = mod(l + f)),
@ -123,300 +123,301 @@ class Point {
(m = mod(g * m)),
(m = mod(m + l)),
new Point(d, u, m)
);
)
}
mul(t, n = !0) {
if (!n && 0n === t) return I;
if ((ge(t) || err("invalid scalar"), this.equals(G))) return wNAF(t).p;
if (!n && 0n === t) return I
if ((ge(t) || err('invalid scalar'), this.equals(G))) return wNAF(t).p
let e = I,
r = G;
r = G
for (let o = this; t > 0n; o = o.double(), t >>= 1n)
1n & t ? (e = e.add(o)) : n && (r = r.add(o));
return e;
1n & t ? (e = e.add(o)) : n && (r = r.add(o))
return e
}
mulAddQUns(t, n, e) {
return this.mul(n, !1).add(t.mul(e, !1)).ok();
return this.mul(n, !1).add(t.mul(e, !1)).ok()
}
toAffine() {
const { px: t, py: n, pz: e } = this;
if (this.equals(I)) return { x: 0n, y: 0n };
if (1n === e) return { x: t, y: n };
const r = inv(e);
const {px: t, py: n, pz: e} = this
if (this.equals(I)) return {x: 0n, y: 0n}
if (1n === e) return {x: t, y: n}
const r = inv(e)
return (
1n !== mod(e * r) && err("invalid inverse"),
{ x: mod(t * r), y: mod(n * r) }
);
1n !== mod(e * r) && err('invalid inverse'),
{x: mod(t * r), y: mod(n * r)}
)
}
assertValidity() {
const { x: t, y: n } = this.aff();
const {x: t, y: n} = this.aff()
return (
(fe(t) && fe(n)) || err("Point invalid: x or y"),
mod(n * n) === crv(t) ? this : err("Point invalid: not on curve")
);
(fe(t) && fe(n)) || err('Point invalid: x or y'),
mod(n * n) === crv(t) ? this : err('Point invalid: not on curve')
)
}
multiply(t) {
return this.mul(t);
return this.mul(t)
}
aff() {
return this.toAffine();
return this.toAffine()
}
ok() {
return this.assertValidity();
return this.assertValidity()
}
toHex(t = !0) {
const { x: n, y: e } = this.aff();
const {x: n, y: e} = this.aff()
return (
(t ? (0n === (1n & e) ? "02" : "03") : "04") + n2h(n) + (t ? "" : n2h(e))
);
(t ? (0n === (1n & e) ? '02' : '03') : '04') + n2h(n) + (t ? '' : n2h(e))
)
}
toRawBytes(t = !0) {
return h2b(this.toHex(t));
return h2b(this.toHex(t))
}
}
(Point.BASE = new Point(Gx, Gy, 1n)), (Point.ZERO = new Point(0n, 1n, 0n));
const { BASE: G, ZERO: I } = Point,
padh = (t, n) => t.toString(16).padStart(n, "0"),
b2h = (t) =>
;((Point.BASE = new Point(Gx, Gy, 1n)), (Point.ZERO = new Point(0n, 1n, 0n)))
const {BASE: G, ZERO: I} = Point,
padh = (t, n) => t.toString(16).padStart(n, '0'),
b2h = t =>
Array.from(t)
.map((t) => padh(t, 2))
.join(""),
h2b = (t) => {
const n = t.length;
(!str(t) || n % 2) && err("hex invalid 1");
const e = u8n(n / 2);
.map(t => padh(t, 2))
.join(''),
h2b = t => {
const n = t.length
;(!str(t) || n % 2) && err('hex invalid 1')
const e = u8n(n / 2)
for (let n = 0; n < e.length; n++) {
const r = 2 * n,
o = t.slice(r, r + 2),
i = Number.parseInt(o, 16);
(Number.isNaN(i) || i < 0) && err("hex invalid 2"), (e[n] = i);
i = Number.parseInt(o, 16)
;((Number.isNaN(i) || i < 0) && err('hex invalid 2'), (e[n] = i))
}
return e;
return e
},
b2n = (t) => BigInt("0x" + (b2h(t) || "0")),
b2n = t => BigInt('0x' + (b2h(t) || '0')),
slcNum = (t, n, e) => b2n(t.slice(n, e)),
n2b = (t) =>
big(t) && t >= 0n && t < B256 ? h2b(padh(t, 64)) : err("bigint expected"),
n2h = (t) => b2h(n2b(t)),
n2b = t =>
big(t) && t >= 0n && t < B256 ? h2b(padh(t, 64)) : err('bigint expected'),
n2h = t => b2h(n2b(t)),
concatB = (...t) => {
const n = u8n(t.reduce((t, n) => t + au8(n).length, 0));
let e = 0;
const n = u8n(t.reduce((t, n) => t + au8(n).length, 0))
let e = 0
return (
t.forEach((t) => {
n.set(t, e), (e += t.length);
t.forEach(t => {
;(n.set(t, e), (e += t.length))
}),
n
);
)
},
inv = (t, n = P) => {
(0n === t || n <= 0n) && err("no inverse n=" + t + " mod=" + n);
;(0n === t || n <= 0n) && err('no inverse n=' + t + ' mod=' + n)
let e = mod(t, n),
r = n,
o = 0n,
i = 1n,
s = 1n,
a = 0n;
a = 0n
for (; 0n !== e; ) {
const t = r / e,
n = r % e,
c = o - s * t,
d = i - a * t;
(r = e), (e = n), (o = s), (i = a), (s = c), (a = d);
d = i - a * t
;((r = e), (e = n), (o = s), (i = a), (s = c), (a = d))
}
return 1n === r ? mod(o, n) : err("no inverse");
return 1n === r ? mod(o, n) : err('no inverse')
},
sqrt = (t) => {
let n = 1n;
sqrt = t => {
let n = 1n
for (let e = t, r = (P + 1n) / 4n; r > 0n; r >>= 1n)
1n & r && (n = (n * e) % P), (e = (e * e) % P);
return mod(n * n) === t ? n : err("sqrt invalid");
(1n & r && (n = (n * e) % P), (e = (e * e) % P))
return mod(n * n) === t ? n : err('sqrt invalid')
},
toPriv = (t) => (
toPriv = t => (
big(t) || (t = b2n(toU8(t, 32))),
ge(t) ? t : err("private key out of range")
ge(t) ? t : err('private key out of range')
),
moreThanHalfN = (t) => t > N >> 1n,
getPublicKey = (t, n = !0) => Point.fromPrivateKey(t).toRawBytes(n);
moreThanHalfN = t => t > N >> 1n,
getPublicKey = (t, n = !0) => Point.fromPrivateKey(t).toRawBytes(n)
class Signature {
constructor(t, n, e) {
(this.r = t), (this.s = n), (this.recovery = e), this.assertValidity();
;((this.r = t), (this.s = n), (this.recovery = e), this.assertValidity())
}
static fromCompact(t) {
return (
(t = toU8(t, 64)), new Signature(slcNum(t, 0, 32), slcNum(t, 32, 64))
);
(t = toU8(t, 64)),
new Signature(slcNum(t, 0, 32), slcNum(t, 32, 64))
)
}
assertValidity() {
return ge(this.r) && ge(this.s) ? this : err();
return ge(this.r) && ge(this.s) ? this : err()
}
addRecoveryBit(t) {
return new Signature(this.r, this.s, t);
return new Signature(this.r, this.s, t)
}
hasHighS() {
return moreThanHalfN(this.s);
return moreThanHalfN(this.s)
}
normalizeS() {
return this.hasHighS()
? new Signature(this.r, mod(this.s, N), this.recovery)
: this;
: this
}
recoverPublicKey(t) {
const { r: n, s: e, recovery: r } = this;
[0, 1, 2, 3].includes(r) || err("recovery id invalid");
const {r: n, s: e, recovery: r} = this
;[0, 1, 2, 3].includes(r) || err('recovery id invalid')
const o = bits2int_modN(toU8(t, 32)),
i = 2 === r || 3 === r ? n + N : n;
i >= P && err("q.x invalid");
const s = 1 & r ? "03" : "02",
i = 2 === r || 3 === r ? n + N : n
i >= P && err('q.x invalid')
const s = 1 & r ? '03' : '02',
a = Point.fromHex(s + n2h(i)),
c = inv(i, N),
d = mod(-o * c, N),
u = mod(e * c, N);
return G.mulAddQUns(a, d, u);
u = mod(e * c, N)
return G.mulAddQUns(a, d, u)
}
toCompactRawBytes() {
return h2b(this.toCompactHex());
return h2b(this.toCompactHex())
}
toCompactHex() {
return n2h(this.r) + n2h(this.s);
return n2h(this.r) + n2h(this.s)
}
}
const bits2int = (t) => {
const bits2int = t => {
const n = 8 * t.length - 256,
e = b2n(t);
return n > 0 ? e >> BigInt(n) : e;
e = b2n(t)
return n > 0 ? e >> BigInt(n) : e
},
bits2int_modN = (t) => mod(bits2int(t), N),
i2o = (t) => n2b(t),
bits2int_modN = t => mod(bits2int(t), N),
i2o = t => n2b(t),
cr = () =>
"object" == typeof globalThis && "crypto" in globalThis
'object' == typeof globalThis && 'crypto' in globalThis
? globalThis.crypto
: void 0;
let _hmacSync;
const optS = { lowS: !0 },
optV = { lowS: !0 },
: void 0
let _hmacSync
const optS = {lowS: !0},
optV = {lowS: !0},
prepSig = (t, n, e = optS) => {
["der", "recovered", "canonical"].some((t) => t in e) &&
err("sign() legacy options not supported");
let { lowS: r } = e;
null == r && (r = !0);
;['der', 'recovered', 'canonical'].some(t => t in e) &&
err('sign() legacy options not supported')
let {lowS: r} = e
null == r && (r = !0)
const o = bits2int_modN(toU8(t)),
i = i2o(o),
s = toPriv(n),
a = [i2o(s), i];
let c = e.extraEntropy;
a = [i2o(s), i]
let c = e.extraEntropy
if (c) {
!0 === c && (c = etc.randomBytes(32));
const t = toU8(c);
32 !== t.length && err(), a.push(t);
!0 === c && (c = etc.randomBytes(32))
const t = toU8(c)
;(32 !== t.length && err(), a.push(t))
}
const d = o;
const d = o
return {
seed: concatB(...a),
k2sig: (t) => {
const n = bits2int(t);
if (!ge(n)) return;
k2sig: t => {
const n = bits2int(t)
if (!ge(n)) return
const e = inv(n, N),
o = G.mul(n).aff(),
i = mod(o.x, N);
if (0n === i) return;
const a = mod(e * mod(d + mod(s * i, N), N), N);
if (0n === a) return;
i = mod(o.x, N)
if (0n === i) return
const a = mod(e * mod(d + mod(s * i, N), N), N)
if (0n === a) return
let c = a,
u = (o.x === i ? 0 : 2) | Number(1n & o.y);
u = (o.x === i ? 0 : 2) | Number(1n & o.y)
return (
r && moreThanHalfN(a) && ((c = mod(-a, N)), (u ^= 1)),
new Signature(i, c, u)
);
},
};
};
)
}
}
}
function hmacDrbg(t) {
let n = u8n(32),
e = u8n(32),
r = 0;
r = 0
const o = () => {
n.fill(1), e.fill(0), (r = 0);
;(n.fill(1), e.fill(0), (r = 0))
},
i = "drbg: tried 1000 values";
i = 'drbg: tried 1000 values'
if (t) {
const t = (...t) => etc.hmacSha256Async(e, n, ...t),
s = async (r = u8n()) => {
(e = await t(u8n([0]), r)),
;((e = await t(u8n([0]), r)),
(n = await t()),
0 !== r.length && ((e = await t(u8n([1]), r)), (n = await t()));
0 !== r.length && ((e = await t(u8n([1]), r)), (n = await t())))
},
a = async () => (r++ >= 1e3 && err(i), (n = await t()), n);
a = async () => (r++ >= 1e3 && err(i), (n = await t()), n)
return async (t, n) => {
let e;
for (o(), await s(t); !(e = n(await a())); ) await s();
return o(), e;
};
let e
for (o(), await s(t); !(e = n(await a())); ) await s()
return (o(), e)
}
}
{
const t = (...t) => {
const r = _hmacSync;
return r || err("etc.hmacSha256Sync not set"), r(e, n, ...t);
const r = _hmacSync
return (r || err('etc.hmacSha256Sync not set'), r(e, n, ...t))
},
s = (r = u8n()) => {
(e = t(u8n([0]), r)),
;((e = t(u8n([0]), r)),
(n = t()),
0 !== r.length && ((e = t(u8n([1]), r)), (n = t()));
0 !== r.length && ((e = t(u8n([1]), r)), (n = t())))
},
a = () => (r++ >= 1e3 && err(i), (n = t()), n);
a = () => (r++ >= 1e3 && err(i), (n = t()), n)
return (t, n) => {
let e;
for (o(), s(t); !(e = n(a())); ) s();
return o(), e;
};
let e
for (o(), s(t); !(e = n(a())); ) s()
return (o(), e)
}
}
}
const signAsync = async (t, n, e = optS) => {
const { seed: r, k2sig: o } = prepSig(t, n, e);
return hmacDrbg(!0)(r, o);
const {seed: r, k2sig: o} = prepSig(t, n, e)
return hmacDrbg(!0)(r, o)
},
sign = (t, n, e = optS) => {
const { seed: r, k2sig: o } = prepSig(t, n, e);
return hmacDrbg(!1)(r, o);
const {seed: r, k2sig: o} = prepSig(t, n, e)
return hmacDrbg(!1)(r, o)
},
verify = (t, n, e, r = optV) => {
let o,
i,
s,
{ lowS: a } = r;
null == a && (a = !0),
"strict" in r && err("verify() legacy options not supported");
const c = t && "object" == typeof t && "r" in t;
c || 64 === toU8(t).length || err("signature must be 64 bytes");
{lowS: a} = r
;(null == a && (a = !0),
'strict' in r && err('verify() legacy options not supported'))
const c = t && 'object' == typeof t && 'r' in t
c || 64 === toU8(t).length || err('signature must be 64 bytes')
try {
(o = c
;((o = c
? new Signature(t.r, t.s).assertValidity()
: Signature.fromCompact(t)),
(i = bits2int_modN(toU8(n))),
(s = e instanceof Point ? e.ok() : Point.fromHex(e));
(s = e instanceof Point ? e.ok() : Point.fromHex(e)))
} catch (t) {
return !1;
return !1
}
if (!o) return !1;
const { r: d, s: u } = o;
if (a && moreThanHalfN(u)) return !1;
let m;
if (!o) return !1
const {r: d, s: u} = o
if (a && moreThanHalfN(u)) return !1
let m
try {
const t = inv(u, N),
n = mod(i * t, N),
e = mod(d * t, N);
m = G.mulAddQUns(s, n, e).aff();
e = mod(d * t, N)
m = G.mulAddQUns(s, n, e).aff()
} catch (t) {
return !1;
return !1
}
if (!m) return !1;
return mod(m.x, N) === d;
if (!m) return !1
return mod(m.x, N) === d
},
getSharedSecret = (t, n, e = !0) =>
Point.fromHex(n).mul(toPriv(t)).toRawBytes(e),
hashToPrivateKey = (t) => {
((t = toU8(t)).length < 40 || t.length > 1024) &&
err("expected proper params");
const n = mod(b2n(t), N - 1n) + 1n;
return n2b(n);
hashToPrivateKey = t => {
;((t = toU8(t)).length < 40 || t.length > 1024) &&
err('expected proper params')
const n = mod(b2n(t), N - 1n) + 1n
return n2b(n)
},
etc = {
hexToBytes: h2b,
@ -428,84 +429,84 @@ const signAsync = async (t, n, e = optS) => {
invert: inv,
hmacSha256Async: async (t, ...n) => {
const e = cr(),
r = e && e.subtle;
if (!r) return err("etc.hmacSha256Async not set");
r = e && e.subtle
if (!r) return err('etc.hmacSha256Async not set')
const o = await r.importKey(
"raw",
'raw',
t,
{ name: "HMAC", hash: { name: "SHA-256" } },
{name: 'HMAC', hash: {name: 'SHA-256'}},
!1,
["sign"],
);
return u8n(await r.sign("HMAC", o, concatB(...n)));
['sign']
)
return u8n(await r.sign('HMAC', o, concatB(...n)))
},
hmacSha256Sync: _hmacSync,
hashToPrivateKey,
randomBytes: (t = 32) => {
const n = cr();
const n = cr()
return (
(n && n.getRandomValues) ||
err("crypto.getRandomValues must be defined"),
err('crypto.getRandomValues must be defined'),
n.getRandomValues(u8n(t))
);
},
)
}
},
utils = {
normPrivateKeyToScalar: toPriv,
isValidPrivateKey: (t) => {
isValidPrivateKey: t => {
try {
return !!toPriv(t);
return !!toPriv(t)
} catch (t) {
return !1;
return !1
}
},
randomPrivateKey: () => hashToPrivateKey(etc.randomBytes(48)),
precompute: (t = 8, n = G) => (n.multiply(3n), n),
};
precompute: (t = 8, n = G) => (n.multiply(3n), n)
}
Object.defineProperties(etc, {
hmacSha256Sync: {
configurable: !1,
get: () => _hmacSync,
set(t) {
_hmacSync || (_hmacSync = t);
},
},
});
_hmacSync || (_hmacSync = t)
}
}
})
const W = 8,
precompute = () => {
const t = [];
const t = []
let n = G,
e = n;
e = n
for (let r = 0; r < 33; r++) {
(e = n), t.push(e);
for (let r = 1; r < 128; r++) (e = e.add(n)), t.push(e);
n = e.double();
;((e = n), t.push(e))
for (let r = 1; r < 128; r++) ((e = e.add(n)), t.push(e))
n = e.double()
}
return t;
};
let Gpows;
const wNAF = (t) => {
return t
}
let Gpows
const wNAF = t => {
const n = Gpows || (Gpows = precompute()),
e = (t, n) => {
let e = n.negate();
return t ? e : n;
};
let e = n.negate()
return t ? e : n
}
let r = I,
o = G;
o = G
const i = BigInt(255),
s = BigInt(8);
s = BigInt(8)
for (let a = 0; a < 33; a++) {
const c = 128 * a;
let d = Number(t & i);
(t >>= s), d > 128 && ((d -= 256), (t += 1n));
const c = 128 * a
let d = Number(t & i)
;((t >>= s), d > 128 && ((d -= 256), (t += 1n)))
const u = c,
m = c + Math.abs(d) - 1,
h = a % 2 != 0,
l = d < 0;
0 === d ? (o = o.add(e(h, n[u]))) : (r = r.add(e(l, n[m])));
l = d < 0
0 === d ? (o = o.add(e(h, n[u]))) : (r = r.add(e(l, n[m])))
}
return { p: r, f: o };
};
return {p: r, f: o}
}
export {
getPublicKey,
sign,
@ -516,5 +517,5 @@ export {
etc,
utils,
Point as ProjectivePoint,
Signature,
};
Signature
}

View file

@ -1,7 +1,7 @@
import asyncio
import time
from math import ceil
from typing import Any, Dict, List, Optional, Tuple
from typing import Any
from bolt11 import decode as bolt11_decode
from lnbits.core.crud import get_payments, get_wallet, get_wallet_payment
@ -35,7 +35,7 @@ from .paranoia import (
from .permission import nwc_permissions
async def _check(nwc: Optional[NWCKey], method: str) -> Optional[Dict]:
async def _check(nwc: NWCKey | None, method: str) -> dict | None:
# check
if not nwc:
return {
@ -46,8 +46,8 @@ async def _check(nwc: Optional[NWCKey], method: str) -> Optional[Dict]:
allowed = False
permissions = nwc.get_permissions()
for p in permissions:
permissions_data: Dict[str, Any] = nwc_permissions.get(p, {})
allowed_methods: List[str] = permissions_data.get("methods", [])
permissions_data: dict[str, Any] = nwc_permissions.get(p, {})
allowed_methods: list[str] = permissions_data.get("methods", [])
if method in allowed_methods:
allowed = True
break
@ -64,7 +64,7 @@ async def _process_invoice(
pubkey: str,
invoice: str,
amount_msats: int,
description: Optional[str] = None,
description: str | None = None,
):
# hardening #
@ -80,7 +80,7 @@ async def _process_invoice(
payment = await pay_invoice(
wallet_id=wallet_id,
payment_request=invoice,
max_sat=int(ceil(amount_msats / 1000)),
max_sat=ceil(amount_msats / 1000),
description=description or "",
)
return payment.payment_hash
@ -109,7 +109,7 @@ async def _process_invoice(
wait_for_preimage = (
True # currently required by nip 47 specs, might change in future
)
payment_status: Optional[PaymentStatus] = None
payment_status: PaymentStatus | None = None
while wait_for_preimage:
payment_status = await check_transaction_status(wallet_id, payment_hash)
if payment_status.success:
@ -128,8 +128,8 @@ async def _process_invoice(
async def _on_pay_invoice(
sp: NWCServiceProvider, pubkey: str, payload: Dict
) -> List[Tuple[Optional[Dict], Optional[Dict], List]]:
sp: NWCServiceProvider, pubkey: str, payload: dict
) -> list[tuple[dict | None, dict | None, list]]:
# hardening #
assert_valid_pubkey(pubkey)
@ -169,8 +169,8 @@ async def _on_pay_invoice(
async def _on_multi_pay_invoice(
sp: NWCServiceProvider, pubkey: str, payload: Dict
) -> List[Tuple[Optional[Dict], Optional[Dict], List]]:
sp: NWCServiceProvider, pubkey: str, payload: dict
) -> list[tuple[dict | None, dict | None, list]]:
# hardening #
assert_valid_pubkey(pubkey)
@ -184,7 +184,7 @@ async def _on_multi_pay_invoice(
raise Exception("Pubkey has no associated wallet")
params = payload.get("params", {})
invoices = params.get("invoices", [])
results: List[Tuple[Optional[Dict], Optional[Dict], List]] = []
results: list[tuple[dict | None, dict | None, list]] = []
# Ensures all invoices are provided
for i in invoices:
@ -228,8 +228,8 @@ async def _on_multi_pay_invoice(
async def _on_make_invoice(
sp: NWCServiceProvider, pubkey: str, payload: Dict
) -> List[Tuple[Optional[Dict], Optional[Dict], List]]:
sp: NWCServiceProvider, pubkey: str, payload: dict
) -> list[tuple[dict | None, dict | None, list]]:
# hardening #
assert_valid_pubkey(pubkey)
@ -298,8 +298,8 @@ async def _on_make_invoice(
async def _on_lookup_invoice(
sp: NWCServiceProvider, pubkey: str, payload: Dict
) -> List[Tuple[Optional[Dict], Optional[Dict], List]]:
sp: NWCServiceProvider, pubkey: str, payload: dict
) -> list[tuple[dict | None, dict | None, list]]:
# hardening #
assert_valid_pubkey(pubkey)
@ -336,12 +336,12 @@ async def _on_lookup_invoice(
invoice_data = bolt11_decode(payment.bolt11)
is_settled = not payment.pending
timestamp = int(payment.time.timestamp()) or int(invoice_data.date)
expiry = int(payment.expiry.timestamp()) or timestamp + 3600
expiry = int(payment.expiry.timestamp()) if payment.expiry else timestamp + 3600
preimage = (
payment.preimage
or "0000000000000000000000000000000000000000000000000000000000000000"
)
res: Dict = {
res: dict = {
"type": "outgoing" if payment.is_out else "incoming",
"invoice": payment.bolt11,
"description": (
@ -363,8 +363,8 @@ async def _on_lookup_invoice(
async def _on_list_transactions(
sp: NWCServiceProvider, pubkey: str, payload: Dict
) -> List[Tuple[Optional[Dict], Optional[Dict], List]]:
sp: NWCServiceProvider, pubkey: str, payload: dict
) -> list[tuple[dict | None, dict | None, list]]:
# hardening #
assert_valid_pubkey(pubkey)
# ## #
@ -392,11 +392,9 @@ async def _on_list_transactions(
assert_sane_string(tx_type)
# ## #
values = []
filters: Filters = Filters()
filters.where(["time <= ?"])
values.append(tuntil)
filters.values(values)
filters.where(["time <= :tuntil"])
filters.values({"tuntil": tuntil})
history = await get_payments(
wallet_id=nwc.wallet,
complete=True,
@ -409,7 +407,7 @@ async def _on_list_transactions(
limit=limit,
offset=offset,
)
transactions: List[Dict] = []
transactions: list[dict] = []
p: Payment
for p in history:
invoice_data = bolt11_decode(p.bolt11)
@ -435,8 +433,8 @@ async def _on_list_transactions(
async def _on_get_balance(
sp: NWCServiceProvider, pubkey: str, payload: Dict
) -> List[Tuple[Optional[Dict], Optional[Dict], List]]:
sp: NWCServiceProvider, pubkey: str, payload: dict
) -> list[tuple[dict | None, dict | None, list]]:
# hardening #
assert_valid_pubkey(pubkey)
@ -458,8 +456,8 @@ async def _on_get_balance(
async def _on_get_info(
sp: NWCServiceProvider, pubkey: str, payload: Dict
) -> List[Tuple[Optional[Dict], Optional[Dict], List]]:
sp: NWCServiceProvider, pubkey: str, payload: dict
) -> list[tuple[dict | None, dict | None, list]]:
# hardening #
assert_valid_pubkey(pubkey)
@ -477,8 +475,8 @@ async def _on_get_info(
account_methods = []
for spm in sp_methods:
for p in permissions:
permissions_data: Dict[str, Any] = nwc_permissions.get(p, {})
allowed_methods: List[str] = permissions_data.get("methods", [])
permissions_data: dict[str, Any] = nwc_permissions.get(p, {})
allowed_methods: list[str] = permissions_data.get("methods", [])
if spm in allowed_methods:
account_methods.append(spm)
break

View file

@ -1,8 +1,8 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block scripts %} {{ window_vars(user) }}
<script type="module">
import * as NobleSecp256k1 from "/nwcprovider/static/js/noble-secp256k1.min.js";
window.NobleSecp256k1 = NobleSecp256k1;
import * as NobleSecp256k1 from '/nwcprovider/static/js/noble-secp256k1.min.js'
window.NobleSecp256k1 = NobleSecp256k1
</script>
<script src="{{ static_url_for('nwcprovider/static', path='js/index.js') }}"></script>
{% endblock %} {% block page %}

0
tests/__init__.py Normal file
View file

View file

@ -4,17 +4,17 @@ import hashlib
import json
import random
import time
from typing import Dict, List, Optional, Union
from typing import Union
import bolt11
import httpx
import pytest
import secp256k1
import websockets.client as websockets
from Cryptodome import Random
from Cryptodome.Cipher import AES
from Cryptodome.Util.Padding import pad, unpad
from loguru import logger
from websockets.legacy.client import connect
wallets = {
"wallet1": {
@ -107,8 +107,8 @@ def gen_keypair():
async def create_nwc(
w: str,
desc: str,
permissions: List[str],
budgets: List[Dict[str, int]],
permissions: list[str],
budgets: list[dict[str, int]],
expiration: int = 0,
):
keypair = gen_keypair()
@ -210,7 +210,7 @@ class NWCWallet:
async def _run(self):
while True:
try:
async with websockets.connect(self.relay) as ws:
async with connect(self.relay) as ws:
self.ws = ws
self.connected = True
self.sub_id = self._get_new_subid()
@ -244,7 +244,7 @@ class NWCWallet:
break
def _encrypt_content(
self, content: str, pubkey_hex: str, iv_seed: Optional[int] = None
self, content: str, pubkey_hex: str, iv_seed: int | None = None
) -> str:
pubkey = secp256k1.PublicKey(bytes.fromhex("02" + pubkey_hex), True)
shared = pubkey.tweak_mul(bytes.fromhex(self.private_key_hex)).serialize()[1:]
@ -273,7 +273,7 @@ class NWCWallet:
decrypted = decrypted_bytes.decode("utf-8")
return decrypted
async def _on_message(self, ws, message: str):
async def _on_message(self, _, message: str):
logger.debug("Received message: " + message)
msg = json.loads(message)
if msg[0] == "EVENT": # Event message
@ -292,12 +292,12 @@ class NWCWallet:
}
)
def _json_dumps(self, data: Union[Dict, list]) -> str:
if isinstance(data, Dict):
def _json_dumps(self, data: Union[dict, list]) -> str:
if isinstance(data, dict):
data = {k: v for k, v in data.items() if v is not None}
return json.dumps(data, separators=(",", ":"), ensure_ascii=False)
def _sign_event(self, event: Dict) -> Dict:
def _sign_event(self, event: dict) -> dict:
signature_data = self._json_dumps(
[
0,
@ -386,14 +386,14 @@ async def test_make_invoice():
await wallet1.send_event(
"make_invoice", {"amount": 1, "description": "test 123", "expiry": 1000}
)
result, tags, error = await wallet1.wait_for("make_invoice")
result, _, error = await wallet1.wait_for("make_invoice")
logger.info(error)
assert error, "Expected internal error, because amount is too low"
await wallet1.send_event(
"make_invoice", {"amount": 123000, "description": "test 123", "expiry": 1000}
)
result, tags, error = await wallet1.wait_for("make_invoice")
result, _, error = await wallet1.wait_for("make_invoice")
assert not error
assert result["type"] == "incoming"
assert result["description"] == "test 123"
@ -424,7 +424,7 @@ async def test_lookup_invoice():
await wallet1.send_event(
"make_invoice", {"amount": 123000, "description": "test 123", "expiry": 1000}
)
result, tags, error = await wallet1.wait_for("make_invoice")
result, _, error = await wallet1.wait_for("make_invoice")
assert not error
assert result["type"] == "incoming"
assert result["description"] == "test 123"
@ -440,7 +440,7 @@ async def test_lookup_invoice():
await wallet2.start()
await wallet2.send_event("lookup_invoice", {"invoice": result["invoice"]})
result, tags, error = await wallet2.wait_for("lookup_invoice")
result, _, error = await wallet2.wait_for("lookup_invoice")
assert not error
assert result["type"] == "incoming"
assert result["description"] == "test 123"
@ -465,7 +465,7 @@ async def test_get_info():
await wallet1.start()
await wallet1.send_event("get_info", {})
result, tags, error = await wallet1.wait_for("get_info")
result, _, error = await wallet1.wait_for("get_info")
assert not error
assert result["alias"] == "LNBits_NWC_SP"
assert result["color"] == ""
@ -492,33 +492,33 @@ async def test_permisions():
await wallet1.start()
await wallet1.send_event("get_info", {})
result, tags, error = await wallet1.wait_for("get_info")
result, _, error = await wallet1.wait_for("get_info")
assert not error
await wallet1.send_event(
"make_invoice", {"amount": 123000, "description": "test 123", "expiry": 1000}
)
result, tags, error = await wallet1.wait_for("make_invoice")
result, _, error = await wallet1.wait_for("make_invoice")
assert error
await wallet1.close()
await wallet2.start()
await wallet2.send_event("get_info", {})
result, tags, error = await wallet2.wait_for("get_info")
result, _, error = await wallet2.wait_for("get_info")
assert error
await wallet2.send_event(
"make_invoice", {"amount": 123000, "description": "test 123", "expiry": 1000}
)
result, tags, error = await wallet2.wait_for("make_invoice")
result, _, error = await wallet2.wait_for("make_invoice")
assert not error
await wallet2.close()
await wallet3.start()
await wallet3.send_event("get_info", {})
result, tags, error = await wallet3.wait_for("get_info")
result, _, error = await wallet3.wait_for("get_info")
assert not error
assert "make_invoice" in result["methods"]
assert "pay_invoice" in result["methods"]
@ -548,7 +548,7 @@ async def test_pay_invoice_and_balance():
"make_invoice", {"amount": 123000, "description": "test 123"}
)
result, tags, error = await wallet1.wait_for("make_invoice")
result, _, error = await wallet1.wait_for("make_invoice")
assert not error
assert result["invoice"]
@ -557,7 +557,7 @@ async def test_pay_invoice_and_balance():
await wallet2.start()
await wallet2.send_event("pay_invoice", {"invoice": invoice})
result, tags, error = await wallet2.wait_for("pay_invoice")
result, _, error = await wallet2.wait_for("pay_invoice")
assert not error
assert result["preimage"]
@ -569,12 +569,12 @@ async def test_pay_invoice_and_balance():
assert wallet2_balance_new == wallet2_balance - 123000
await wallet1.send_event("get_balance", {})
result, tags, error = await wallet1.wait_for("get_balance")
result, _, error = await wallet1.wait_for("get_balance")
assert not error
assert result["balance"] == wallet1_balance_new
await wallet2.send_event("get_balance", {})
result, tags, error = await wallet2.wait_for("get_balance")
result, _, error = await wallet2.wait_for("get_balance")
assert not error
assert result["balance"] == wallet2_balance_new
@ -703,13 +703,13 @@ async def test_insufficient_balance():
await wallet2.send_event(
"make_invoice", {"amount": amount_to_spend, "description": "test 123"}
)
result, tags, error = await wallet2.wait_for("make_invoice")
result, _, error = await wallet2.wait_for("make_invoice")
assert not error
assert result["invoice"]
invoice = result["invoice"]
await wallet1.send_event("pay_invoice", {"invoice": invoice})
result, tags, error = await wallet1.wait_for("pay_invoice")
result, _, error = await wallet1.wait_for("pay_invoice")
logger.info(error)
logger.info(result)
logger.info(amount_to_spend)
@ -735,7 +735,7 @@ async def test_expiry():
await wallet3.send_event(
"make_invoice", {"amount": 123000, "description": "test 123"}
)
result, tags, error = await wallet3.wait_for("make_invoice")
_, _, error = await wallet3.wait_for("make_invoice")
assert error
assert (
error["code"] == "UNAUTHORIZED"
@ -768,22 +768,22 @@ async def test_budget():
await wallet1.send_event(
"make_invoice", {"amount": 101000, "description": "Invalid"}
)
result, tags, error = await wallet1.wait_for("make_invoice")
result, _, error = await wallet1.wait_for("make_invoice")
assert not error
await wallet3.send_event("pay_invoice", {"invoice": result["invoice"]})
result, tags, error = await wallet3.wait_for("pay_invoice")
result, _, error = await wallet3.wait_for("pay_invoice")
assert error
assert (
error["code"] == "QUOTA_EXCEEDED"
), "Expected QUOTA_EXCEEDED error, because the budget was exceeded"
await wallet1.send_event("make_invoice", {"amount": 99000, "description": "Valid"})
result, tags, error = await wallet1.wait_for("make_invoice")
result, _, error = await wallet1.wait_for("make_invoice")
assert not error
await wallet3.send_event("pay_invoice", {"invoice": result["invoice"]})
result, tags, error = await wallet3.wait_for("pay_invoice")
result, _, error = await wallet3.wait_for("pay_invoice")
assert not error, "Expected successful payment, because the budget was not exceeded"
assert result["preimage"]
@ -791,11 +791,11 @@ async def test_budget():
"make_invoice", {"amount": 100000 - 99000 + 1000, "description": "Invalid"}
)
result, tags, error = await wallet1.wait_for("make_invoice")
result, _, error = await wallet1.wait_for("make_invoice")
assert not error
await wallet3.send_event("pay_invoice", {"invoice": result["invoice"]})
result, tags, error = await wallet3.wait_for("pay_invoice")
result, _, error = await wallet3.wait_for("pay_invoice")
assert error
assert (
error["code"] == "QUOTA_EXCEEDED"
@ -824,21 +824,21 @@ async def test_budget_refresh():
await wallet1.send_event(
"make_invoice", {"amount": 100000, "description": "Invalid"}
)
result, tags, error = await wallet1.wait_for("make_invoice")
result, _, error = await wallet1.wait_for("make_invoice")
assert not error
await wallet1.send_event(
"make_invoice", {"amount": 100000, "description": "Invalid"}
)
result2, tags, error = await wallet1.wait_for("make_invoice")
result2, _, error = await wallet1.wait_for("make_invoice")
assert not error
await wallet3.send_event("pay_invoice", {"invoice": result["invoice"]})
result, tags, error = await wallet3.wait_for("pay_invoice")
result, _, error = await wallet3.wait_for("pay_invoice")
assert not error, "Expected successful payment, because the budget was not exceeded"
await wallet3.send_event("pay_invoice", {"invoice": result2["invoice"]})
result, tags, error = await wallet3.wait_for("pay_invoice")
result, _, error = await wallet3.wait_for("pay_invoice")
assert error
assert (
error["code"] == "QUOTA_EXCEEDED"
@ -846,11 +846,11 @@ async def test_budget_refresh():
await asyncio.sleep(5)
await wallet1.send_event("make_invoice", {"amount": 100000, "description": "Valid"})
result, tags, error = await wallet1.wait_for("make_invoice")
result, _, error = await wallet1.wait_for("make_invoice")
assert not error
await wallet3.send_event("pay_invoice", {"invoice": result["invoice"]})
result, tags, error = await wallet3.wait_for("pay_invoice")
result, _, error = await wallet3.wait_for("pay_invoice")
assert not error, "Expected successful payment, because the budget was refreshed"
await wallet3.close()
@ -916,6 +916,7 @@ async def test_invalid_invoice_handling():
# Send invalid invoice
await wallet.send_event("pay_invoice", {"invoice": "invalid_lninvoice"})
_, _, error = await wallet.wait_for("pay_invoice")
assert error
assert error["code"] == "INTERNAL"
@ -935,6 +936,7 @@ async def test_replay_attack():
# Replay same message
await wallet.send_event("pay_invoice", {"invoice": valid_invoice})
_, _, error = await wallet.wait_for("pay_invoice")
assert error
assert error["code"] == "PAYMENT_FAILED"
@ -967,6 +969,7 @@ async def test_budget_bypass():
invoice2 = await create_valid_invoice(wallet, 60000)
await wallet.send_event("pay_invoice", {"invoice": invoice2})
_, _, error = await wallet.wait_for("pay_invoice")
assert error
assert error["code"] == "QUOTA_EXCEEDED"
@ -992,19 +995,28 @@ async def create_valid_invoice(wallet, amount=1000):
await wallet.send_event(
"make_invoice", {"amount": amount, "description": "test invoice"}
)
result, tags, error = await wallet.wait_for("make_invoice")
result, _, error = await wallet.wait_for("make_invoice")
if error:
raise Exception(f"Failed to create invoice: {error}")
return result["invoice"]
@pytest.mark.asyncio
async def test_list_transactions():
# Create wallets with required permissions
nwc1 = await create_nwc(
"wallet1", "test_list_transactions", ["invoice", "pay", "balance", "history"], [], 0
"wallet1",
"test_list_transactions",
["invoice", "pay", "balance", "history"],
[],
0,
)
nwc2 = await create_nwc(
"wallet2", "test_list_transactions", ["invoice", "pay", "balance", "history"], [], 0
"wallet2",
"test_list_transactions",
["invoice", "pay", "balance", "history"],
[],
0,
)
wallet1 = NWCWallet(nwc1["pairing"])

0
tests/unit/__init__.py Normal file
View file

View file

@ -1,18 +1,11 @@
import json
import os
import sys
from loguru import logger
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
####
import random
import string
import pytest
from loguru import logger
from nwcp import NWCServiceProvider
from ...nwcp import NWCServiceProvider
@pytest.fixture

2276
uv.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,4 @@
from http import HTTPStatus
from typing import Dict, List, Optional
import secp256k1
from fastapi import APIRouter, Depends, Request
@ -38,20 +37,18 @@ nwcprovider_api_router = APIRouter()
# Get supported permissions
@nwcprovider_api_router.get("/api/v1/permissions", status_code=HTTPStatus.OK)
async def api_get_permissions() -> Dict:
@nwcprovider_api_router.get("/api/v1/permissions")
async def api_get_permissions() -> dict:
return nwc_permissions
## Get nwc keys associated with the wallet
@nwcprovider_api_router.get(
"/api/v1/nwc", status_code=HTTPStatus.OK, response_model=List[NWCGetResponse]
)
@nwcprovider_api_router.get("/api/v1/nwc")
async def api_get_nwcs(
include_expired: bool = False,
calculate_spent_budget: bool = False,
wallet: WalletTypeInfo = Depends(require_admin_key),
):
) -> list[NWCGetResponse]:
wallet_id = wallet.wallet.id
# hardening #
@ -74,9 +71,7 @@ async def api_get_nwcs(
# Get a nwc key
@nwcprovider_api_router.get(
"/api/v1/nwc/{pubkey}", status_code=HTTPStatus.OK, response_model=NWCGetResponse
)
@nwcprovider_api_router.get("/api/v1/nwc/{pubkey}")
async def api_get_nwc(
pubkey: str,
include_expired: bool = False,
@ -102,22 +97,20 @@ async def api_get_nwc(
# Get pairing url for given secret
@nwcprovider_api_router.get(
"/api/v1/pairing/{secret}", status_code=HTTPStatus.OK, response_model=str
)
@nwcprovider_api_router.get("/api/v1/pairing/{secret}")
async def api_get_pairing_url(req: Request, secret: str) -> str:
# hardening #
assert_sane_string(secret)
# ## #
pprivkey: Optional[str] = await get_config_nwc("provider_key")
pprivkey: str | None = await get_config_nwc("provider_key")
if not pprivkey:
raise Exception("Extension is not configured")
relay = await get_config_nwc("relay")
if not relay:
raise Exception("Extension is not configured")
relay_alias: Optional[str] = await get_config_nwc("relay_alias")
relay_alias: str | None = await get_config_nwc("relay_alias")
if relay_alias:
relay = relay_alias
else:
@ -147,13 +140,12 @@ async def api_get_pairing_url(req: Request, secret: str) -> str:
@nwcprovider_api_router.put(
"/api/v1/nwc/{pubkey}",
status_code=HTTPStatus.CREATED,
response_model=NWCGetResponse,
)
async def api_register_nwc(
pubkey: str,
data: NWCRegistrationRequest,
wallet: WalletTypeInfo = Depends(require_admin_key),
):
) -> NWCGetResponse:
wallet_id = wallet.wallet.id
# hardening #
@ -177,7 +169,7 @@ async def api_register_nwc(
# Delete a nwc key
@nwcprovider_api_router.delete("/api/v1/nwc/{pubkey}", status_code=HTTPStatus.OK)
@nwcprovider_api_router.delete("/api/v1/nwc/{pubkey}")
async def api_delete_nwc(
pubkey: str, wallet: WalletTypeInfo = Depends(require_admin_key)
):
@ -193,9 +185,7 @@ async def api_delete_nwc(
# Get config
@nwcprovider_api_router.get(
"/api/v1/config", status_code=HTTPStatus.OK, dependencies=[Depends(check_admin)]
)
@nwcprovider_api_router.get("/api/v1/config", dependencies=[Depends(check_admin)])
async def api_get_all_config_nwc():
config = await get_all_config_nwc()
return config
@ -204,7 +194,6 @@ async def api_get_all_config_nwc():
# Get config
@nwcprovider_api_router.get(
"/api/v1/config/{key}",
status_code=HTTPStatus.OK,
dependencies=[Depends(check_admin)],
)
async def api_get_config_nwc(key: str):
@ -215,9 +204,7 @@ async def api_get_config_nwc(key: str):
# Set config
@nwcprovider_api_router.post(
"/api/v1/config", status_code=HTTPStatus.OK, dependencies=[Depends(check_admin)]
)
@nwcprovider_api_router.post("/api/v1/config", dependencies=[Depends(check_admin)])
async def api_set_config_nwc(req: Request):
data = await req.json()