fix: use lnbits linting, change to uv (#9)
* 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:
parent
c1f9d6a754
commit
603b345073
28 changed files with 5059 additions and 2199 deletions
10
.github/workflows/lint.yml
vendored
Normal file
10
.github/workflows/lint.yml
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
name: lint
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
uses: lnbits/lnbits/.github/workflows/lint.yml@dev
|
||||||
52
.github/workflows/main.yml
vendored
52
.github/workflows/main.yml
vendored
|
|
@ -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
|
|
||||||
26
.github/workflows/release.yml
vendored
26
.github/workflows/release.yml
vendored
|
|
@ -1,51 +1,57 @@
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "v[0-9]+.[0-9]+.[0-9]+"
|
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Create github release
|
- name: Create github release
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
tag: ${{ github.ref_name }}
|
tag: ${{ github.ref_name }}
|
||||||
run: |
|
run: |
|
||||||
gh release create "$tag" --generate-notes
|
gh release create "$tag" --generate-notes
|
||||||
|
|
||||||
pullrequest:
|
pullrequest:
|
||||||
needs: [release]
|
needs: [release]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.EXT_GITHUB }}
|
token: ${{ secrets.EXT_GITHUB }}
|
||||||
repository: lnbits/lnbits-extensions
|
repository: lnbits/lnbits-extensions
|
||||||
path: "./lnbits-extensions"
|
path: './lnbits-extensions'
|
||||||
|
|
||||||
- name: setup git user
|
- name: setup git user
|
||||||
run: |
|
run: |
|
||||||
git config --global user.name "alan"
|
git config --global user.name "alan"
|
||||||
git config --global user.email "alan@lnbits.com"
|
git config --global user.email "alan@lnbits.com"
|
||||||
|
|
||||||
- name: Create pull request in extensions repo
|
- name: Create pull request in extensions repo
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.EXT_GITHUB }}
|
GH_TOKEN: ${{ secrets.EXT_GITHUB }}
|
||||||
repo_name: "${{ github.event.repository.name }}"
|
repo_name: '${{ github.event.repository.name }}'
|
||||||
tag: "${{ github.ref_name }}"
|
tag: '${{ github.ref_name }}'
|
||||||
branch: "update-${{ github.event.repository.name }}-${{ github.ref_name }}"
|
branch: 'update-${{ github.event.repository.name }}-${{ github.ref_name }}'
|
||||||
title: "[UPDATE] ${{ github.event.repository.name }} to ${{ 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 }}"
|
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"
|
archive: 'https://github.com/lnbits/${{ github.event.repository.name }}/archive/refs/tags/${{ github.ref_name }}.zip'
|
||||||
run: |
|
run: |
|
||||||
cd lnbits-extensions
|
cd lnbits-extensions
|
||||||
git checkout -b $branch
|
git checkout -b $branch
|
||||||
|
|
||||||
# if there is another open PR
|
# if there is another open PR
|
||||||
git pull origin $branch || echo "branch does not exist"
|
git pull origin $branch || echo "branch does not exist"
|
||||||
|
|
||||||
sh util.sh update_extension $repo_name $tag
|
sh util.sh update_extension $repo_name $tag
|
||||||
|
|
||||||
git add -A
|
git add -A
|
||||||
git commit -am "$title"
|
git commit -am "$title"
|
||||||
git push origin $branch
|
git push origin $branch
|
||||||
|
|
||||||
# check if pr exists before creating it
|
# check if pr exists before creating it
|
||||||
gh config set pager cat
|
gh config set pager cat
|
||||||
check=$(gh pr list -H $branch | wc -l)
|
check=$(gh pr list -H $branch | wc -l)
|
||||||
|
|
|
||||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
|
|
@ -10,12 +10,12 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: "3.9"
|
python-version: '3.10'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
|
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -8,6 +8,5 @@ node_modules
|
||||||
.mypy_cache
|
.mypy_cache
|
||||||
data
|
data
|
||||||
.vscode
|
.vscode
|
||||||
package.json
|
|
||||||
package-lock.json
|
|
||||||
dump
|
dump
|
||||||
|
.venv
|
||||||
|
|
|
||||||
12
.prettierrc
Normal file
12
.prettierrc
Normal 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
|
||||||
|
}
|
||||||
24
Makefile
24
Makefile
|
|
@ -5,27 +5,27 @@ format: prettier black ruff
|
||||||
check: mypy pyright checkblack checkruff checkprettier
|
check: mypy pyright checkblack checkruff checkprettier
|
||||||
|
|
||||||
prettier:
|
prettier:
|
||||||
poetry run ./node_modules/.bin/prettier --write .
|
uv run ./node_modules/.bin/prettier --write .
|
||||||
pyright:
|
pyright:
|
||||||
poetry run ./node_modules/.bin/pyright
|
uv run ./node_modules/.bin/pyright
|
||||||
|
|
||||||
mypy:
|
mypy:
|
||||||
poetry run mypy .
|
uv run mypy .
|
||||||
|
|
||||||
black:
|
black:
|
||||||
poetry run black .
|
uv run black .
|
||||||
|
|
||||||
ruff:
|
ruff:
|
||||||
poetry run ruff check . --fix
|
uv run ruff check . --fix
|
||||||
|
|
||||||
checkruff:
|
checkruff:
|
||||||
poetry run ruff check .
|
uv run ruff check .
|
||||||
|
|
||||||
checkprettier:
|
checkprettier:
|
||||||
poetry run ./node_modules/.bin/prettier --check .
|
uv run ./node_modules/.bin/prettier --check .
|
||||||
|
|
||||||
checkblack:
|
checkblack:
|
||||||
poetry run black --check .
|
uv run black --check .
|
||||||
|
|
||||||
checkeditorconfig:
|
checkeditorconfig:
|
||||||
editorconfig-checker
|
editorconfig-checker
|
||||||
|
|
@ -33,14 +33,14 @@ checkeditorconfig:
|
||||||
test:
|
test:
|
||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
DEBUG=true \
|
DEBUG=true \
|
||||||
poetry run pytest
|
uv run pytest
|
||||||
install-pre-commit-hook:
|
install-pre-commit-hook:
|
||||||
@echo "Installing pre-commit hook to git"
|
@echo "Installing pre-commit hook to git"
|
||||||
@echo "Uninstall the hook with poetry run pre-commit uninstall"
|
@echo "Uninstall the hook with uv run pre-commit uninstall"
|
||||||
poetry run pre-commit install
|
uv run pre-commit install
|
||||||
|
|
||||||
pre-commit:
|
pre-commit:
|
||||||
poetry run pre-commit run --all-files
|
uv run pre-commit run --all-files
|
||||||
|
|
||||||
|
|
||||||
checkbundle:
|
checkbundle:
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ def nwcprovider_start():
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"db",
|
"db",
|
||||||
"nwcprovider_ext",
|
"nwcprovider_ext",
|
||||||
"nwcprovider_static_files",
|
|
||||||
"nwcprovider_start",
|
"nwcprovider_start",
|
||||||
|
"nwcprovider_static_files",
|
||||||
"nwcprovider_stop",
|
"nwcprovider_stop",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
36
crud.py
36
crud.py
|
|
@ -1,5 +1,4 @@
|
||||||
import time
|
import time
|
||||||
from typing import List, Optional
|
|
||||||
|
|
||||||
from lnbits.db import Database
|
from lnbits.db import Database
|
||||||
|
|
||||||
|
|
@ -11,6 +10,7 @@ from .models import (
|
||||||
GetNWC,
|
GetNWC,
|
||||||
GetWalletNWC,
|
GetWalletNWC,
|
||||||
NWCBudget,
|
NWCBudget,
|
||||||
|
NWCConfig,
|
||||||
NWCKey,
|
NWCKey,
|
||||||
NWCNewBudget,
|
NWCNewBudget,
|
||||||
TrackedSpendNWC,
|
TrackedSpendNWC,
|
||||||
|
|
@ -71,6 +71,7 @@ async def delete_nwc(data: DeleteNWC) -> None:
|
||||||
|
|
||||||
# hardening #
|
# hardening #
|
||||||
assert_valid_pubkey(data.pubkey)
|
assert_valid_pubkey(data.pubkey)
|
||||||
|
if data.wallet:
|
||||||
assert_valid_wallet_id(data.wallet)
|
assert_valid_wallet_id(data.wallet)
|
||||||
# ## #
|
# ## #
|
||||||
|
|
||||||
|
|
@ -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
|
expires = int(time.time()) if not data.include_expired else -1
|
||||||
|
|
||||||
|
if not data.wallet:
|
||||||
|
return []
|
||||||
|
|
||||||
# hardening #
|
# hardening #
|
||||||
assert_valid_wallet_id(data.wallet)
|
assert_valid_wallet_id(data.wallet)
|
||||||
assert_valid_expiration_seconds(expires)
|
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
|
expires = int(time.time()) if not data.include_expired else -1
|
||||||
|
|
||||||
# hardening #
|
# hardening #
|
||||||
|
|
@ -150,17 +154,17 @@ async def get_nwc(data: GetNWC) -> Optional[NWCKey]:
|
||||||
return row
|
return row
|
||||||
|
|
||||||
|
|
||||||
async def get_budgets_nwc(data: GetBudgetsNWC) -> Optional[NWCBudget]:
|
async def get_budgets_nwc(data: GetBudgetsNWC) -> list[NWCBudget]:
|
||||||
|
|
||||||
# hardening #
|
# hardening #
|
||||||
assert_valid_pubkey(data.pubkey)
|
assert_valid_pubkey(data.pubkey)
|
||||||
# ## #
|
# ## #
|
||||||
|
|
||||||
rows = await db.fetchall(
|
budgets = await db.fetchall(
|
||||||
"SELECT * FROM nwcprovider.budgets WHERE pubkey = :pubkey",
|
"SELECT * FROM nwcprovider.budgets WHERE pubkey = :pubkey",
|
||||||
{"pubkey": data.pubkey},
|
{"pubkey": data.pubkey},
|
||||||
|
model=NWCBudget,
|
||||||
)
|
)
|
||||||
budgets = [NWCBudget(**row) for row in rows]
|
|
||||||
if data.calculate_spent:
|
if data.calculate_spent:
|
||||||
for budget in budgets:
|
for budget in budgets:
|
||||||
last_cycle, next_cycle = budget.get_timestamp_range()
|
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)
|
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
|
SELECT SUM(amount_msats) FROM nwcprovider.spent
|
||||||
WHERE pubkey = :pubkey AND created_at >=
|
WHERE pubkey = :pubkey AND created_at >=
|
||||||
|
|
@ -182,9 +186,7 @@ async def get_budgets_nwc(data: GetBudgetsNWC) -> Optional[NWCBudget]:
|
||||||
"next_cycle": next_cycle,
|
"next_cycle": next_cycle,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
tot_spent_in_range_msats = (
|
tot_spent_in_range_msats = next(iter(result.values())) or 0
|
||||||
next(iter(tot_spent_in_range_msats.values())) or 0
|
|
||||||
)
|
|
||||||
|
|
||||||
# hardening #
|
# hardening #
|
||||||
assert_valid_msats(tot_spent_in_range_msats)
|
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):
|
async def get_config_nwc(key: str):
|
||||||
row = await db.fetchone(
|
config = await db.fetchone(
|
||||||
"SELECT * FROM nwcprovider.config WHERE key = :key", {"key": key}
|
"SELECT * FROM nwcprovider.config WHERE key = :key",
|
||||||
|
{"key": key},
|
||||||
|
model=NWCConfig,
|
||||||
)
|
)
|
||||||
if not row:
|
if not config:
|
||||||
return None
|
return None
|
||||||
return row["value"]
|
return config.value
|
||||||
|
|
||||||
|
|
||||||
async def set_config_nwc(key: str, value: str):
|
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():
|
async def get_all_config_nwc():
|
||||||
rows = await db.fetchall("SELECT * FROM nwcprovider.config")
|
rows = await db.fetchall("SELECT * FROM nwcprovider.config", model=NWCConfig)
|
||||||
return {row["key"]: row["value"] for row in rows}
|
return {row.key: row.value for row in rows}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import asyncio
|
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):
|
async def enqueue(action):
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,8 @@ async def m003_default_config(db):
|
||||||
new_private_key = bytes.hex(secp256k1._gen_private_key())
|
new_private_key = bytes.hex(secp256k1._gen_private_key())
|
||||||
await db.execute(
|
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;
|
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value;
|
||||||
""",
|
""",
|
||||||
{"provider_key": new_private_key},
|
{"provider_key": new_private_key},
|
||||||
|
|
@ -94,6 +95,7 @@ async def m004_default_config2(db):
|
||||||
{"value": ""},
|
{"value": ""},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def m005_key_last_used(db):
|
async def m005_key_last_used(db):
|
||||||
"""
|
"""
|
||||||
Add last_used to keys
|
Add last_used to keys
|
||||||
|
|
@ -111,7 +113,8 @@ async def m006_default_config3(db):
|
||||||
"""
|
"""
|
||||||
await db.execute(
|
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;
|
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value;
|
||||||
""",
|
""",
|
||||||
{"value": "0"},
|
{"value": "0"},
|
||||||
|
|
|
||||||
37
models.py
37
models.py
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from sqlite3 import Row
|
from sqlite3 import Row
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
@ -16,14 +16,14 @@ class NWCKey(BaseModel):
|
||||||
created_at: int
|
created_at: int
|
||||||
last_used: int
|
last_used: int
|
||||||
|
|
||||||
def get_permissions(self) -> List[str]:
|
def get_permissions(self) -> list[str]:
|
||||||
try:
|
try:
|
||||||
return self.permissions.split(" ")
|
return self.permissions.split(" ")
|
||||||
except Exception:
|
except Exception:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_row(cls, row: Dict[str, Any]) -> "NWCKey":
|
def from_row(cls, row: dict[str, Any]) -> "NWCKey":
|
||||||
return cls(**row)
|
return cls(**row)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -53,7 +53,7 @@ class NWCBudget(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class NWCNewBudget(BaseModel):
|
class NWCNewBudget(BaseModel):
|
||||||
pubkey: Optional[str]
|
pubkey: str | None
|
||||||
budget_msats: int
|
budget_msats: int
|
||||||
refresh_window: int
|
refresh_window: int
|
||||||
created_at: int
|
created_at: int
|
||||||
|
|
@ -65,30 +65,30 @@ class CreateNWCKey(BaseModel):
|
||||||
wallet: str
|
wallet: str
|
||||||
description: str
|
description: str
|
||||||
expires_at: int
|
expires_at: int
|
||||||
permissions: List[str]
|
permissions: list[str]
|
||||||
budgets: Optional[List[NWCNewBudget]] = None
|
budgets: list[NWCNewBudget] | None = None
|
||||||
|
|
||||||
|
|
||||||
class DeleteNWC(BaseModel):
|
class DeleteNWC(BaseModel):
|
||||||
pubkey: str
|
pubkey: str
|
||||||
wallet: Optional[str] = None
|
wallet: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class GetWalletNWC(BaseModel):
|
class GetWalletNWC(BaseModel):
|
||||||
wallet: Optional[str] = None
|
wallet: str | None = None
|
||||||
include_expired: Optional[bool] = False
|
include_expired: bool | None = False
|
||||||
|
|
||||||
|
|
||||||
class GetNWC(BaseModel):
|
class GetNWC(BaseModel):
|
||||||
pubkey: str
|
pubkey: str
|
||||||
wallet: Optional[str] = None
|
wallet: str | None = None
|
||||||
include_expired: Optional[bool] = False
|
include_expired: bool | None = False
|
||||||
refresh_last_used: Optional[bool] = False
|
refresh_last_used: bool | None = False
|
||||||
|
|
||||||
|
|
||||||
class GetBudgetsNWC(BaseModel):
|
class GetBudgetsNWC(BaseModel):
|
||||||
pubkey: str
|
pubkey: str
|
||||||
calculate_spent: Optional[bool] = False
|
calculate_spent: bool | None = False
|
||||||
|
|
||||||
|
|
||||||
class TrackedSpendNWC(BaseModel):
|
class TrackedSpendNWC(BaseModel):
|
||||||
|
|
@ -98,12 +98,17 @@ class TrackedSpendNWC(BaseModel):
|
||||||
|
|
||||||
# API models
|
# API models
|
||||||
class NWCRegistrationRequest(BaseModel):
|
class NWCRegistrationRequest(BaseModel):
|
||||||
permissions: List[str]
|
permissions: list[str]
|
||||||
description: str
|
description: str
|
||||||
expires_at: int
|
expires_at: int
|
||||||
budgets: List[NWCNewBudget]
|
budgets: list[NWCNewBudget]
|
||||||
|
|
||||||
|
|
||||||
class NWCGetResponse(BaseModel):
|
class NWCGetResponse(BaseModel):
|
||||||
data: NWCKey
|
data: NWCKey
|
||||||
budgets: List[NWCBudget]
|
budgets: list[NWCBudget]
|
||||||
|
|
||||||
|
|
||||||
|
class NWCConfig(BaseModel):
|
||||||
|
key: str
|
||||||
|
value: str
|
||||||
|
|
|
||||||
61
nwcp.py
61
nwcp.py
|
|
@ -4,16 +4,17 @@ import hashlib
|
||||||
import json
|
import json
|
||||||
import random
|
import random
|
||||||
import time
|
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 secp256k1
|
||||||
import websockets.client as websockets
|
|
||||||
from Cryptodome import Random
|
from Cryptodome import Random
|
||||||
from Cryptodome.Cipher import AES
|
from Cryptodome.Cipher import AES
|
||||||
from Cryptodome.Util.Padding import pad, unpad
|
from Cryptodome.Util.Padding import pad, unpad
|
||||||
from lnbits.helpers import encrypt_internal_message
|
from lnbits.helpers import encrypt_internal_message
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
from websockets.legacy.client import connect
|
||||||
|
|
||||||
|
|
||||||
class RateLimit:
|
class RateLimit:
|
||||||
|
|
@ -23,14 +24,14 @@ class RateLimit:
|
||||||
|
|
||||||
class MainSubscription:
|
class MainSubscription:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.requests_sub_id: Optional[str] = None
|
self.requests_sub_id: str | None = None
|
||||||
self.responses_sub_id: Optional[str] = None
|
self.responses_sub_id: str | None = None
|
||||||
self.requests_eose = False
|
self.requests_eose = False
|
||||||
self.responses_eose = False
|
self.responses_eose = False
|
||||||
self.events: Dict[str, Dict] = {}
|
self.events: dict[str, dict] = {}
|
||||||
self.responses: List[str] = []
|
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.
|
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:
|
if event_id not in self.responses:
|
||||||
self.responses.append(event_id)
|
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
|
Garbage collection, remove all the events that have a response older
|
||||||
than expire seconds (defaults to 1 hour if 0 or None)
|
than expire seconds (defaults to 1 hour if 0 or None)
|
||||||
|
|
@ -74,8 +75,8 @@ class MainSubscription:
|
||||||
class NWCServiceProvider:
|
class NWCServiceProvider:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
private_key: Optional[str] = None,
|
private_key: str | None = None,
|
||||||
relay: Optional[str] = None,
|
relay: str | None = None,
|
||||||
handle_missed_events: int = 0,
|
handle_missed_events: int = 0,
|
||||||
):
|
):
|
||||||
if not relay: # Connect to nostrclient
|
if not relay: # Connect to nostrclient
|
||||||
|
|
@ -100,17 +101,17 @@ class NWCServiceProvider:
|
||||||
self.public_key_hex = self.public_key.serialize().hex()[2:]
|
self.public_key_hex = self.public_key.serialize().hex()[2:]
|
||||||
|
|
||||||
# List of supported methods
|
# 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)
|
# Keep track of the number of subscriptions (used for unique subid)
|
||||||
self.subscriptions_count: int = 0
|
self.subscriptions_count: int = 0
|
||||||
|
|
||||||
# Request listeners, listen to specific methods
|
# Request listeners, listen to specific methods
|
||||||
self.request_listeners: Dict[
|
self.request_listeners: dict[
|
||||||
str,
|
str,
|
||||||
Callable[
|
Callable[
|
||||||
[NWCServiceProvider, str, Dict],
|
[NWCServiceProvider, str, dict],
|
||||||
Awaitable[List[Tuple[Optional[Dict], Optional[Dict], List]]],
|
Awaitable[list[tuple[dict | None, dict | None, list]]],
|
||||||
],
|
],
|
||||||
] = {}
|
] = {}
|
||||||
|
|
||||||
|
|
@ -122,7 +123,7 @@ class NWCServiceProvider:
|
||||||
|
|
||||||
# Subscription
|
# Subscription
|
||||||
self.sub = None
|
self.sub = None
|
||||||
self.rate_limit: Dict[str, RateLimit] = {}
|
self.rate_limit: dict[str, RateLimit] = {}
|
||||||
|
|
||||||
# websocket connection
|
# websocket connection
|
||||||
self.ws = None
|
self.ws = None
|
||||||
|
|
@ -161,8 +162,8 @@ class NWCServiceProvider:
|
||||||
self,
|
self,
|
||||||
method: str,
|
method: str,
|
||||||
listener: Callable[
|
listener: Callable[
|
||||||
["NWCServiceProvider", str, Dict],
|
["NWCServiceProvider", str, dict],
|
||||||
Awaitable[List[Tuple[Optional[Dict], Optional[Dict], List]]],
|
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.reconnect_task = asyncio.create_task(self._connect_to_relay())
|
||||||
self.gc_task = asyncio.create_task(self._gc_loop())
|
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.
|
Converts a Python dictionary to a JSON string with compact encoding.
|
||||||
|
|
||||||
|
|
@ -195,7 +196,7 @@ class NWCServiceProvider:
|
||||||
Returns:
|
Returns:
|
||||||
str: The compact JSON string.
|
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}
|
data = {k: v for k, v in data.items() if v is not None}
|
||||||
return json.dumps(data, separators=(",", ":"), ensure_ascii=False)
|
return json.dumps(data, separators=(",", ":"), ensure_ascii=False)
|
||||||
|
|
||||||
|
|
@ -205,7 +206,7 @@ class NWCServiceProvider:
|
||||||
"""
|
"""
|
||||||
return self.shutdown or not settings.lnbits_running
|
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.
|
Sends data to the relay.
|
||||||
|
|
||||||
|
|
@ -249,7 +250,7 @@ class NWCServiceProvider:
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
async def _ratelimit(self, unit: str, max_sleep_time: int = 120) -> None:
|
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:
|
if not limit:
|
||||||
self.rate_limit[unit] = limit = RateLimit()
|
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.requests_sub_id, req_filter])
|
||||||
await self._send(["REQ", self.sub.responses_sub_id, res_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
|
On connection callback, announce the service provider
|
||||||
methods and subscribe to nip67 events.
|
methods and subscribe to nip67 events.
|
||||||
|
|
@ -306,7 +307,7 @@ class NWCServiceProvider:
|
||||||
# Resubscribe to nwc events
|
# Resubscribe to nwc events
|
||||||
await self._subscribe()
|
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
|
Handle a nwc request
|
||||||
"""
|
"""
|
||||||
|
|
@ -319,7 +320,7 @@ class NWCServiceProvider:
|
||||||
# Handle request
|
# Handle request
|
||||||
method = content["method"]
|
method = content["method"]
|
||||||
listener = self.request_listeners.get(method, None)
|
listener = self.request_listeners.get(method, None)
|
||||||
outs: List[Dict[str, Any]] = []
|
outs: list[dict[str, Any]] = []
|
||||||
if not listener:
|
if not listener:
|
||||||
outs.append(
|
outs.append(
|
||||||
{
|
{
|
||||||
|
|
@ -352,7 +353,7 @@ class NWCServiceProvider:
|
||||||
if "error" in out:
|
if "error" in out:
|
||||||
content["error"] = out["error"]
|
content["error"] = out["error"]
|
||||||
# Prepare response event
|
# Prepare response event
|
||||||
res: Dict = {
|
res: dict = {
|
||||||
"kind": 23195,
|
"kind": 23195,
|
||||||
"created_at": int(time.time()),
|
"created_at": int(time.time()),
|
||||||
"tags": out.get("tags", []),
|
"tags": out.get("tags", []),
|
||||||
|
|
@ -451,7 +452,7 @@ class NWCServiceProvider:
|
||||||
await self._ratelimit("subscribing")
|
await self._ratelimit("subscribing")
|
||||||
await self._subscribe()
|
await self._subscribe()
|
||||||
|
|
||||||
async def _on_message(self, ws, message: str):
|
async def _on_message(self, _, message: str):
|
||||||
"""
|
"""
|
||||||
Handle incoming messages from the relay.
|
Handle incoming messages from the relay.
|
||||||
"""
|
"""
|
||||||
|
|
@ -484,7 +485,7 @@ class NWCServiceProvider:
|
||||||
): # Reconnect until the instance is shutting down
|
): # Reconnect until the instance is shutting down
|
||||||
logger.debug("Creating new connection...")
|
logger.debug("Creating new connection...")
|
||||||
try:
|
try:
|
||||||
async with websockets.connect(self.relay) as ws:
|
async with connect(self.relay) as ws:
|
||||||
self.ws = ws
|
self.ws = ws
|
||||||
self.connected = True
|
self.connected = True
|
||||||
await self._on_connection(ws)
|
await self._on_connection(ws)
|
||||||
|
|
@ -513,7 +514,7 @@ class NWCServiceProvider:
|
||||||
await self._ratelimit("connecting")
|
await self._ratelimit("connecting")
|
||||||
|
|
||||||
def _encrypt_content(
|
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:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Encrypts the content for the given public key
|
Encrypts the content for the given public key
|
||||||
|
|
@ -571,7 +572,7 @@ class NWCServiceProvider:
|
||||||
decrypted = decrypted_bytes.decode("utf-8")
|
decrypted = decrypted_bytes.decode("utf-8")
|
||||||
return decrypted
|
return decrypted
|
||||||
|
|
||||||
def _verify_event(self, event: Dict) -> bool:
|
def _verify_event(self, event: dict) -> bool:
|
||||||
"""
|
"""
|
||||||
Verify the event signature
|
Verify the event signature
|
||||||
|
|
||||||
|
|
@ -602,7 +603,7 @@ class NWCServiceProvider:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _sign_event(self, event: Dict) -> Dict:
|
def _sign_event(self, event: dict) -> dict:
|
||||||
"""
|
"""
|
||||||
Signs the event (in place)
|
Signs the event (in place)
|
||||||
|
|
||||||
|
|
|
||||||
62
package-lock.json
generated
Normal file
62
package-lock.json
generated
Normal 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
15
package.json
Normal 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
3385
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,37 +1,41 @@
|
||||||
[tool.poetry]
|
[project]
|
||||||
name = "nwcprovider"
|
name = "nwcprovider"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
description = "A NWC service provider for LNbits."
|
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]
|
[tool.poetry]
|
||||||
python = "^3.10 | ^3.9"
|
package-mode = false
|
||||||
lnbits = {version = "*", allow-prereleases = true}
|
|
||||||
mypy = "^1.13.0"
|
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.uv]
|
||||||
black = "^24.3.0"
|
dev-dependencies = [
|
||||||
pytest-asyncio = "^0.21.0"
|
"black",
|
||||||
pytest = "^7.3.2"
|
"pytest-asyncio",
|
||||||
mypy = "^1.5.1"
|
"pytest",
|
||||||
pre-commit = "^3.2.2"
|
"mypy",
|
||||||
ruff = "^0.3.2"
|
"pre-commit",
|
||||||
pytest-md = "^0.2.0"
|
"ruff",
|
||||||
|
"pytest-md",
|
||||||
[build-system]
|
]
|
||||||
requires = ["poetry-core>=1.0.0"]
|
|
||||||
build-backend = "poetry.core.masonry.api"
|
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
|
plugins = ["pydantic.mypy"]
|
||||||
|
|
||||||
[[tool.mypy.overrides]]
|
[[tool.mypy.overrides]]
|
||||||
module = [
|
module = [
|
||||||
"lnbits.*",
|
"secp256k1.*",
|
||||||
"loguru.*",
|
|
||||||
"fastapi.*",
|
|
||||||
"pydantic.*",
|
|
||||||
]
|
]
|
||||||
ignore_missing_imports = "True"
|
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]
|
[tool.pytest.ini_options]
|
||||||
log_cli = false
|
log_cli = false
|
||||||
testpaths = [
|
testpaths = [
|
||||||
|
|
|
||||||
|
|
@ -1,87 +1,87 @@
|
||||||
window.app = Vue.createApp({
|
window.app = Vue.createApp({
|
||||||
el: "#vue",
|
el: '#vue',
|
||||||
mixins: [windowMixin],
|
mixins: [windowMixin],
|
||||||
delimiters: ["${", "}"],
|
delimiters: ['${', '}'],
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
entries: [],
|
entries: [],
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
name: "key",
|
name: 'key',
|
||||||
required: true,
|
required: true,
|
||||||
label: "Key",
|
label: 'Key',
|
||||||
align: "left",
|
align: 'left',
|
||||||
field: (row) => row.key,
|
field: row => row.key,
|
||||||
sortable: true,
|
sortable: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "value",
|
name: 'value',
|
||||||
required: true,
|
required: true,
|
||||||
label: "Value",
|
label: 'Value',
|
||||||
align: "left",
|
align: 'left',
|
||||||
field: (row) => row.value,
|
field: row => row.value,
|
||||||
sortable: true,
|
sortable: true
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
fetchConfig() {
|
fetchConfig() {
|
||||||
this.entries = [];
|
this.entries = []
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request(
|
.request(
|
||||||
"GET",
|
'GET',
|
||||||
"/nwcprovider/api/v1/config",
|
'/nwcprovider/api/v1/config',
|
||||||
this.g.user.wallets[0].adminkey,
|
this.g.user.wallets[0].adminkey
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then(response => {
|
||||||
const newEntries = [];
|
const newEntries = []
|
||||||
for (const [key, value] of Object.entries(response.data)) {
|
for (const [key, value] of Object.entries(response.data)) {
|
||||||
newEntries.push({
|
newEntries.push({
|
||||||
key: key,
|
key: key,
|
||||||
value: value,
|
value: value
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
this.entries.length = 0;
|
this.entries.length = 0
|
||||||
this.entries.push(...newEntries);
|
this.entries.push(...newEntries)
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
console.error("Error fetching config:", error);
|
console.error('Error fetching config:', error)
|
||||||
});
|
})
|
||||||
},
|
},
|
||||||
async saveConfig() {
|
async saveConfig() {
|
||||||
const data = {};
|
const data = {}
|
||||||
for (const entry of this.entries) {
|
for (const entry of this.entries) {
|
||||||
data[entry.key] = entry.value;
|
data[entry.key] = entry.value
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const response = await LNbits.api.request(
|
const response = await LNbits.api.request(
|
||||||
"POST",
|
'POST',
|
||||||
"/nwcprovider/api/v1/config",
|
'/nwcprovider/api/v1/config',
|
||||||
this.g.user.wallets[0].adminkey,
|
this.g.user.wallets[0].adminkey,
|
||||||
data,
|
data
|
||||||
);
|
)
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
type: "positive",
|
type: 'positive',
|
||||||
message: "Config saved!",
|
message: 'Config saved!'
|
||||||
});
|
})
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
type: "warning",
|
type: 'warning',
|
||||||
message:
|
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) {
|
} catch (error) {
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
type: "negative",
|
type: 'negative',
|
||||||
message: "Error saving config: " + String(error),
|
message: 'Error saving config: ' + String(error)
|
||||||
});
|
})
|
||||||
console.error("Error saving config:", error);
|
console.error('Error saving config:', error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
created: function () {
|
created: function () {
|
||||||
this.fetchConfig();
|
this.fetchConfig()
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
window.app = Vue.createApp({
|
window.app = Vue.createApp({
|
||||||
el: "#vue",
|
el: '#vue',
|
||||||
mixins: [windowMixin],
|
mixins: [windowMixin],
|
||||||
delimiters: ["${", "}"],
|
delimiters: ['${', '}'],
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
selectedWallet: null,
|
selectedWallet: null,
|
||||||
|
|
@ -10,244 +10,244 @@ window.app = Vue.createApp({
|
||||||
nwcsTable: {
|
nwcsTable: {
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
name: "description",
|
name: 'description',
|
||||||
align: "left",
|
align: 'left',
|
||||||
label: "Description",
|
label: 'Description',
|
||||||
field: "description",
|
field: 'description'
|
||||||
},
|
},
|
||||||
{ name: "status", align: "left", label: "Status", field: "status" },
|
{name: 'status', align: 'left', label: 'Status', field: 'status'},
|
||||||
{
|
{
|
||||||
name: "last_used",
|
name: 'last_used',
|
||||||
align: "left",
|
align: 'left',
|
||||||
label: "Last used",
|
label: 'Last used',
|
||||||
field: "last_used",
|
field: 'last_used'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "created_at",
|
name: 'created_at',
|
||||||
align: "left",
|
align: 'left',
|
||||||
label: "Created",
|
label: 'Created',
|
||||||
field: "created_at",
|
field: 'created_at'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "expires_at",
|
name: 'expires_at',
|
||||||
align: "left",
|
align: 'left',
|
||||||
label: "Expires",
|
label: 'Expires',
|
||||||
field: "expires_at",
|
field: 'expires_at'
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
pagination: {
|
pagination: {
|
||||||
rowsPerPage: 10,
|
rowsPerPage: 10
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
connectDialog: {
|
connectDialog: {
|
||||||
show: false,
|
show: false,
|
||||||
data: {},
|
data: {}
|
||||||
},
|
},
|
||||||
pairingDialog: {
|
pairingDialog: {
|
||||||
show: false,
|
show: false,
|
||||||
data: {
|
data: {
|
||||||
pairingUrl: "",
|
pairingUrl: ''
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
pairingQrDialog: {
|
pairingQrDialog: {
|
||||||
show: false,
|
show: false,
|
||||||
data: {
|
data: {
|
||||||
pairingUrl: "",
|
pairingUrl: ''
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
connectionInfoDialog: {
|
connectionInfoDialog: {
|
||||||
show: false,
|
show: false,
|
||||||
data: {},
|
data: {}
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
showConnectDialog() {
|
showConnectDialog() {
|
||||||
const wallet = this.getWallet();
|
const wallet = this.getWallet()
|
||||||
if (!wallet) {
|
if (!wallet) {
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
type: "negative",
|
type: 'negative',
|
||||||
message: "Please select a wallet first",
|
message: 'Please select a wallet first'
|
||||||
});
|
})
|
||||||
return;
|
return
|
||||||
} else {
|
} else {
|
||||||
this.connectDialog.show = true;
|
this.connectDialog.show = true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
openConnectionInfoDialog(data) {
|
openConnectionInfoDialog(data) {
|
||||||
this.connectionInfoDialog.data = data;
|
this.connectionInfoDialog.data = data
|
||||||
this.connectionInfoDialog.show = true;
|
this.connectionInfoDialog.show = true
|
||||||
},
|
},
|
||||||
closeConnectionInfoDialog() {
|
closeConnectionInfoDialog() {
|
||||||
this.connectionInfoDialog.show = false;
|
this.connectionInfoDialog.show = false
|
||||||
},
|
},
|
||||||
openPairingUrl() {
|
openPairingUrl() {
|
||||||
const url = this.pairingDialog.data.pairingUrl;
|
const url = this.pairingDialog.data.pairingUrl
|
||||||
if (url) window.open(url, "_blank");
|
if (url) window.open(url, '_blank')
|
||||||
},
|
},
|
||||||
go(url) {
|
go(url) {
|
||||||
window.open(url, "_blank");
|
window.open(url, '_blank')
|
||||||
},
|
},
|
||||||
async copyPairingUrl() {
|
async copyPairingUrl() {
|
||||||
const url = this.pairingDialog.data.pairingUrl;
|
const url = this.pairingDialog.data.pairingUrl
|
||||||
if (url) {
|
if (url) {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(url);
|
await navigator.clipboard.writeText(url)
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
type: "positive",
|
type: 'positive',
|
||||||
message: "URL copied to clipboard",
|
message: 'URL copied to clipboard'
|
||||||
});
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
type: "negative",
|
type: 'negative',
|
||||||
message: "Failed to copy URL.",
|
message: 'Failed to copy URL.'
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
showPairingQR() {
|
showPairingQR() {
|
||||||
this.pairingQrDialog.data.pairingUrl = this.pairingDialog.data.pairingUrl;
|
this.pairingQrDialog.data.pairingUrl = this.pairingDialog.data.pairingUrl
|
||||||
this.pairingQrDialog.show = true;
|
this.pairingQrDialog.show = true
|
||||||
},
|
},
|
||||||
closePairingQrDialog() {
|
closePairingQrDialog() {
|
||||||
this.pairingQrDialog.show = false;
|
this.pairingQrDialog.show = false
|
||||||
},
|
},
|
||||||
loadConnectDialogData() {
|
loadConnectDialogData() {
|
||||||
this.connectDialog.data = {
|
this.connectDialog.data = {
|
||||||
description: "",
|
description: '',
|
||||||
expires_at: Date.now() + 1000 * 60 * 60 * 24 * 7,
|
expires_at: Date.now() + 1000 * 60 * 60 * 24 * 7,
|
||||||
neverExpires: true,
|
neverExpires: true,
|
||||||
permissions: [],
|
permissions: [],
|
||||||
budgets: [],
|
budgets: []
|
||||||
};
|
}
|
||||||
for (const permission of this.nodePermissions) {
|
for (const permission of this.nodePermissions) {
|
||||||
this.connectDialog.data.permissions.push({
|
this.connectDialog.data.permissions.push({
|
||||||
key: permission.key,
|
key: permission.key,
|
||||||
name: permission.name,
|
name: permission.name,
|
||||||
value: permission.value,
|
value: permission.value
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
deleteBudget(index) {
|
deleteBudget(index) {
|
||||||
this.connectDialog.data.budgets.splice(index, 1);
|
this.connectDialog.data.budgets.splice(index, 1)
|
||||||
},
|
},
|
||||||
addBudget() {
|
addBudget() {
|
||||||
this.connectDialog.data.budgets.push({
|
this.connectDialog.data.budgets.push({
|
||||||
budget_sats: 1000,
|
budget_sats: 1000,
|
||||||
used_budget_sats: 0,
|
used_budget_sats: 0,
|
||||||
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,
|
||||||
expiration: "never",
|
expiration: 'never'
|
||||||
});
|
})
|
||||||
},
|
},
|
||||||
closeConnectDialog() {
|
closeConnectDialog() {
|
||||||
this.connectDialog.show = false;
|
this.connectDialog.show = false
|
||||||
this.loadConnectDialogData();
|
this.loadConnectDialogData()
|
||||||
},
|
},
|
||||||
getWallet: function () {
|
getWallet: function () {
|
||||||
let wallet = undefined;
|
let wallet = undefined
|
||||||
for (let i = 0; i < this.g.user.wallets.length; i++) {
|
for (let i = 0; i < this.g.user.wallets.length; i++) {
|
||||||
if (this.g.user.wallets[i].id == this.selectedWallet) {
|
if (this.g.user.wallets[i].id == this.selectedWallet) {
|
||||||
wallet = this.g.user.wallets[i];
|
wallet = this.g.user.wallets[i]
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return wallet;
|
return wallet
|
||||||
},
|
},
|
||||||
async generateKeyPair() {
|
async generateKeyPair() {
|
||||||
while (!window.NobleSecp256k1) {
|
while (!window.NobleSecp256k1) {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1));
|
await new Promise(resolve => setTimeout(resolve, 1))
|
||||||
}
|
}
|
||||||
const privKeyBytes = window.NobleSecp256k1.utils.randomPrivateKey();
|
const privKeyBytes = window.NobleSecp256k1.utils.randomPrivateKey()
|
||||||
const pubKeyBytes = window.NobleSecp256k1.getPublicKey(privKeyBytes);
|
const pubKeyBytes = window.NobleSecp256k1.getPublicKey(privKeyBytes)
|
||||||
const out = {
|
const out = {
|
||||||
privKeyBytes: privKeyBytes,
|
privKeyBytes: privKeyBytes,
|
||||||
pubKeyBytes: pubKeyBytes,
|
pubKeyBytes: pubKeyBytes,
|
||||||
privKey: window.NobleSecp256k1.etc.bytesToHex(privKeyBytes),
|
privKey: window.NobleSecp256k1.etc.bytesToHex(privKeyBytes),
|
||||||
pubKey: window.NobleSecp256k1.etc.bytesToHex(pubKeyBytes.slice(1)),
|
pubKey: window.NobleSecp256k1.etc.bytesToHex(pubKeyBytes.slice(1))
|
||||||
};
|
}
|
||||||
return out;
|
return out
|
||||||
},
|
},
|
||||||
deleteNWC: async function (pubkey) {
|
deleteNWC: async function (pubkey) {
|
||||||
Quasar.Dialog.create({
|
Quasar.Dialog.create({
|
||||||
title: "Confirm Deletion",
|
title: 'Confirm Deletion',
|
||||||
message: "Are you sure you want to delete this connection?",
|
message: 'Are you sure you want to delete this connection?',
|
||||||
cancel: true,
|
cancel: true,
|
||||||
persistent: true,
|
persistent: true
|
||||||
})
|
})
|
||||||
.onOk(async () => {
|
.onOk(async () => {
|
||||||
try {
|
try {
|
||||||
const wallet = this.getWallet();
|
const wallet = this.getWallet()
|
||||||
const response = await LNbits.api.request(
|
const response = await LNbits.api.request(
|
||||||
"DELETE",
|
'DELETE',
|
||||||
`/nwcprovider/api/v1/nwc/${pubkey}`,
|
`/nwcprovider/api/v1/nwc/${pubkey}`,
|
||||||
wallet.adminkey,
|
wallet.adminkey
|
||||||
);
|
)
|
||||||
this.loadNwcs();
|
this.loadNwcs()
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
type: "positive",
|
type: 'positive',
|
||||||
message: "Deleted successfully",
|
message: 'Deleted successfully'
|
||||||
});
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
LNbits.utils.notifyApiError(error);
|
LNbits.utils.notifyApiError(error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.onCancel(() => {
|
.onCancel(() => {
|
||||||
// User canceled the operation
|
// User canceled the operation
|
||||||
});
|
})
|
||||||
},
|
},
|
||||||
loadNwcs: async function () {
|
loadNwcs: async function () {
|
||||||
const wallet = this.getWallet();
|
const wallet = this.getWallet()
|
||||||
if (!wallet) {
|
if (!wallet) {
|
||||||
this.nwcs = [];
|
this.nwcs = []
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const response = await LNbits.api.request(
|
const response = await LNbits.api.request(
|
||||||
"GET",
|
'GET',
|
||||||
"/nwcprovider/api/v1/nwc?include_expired=true&calculate_spent_budget=true",
|
'/nwcprovider/api/v1/nwc?include_expired=true&calculate_spent_budget=true',
|
||||||
wallet.adminkey,
|
wallet.adminkey
|
||||||
);
|
)
|
||||||
this.nwcs = response.data;
|
this.nwcs = response.data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.nwcs = [];
|
this.nwcs = []
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const response = await LNbits.api.request(
|
const response = await LNbits.api.request(
|
||||||
"GET",
|
'GET',
|
||||||
"/nwcprovider/api/v1/permissions",
|
'/nwcprovider/api/v1/permissions',
|
||||||
wallet.adminkey,
|
wallet.adminkey
|
||||||
);
|
)
|
||||||
const permissions = [];
|
const permissions = []
|
||||||
for (const [key, value] of Object.entries(response.data)) {
|
for (const [key, value] of Object.entries(response.data)) {
|
||||||
permissions.push({
|
permissions.push({
|
||||||
key: key,
|
key: key,
|
||||||
name: value.name,
|
name: value.name,
|
||||||
value: value.default,
|
value: value.default
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
this.nodePermissions = permissions;
|
this.nodePermissions = permissions
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Lnbits.utils.notifyApiError(error);
|
Lnbits.utils.notifyApiError(error)
|
||||||
}
|
}
|
||||||
this.loadConnectDialogData();
|
this.loadConnectDialogData()
|
||||||
const newTableEntries = [];
|
const newTableEntries = []
|
||||||
for (const nwc of this.nwcs) {
|
for (const nwc of this.nwcs) {
|
||||||
const t = Quasar.date.formatDate(
|
const t = Quasar.date.formatDate(
|
||||||
new Date(nwc.data.created_at * 1000),
|
new Date(nwc.data.created_at * 1000),
|
||||||
"YYYY-MM-DD HH:mm",
|
'YYYY-MM-DD HH:mm'
|
||||||
);
|
)
|
||||||
const e =
|
const e =
|
||||||
nwc.data.expires_at > 0
|
nwc.data.expires_at > 0
|
||||||
? Quasar.date.formatDate(
|
? Quasar.date.formatDate(
|
||||||
new Date(nwc.data.expires_at * 1000),
|
new Date(nwc.data.expires_at * 1000),
|
||||||
"YYYY-MM-DD HH:mm",
|
'YYYY-MM-DD HH:mm'
|
||||||
)
|
)
|
||||||
: "Never";
|
: 'Never'
|
||||||
const l = Quasar.date.formatDate(
|
const l = Quasar.date.formatDate(
|
||||||
new Date(nwc.data.last_used * 1000),
|
new Date(nwc.data.last_used * 1000),
|
||||||
"YYYY-MM-DD HH:mm",
|
'YYYY-MM-DD HH:mm'
|
||||||
);
|
)
|
||||||
const nwcTableEntry = {
|
const nwcTableEntry = {
|
||||||
description: nwc.data.description,
|
description: nwc.data.description,
|
||||||
created_at: t,
|
created_at: t,
|
||||||
|
|
@ -256,131 +256,130 @@ window.app = Vue.createApp({
|
||||||
pubkey: nwc.data.pubkey,
|
pubkey: nwc.data.pubkey,
|
||||||
permissions: nwc.data.permissions,
|
permissions: nwc.data.permissions,
|
||||||
budgets: [],
|
budgets: [],
|
||||||
status: "Active",
|
status: 'Active'
|
||||||
};
|
}
|
||||||
if (
|
if (
|
||||||
nwc.data.expires_at > 0 &&
|
nwc.data.expires_at > 0 &&
|
||||||
nwc.data.expires_at < new Date().getTime() / 1000
|
nwc.data.expires_at < new Date().getTime() / 1000
|
||||||
) {
|
) {
|
||||||
nwcTableEntry.status = "Expired";
|
nwcTableEntry.status = 'Expired'
|
||||||
}
|
}
|
||||||
for (const budget of nwc.budgets) {
|
for (const budget of nwc.budgets) {
|
||||||
const createdAt = Quasar.date.formatDate(
|
const createdAt = Quasar.date.formatDate(
|
||||||
new Date(budget.created_at * 1000),
|
new Date(budget.created_at * 1000),
|
||||||
"YYYY-MM-DD HH:mm",
|
'YYYY-MM-DD HH:mm'
|
||||||
);
|
)
|
||||||
let refreshWindow = budget.refresh_window;
|
let refreshWindow = budget.refresh_window
|
||||||
if (refreshWindow <= 0) {
|
if (refreshWindow <= 0) {
|
||||||
refreshWindow = "Never";
|
refreshWindow = 'Never'
|
||||||
} else if (refreshWindow == 60 * 60 * 24) {
|
} else if (refreshWindow == 60 * 60 * 24) {
|
||||||
refreshWindow = "Daily";
|
refreshWindow = 'Daily'
|
||||||
} else if (refreshWindow == 60 * 60 * 24 * 7) {
|
} else if (refreshWindow == 60 * 60 * 24 * 7) {
|
||||||
refreshWindow = "Weekly";
|
refreshWindow = 'Weekly'
|
||||||
} else if (refreshWindow == 60 * 60 * 24 * 30) {
|
} else if (refreshWindow == 60 * 60 * 24 * 30) {
|
||||||
refreshWindow = "Monthly";
|
refreshWindow = 'Monthly'
|
||||||
} else if (refreshWindow == 60 * 60 * 24 * 365) {
|
} else if (refreshWindow == 60 * 60 * 24 * 365) {
|
||||||
refreshWindow = "Yearly";
|
refreshWindow = 'Yearly'
|
||||||
}
|
}
|
||||||
nwcTableEntry.budgets.push({
|
nwcTableEntry.budgets.push({
|
||||||
budget_sats: budget.budget_msats / 1000,
|
budget_sats: budget.budget_msats / 1000,
|
||||||
used_budget_sats: budget.used_budget_msats / 1000,
|
used_budget_sats: budget.used_budget_msats / 1000,
|
||||||
created_at: createdAt,
|
created_at: createdAt,
|
||||||
refresh_window: refreshWindow,
|
refresh_window: refreshWindow
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
newTableEntries.push(nwcTableEntry);
|
newTableEntries.push(nwcTableEntry)
|
||||||
}
|
}
|
||||||
this.nwcEntries = newTableEntries;
|
this.nwcEntries = newTableEntries
|
||||||
},
|
},
|
||||||
closePairingDialog() {
|
closePairingDialog() {
|
||||||
this.pairingDialog.show = false;
|
this.pairingDialog.show = false
|
||||||
},
|
},
|
||||||
async showPairingDialog(secret) {
|
async showPairingDialog(secret) {
|
||||||
let response = await LNbits.api.request(
|
let response = await LNbits.api.request(
|
||||||
"GET",
|
'GET',
|
||||||
"/nwcprovider/api/v1/pairing/{SECRET}",
|
'/nwcprovider/api/v1/pairing/{SECRET}'
|
||||||
);
|
)
|
||||||
response = response.data;
|
response = response.data
|
||||||
response = response.replace("{SECRET}", secret);
|
response = response.replace('{SECRET}', secret)
|
||||||
this.pairingDialog.data.pairingUrl = response;
|
this.pairingDialog.data.pairingUrl = response
|
||||||
this.pairingDialog.show = true;
|
this.pairingDialog.show = true
|
||||||
},
|
},
|
||||||
async confirmConnectDialog() {
|
async confirmConnectDialog() {
|
||||||
const keyPair = await this.generateKeyPair();
|
const keyPair = await this.generateKeyPair()
|
||||||
// timestamp
|
// timestamp
|
||||||
let expires_at = 0;
|
let expires_at = 0
|
||||||
if (!this.connectDialog.data.neverExpires) {
|
if (!this.connectDialog.data.neverExpires) {
|
||||||
expires_at =
|
expires_at =
|
||||||
new Date(this.connectDialog.data.expires_at).getTime() / 1000;
|
new Date(this.connectDialog.data.expires_at).getTime() / 1000
|
||||||
}
|
}
|
||||||
const data = {
|
const data = {
|
||||||
permissions: [],
|
permissions: [],
|
||||||
description: this.connectDialog.data.description,
|
description: this.connectDialog.data.description,
|
||||||
expires_at: expires_at,
|
expires_at: expires_at,
|
||||||
budgets: [],
|
budgets: []
|
||||||
};
|
}
|
||||||
for (const permission of this.connectDialog.data.permissions) {
|
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) {
|
for (const budget of this.connectDialog.data.budgets) {
|
||||||
const budget_msats = budget.budget_sats * 1000;
|
const budget_msats = budget.budget_sats * 1000
|
||||||
let refresh_window = 0;
|
let refresh_window = 0
|
||||||
switch (budget.expiry) {
|
switch (budget.expiry) {
|
||||||
case "Daily":
|
case 'Daily':
|
||||||
refresh_window = 60 * 60 * 24;
|
refresh_window = 60 * 60 * 24
|
||||||
break;
|
break
|
||||||
case "Weekly":
|
case 'Weekly':
|
||||||
refresh_window = 60 * 60 * 24 * 7;
|
refresh_window = 60 * 60 * 24 * 7
|
||||||
break;
|
break
|
||||||
case "Monthly":
|
case 'Monthly':
|
||||||
refresh_window = 60 * 60 * 24 * 30;
|
refresh_window = 60 * 60 * 24 * 30
|
||||||
break;
|
break
|
||||||
case "Yearly":
|
case 'Yearly':
|
||||||
refresh_window = 60 * 60 * 24 * 365;
|
refresh_window = 60 * 60 * 24 * 365
|
||||||
break;
|
break
|
||||||
case "Never":
|
case 'Never':
|
||||||
refresh_window = 0;
|
refresh_window = 0
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
data.budgets.push({
|
data.budgets.push({
|
||||||
budget_msats: budget_msats,
|
budget_msats: budget_msats,
|
||||||
refresh_window: refresh_window,
|
refresh_window: refresh_window,
|
||||||
created_at:
|
created_at: new Date(new Date().setHours(0, 0, 0, 0)).getTime() / 1000
|
||||||
new Date(new Date().setHours(0, 0, 0, 0)).getTime() / 1000,
|
})
|
||||||
});
|
|
||||||
}
|
}
|
||||||
const wallet = this.getWallet();
|
const wallet = this.getWallet()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await LNbits.api.request(
|
const response = await LNbits.api.request(
|
||||||
"PUT",
|
'PUT',
|
||||||
"/nwcprovider/api/v1/nwc/" + keyPair.pubKey,
|
'/nwcprovider/api/v1/nwc/' + keyPair.pubKey,
|
||||||
wallet.adminkey,
|
wallet.adminkey,
|
||||||
data,
|
data
|
||||||
);
|
)
|
||||||
this.closeConnectDialog();
|
this.closeConnectDialog()
|
||||||
if (
|
if (
|
||||||
!response.data ||
|
!response.data ||
|
||||||
!response.data.data ||
|
!response.data.data ||
|
||||||
!response.data.data.pubkey
|
!response.data.data.pubkey
|
||||||
) {
|
) {
|
||||||
LNbits.utils.notifyApiError("Error creating nwc pairing");
|
LNbits.utils.notifyApiError('Error creating nwc pairing')
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
this.showPairingDialog(keyPair.privKey);
|
this.showPairingDialog(keyPair.privKey)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
LNbits.utils.notifyApiError(error);
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
this.loadNwcs()
|
||||||
}
|
}
|
||||||
this.loadNwcs();
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
created: function () {
|
created: function () {
|
||||||
this.loadNwcs();
|
this.loadNwcs()
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
selectedWallet(newValue, oldValue) {
|
selectedWallet(newValue, oldValue) {
|
||||||
this.loadNwcs();
|
this.loadNwcs()
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
|
|
|
||||||
461
static/js/noble-secp256k1.min.js
vendored
461
static/js/noble-secp256k1.min.js
vendored
|
|
@ -6,60 +6,60 @@ const B256 = 2n ** 256n,
|
||||||
Gy = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8n,
|
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,
|
fLen = 32,
|
||||||
crv = (t) => mod(mod(t * t) * t + CURVE.b),
|
crv = t => mod(mod(t * t) * t + CURVE.b),
|
||||||
err = (t = "") => {
|
err = (t = '') => {
|
||||||
throw new Error(t);
|
throw new Error(t)
|
||||||
},
|
},
|
||||||
big = (t) => "bigint" == typeof t,
|
big = t => 'bigint' == typeof t,
|
||||||
str = (t) => "string" == typeof t,
|
str = t => 'string' == typeof t,
|
||||||
fe = (t) => big(t) && 0n < t && t < P,
|
fe = t => big(t) && 0n < t && t < P,
|
||||||
ge = (t) => big(t) && 0n < t && t < N,
|
ge = t => big(t) && 0n < t && t < N,
|
||||||
isu8 = (t) =>
|
isu8 = t =>
|
||||||
t instanceof Uint8Array ||
|
t instanceof Uint8Array ||
|
||||||
(null != t && "object" == typeof t && "Uint8Array" === t.constructor.name),
|
(null != t && 'object' == typeof t && 'Uint8Array' === t.constructor.name),
|
||||||
au8 = (t, n) =>
|
au8 = (t, n) =>
|
||||||
!isu8(t) || ("number" == typeof n && n > 0 && t.length !== n)
|
!isu8(t) || ('number' == typeof n && n > 0 && t.length !== n)
|
||||||
? err("Uint8Array expected")
|
? err('Uint8Array expected')
|
||||||
: t,
|
: t,
|
||||||
u8n = (t) => new Uint8Array(t),
|
u8n = t => new Uint8Array(t),
|
||||||
toU8 = (t, n) => au8(str(t) ? h2b(t) : u8n(au8(t)), n),
|
toU8 = (t, n) => au8(str(t) ? h2b(t) : u8n(au8(t)), n),
|
||||||
mod = (t, n = P) => {
|
mod = (t, n = P) => {
|
||||||
let e = t % n;
|
let e = t % n
|
||||||
return e >= 0n ? e : n + e;
|
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 {
|
class Point {
|
||||||
constructor(t, n, e) {
|
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) {
|
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) {
|
static fromHex(t) {
|
||||||
let n;
|
let n
|
||||||
const e = (t = toU8(t))[0],
|
const e = (t = toU8(t))[0],
|
||||||
r = t.subarray(1),
|
r = t.subarray(1),
|
||||||
o = slcNum(r, 0, 32),
|
o = slcNum(r, 0, 32),
|
||||||
i = t.length;
|
i = t.length
|
||||||
if (33 === i && [2, 3].includes(e)) {
|
if (33 === i && [2, 3].includes(e)) {
|
||||||
fe(o) || err("Point hex invalid: x not FE");
|
fe(o) || err('Point hex invalid: x not FE')
|
||||||
let t = sqrt(crv(o));
|
let t = sqrt(crv(o))
|
||||||
!(1 & ~e) !== (1n === (1n & t)) && (t = mod(-t)),
|
;(!(1 & ~e) !== (1n === (1n & t)) && (t = mod(-t)),
|
||||||
(n = new Point(o, t, 1n));
|
(n = new Point(o, t, 1n)))
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
65 === i && 4 === e && (n = new Point(o, slcNum(r, 32, 64), 1n)),
|
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) {
|
static fromPrivateKey(t) {
|
||||||
return G.mul(toPriv(t));
|
return G.mul(toPriv(t))
|
||||||
}
|
}
|
||||||
get x() {
|
get x() {
|
||||||
return this.aff().x;
|
return this.aff().x
|
||||||
}
|
}
|
||||||
get y() {
|
get y() {
|
||||||
return this.aff().y;
|
return this.aff().y
|
||||||
}
|
}
|
||||||
equals(t) {
|
equals(t) {
|
||||||
const {px: n, py: e, pz: r} = this,
|
const {px: n, py: e, pz: r} = this,
|
||||||
|
|
@ -67,30 +67,30 @@ class Point {
|
||||||
a = mod(n * s),
|
a = mod(n * s),
|
||||||
c = mod(o * r),
|
c = mod(o * r),
|
||||||
d = mod(e * s),
|
d = mod(e * s),
|
||||||
u = mod(i * r);
|
u = mod(i * r)
|
||||||
return a === c && d === u;
|
return a === c && d === u
|
||||||
}
|
}
|
||||||
negate() {
|
negate() {
|
||||||
return new Point(this.px, mod(-this.py), this.pz);
|
return new Point(this.px, mod(-this.py), this.pz)
|
||||||
}
|
}
|
||||||
double() {
|
double() {
|
||||||
return this.add(this);
|
return this.add(this)
|
||||||
}
|
}
|
||||||
add(t) {
|
add(t) {
|
||||||
const {px: n, py: e, pz: r} = this,
|
const {px: n, py: e, pz: r} = this,
|
||||||
{px: o, py: i, pz: s} = isPoint(t),
|
{px: o, py: i, pz: s} = isPoint(t),
|
||||||
{ a, b: c } = CURVE;
|
{a, b: c} = CURVE
|
||||||
let d = 0n,
|
let d = 0n,
|
||||||
u = 0n,
|
u = 0n,
|
||||||
m = 0n;
|
m = 0n
|
||||||
const h = mod(3n * c);
|
const h = mod(3n * c)
|
||||||
let l = mod(n * o),
|
let l = mod(n * o),
|
||||||
y = mod(e * i),
|
y = mod(e * i),
|
||||||
f = mod(r * s),
|
f = mod(r * s),
|
||||||
p = mod(n + e),
|
p = mod(n + e),
|
||||||
b = mod(o + i);
|
b = mod(o + i)
|
||||||
(p = mod(p * b)), (b = mod(l + y)), (p = mod(p - b)), (b = mod(n + r));
|
;((p = mod(p * b)), (b = mod(l + y)), (p = mod(p - b)), (b = mod(n + r)))
|
||||||
let g = mod(o + s);
|
let g = mod(o + s)
|
||||||
return (
|
return (
|
||||||
(b = mod(b * g)),
|
(b = mod(b * g)),
|
||||||
(g = mod(l + f)),
|
(g = mod(l + f)),
|
||||||
|
|
@ -123,300 +123,301 @@ class Point {
|
||||||
(m = mod(g * m)),
|
(m = mod(g * m)),
|
||||||
(m = mod(m + l)),
|
(m = mod(m + l)),
|
||||||
new Point(d, u, m)
|
new Point(d, u, m)
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
mul(t, n = !0) {
|
mul(t, n = !0) {
|
||||||
if (!n && 0n === t) return I;
|
if (!n && 0n === t) return I
|
||||||
if ((ge(t) || err("invalid scalar"), this.equals(G))) return wNAF(t).p;
|
if ((ge(t) || err('invalid scalar'), this.equals(G))) return wNAF(t).p
|
||||||
let e = I,
|
let e = I,
|
||||||
r = G;
|
r = G
|
||||||
for (let o = this; t > 0n; o = o.double(), t >>= 1n)
|
for (let o = this; t > 0n; o = o.double(), t >>= 1n)
|
||||||
1n & t ? (e = e.add(o)) : n && (r = r.add(o));
|
1n & t ? (e = e.add(o)) : n && (r = r.add(o))
|
||||||
return e;
|
return e
|
||||||
}
|
}
|
||||||
mulAddQUns(t, n, 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() {
|
toAffine() {
|
||||||
const { px: t, py: n, pz: e } = this;
|
const {px: t, py: n, pz: e} = this
|
||||||
if (this.equals(I)) return { x: 0n, y: 0n };
|
if (this.equals(I)) return {x: 0n, y: 0n}
|
||||||
if (1n === e) return { x: t, y: n };
|
if (1n === e) return {x: t, y: n}
|
||||||
const r = inv(e);
|
const r = inv(e)
|
||||||
return (
|
return (
|
||||||
1n !== mod(e * r) && err("invalid inverse"),
|
1n !== mod(e * r) && err('invalid inverse'),
|
||||||
{x: mod(t * r), y: mod(n * r)}
|
{x: mod(t * r), y: mod(n * r)}
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
assertValidity() {
|
assertValidity() {
|
||||||
const { x: t, y: n } = this.aff();
|
const {x: t, y: n} = this.aff()
|
||||||
return (
|
return (
|
||||||
(fe(t) && fe(n)) || err("Point invalid: x or y"),
|
(fe(t) && fe(n)) || err('Point invalid: x or y'),
|
||||||
mod(n * n) === crv(t) ? this : err("Point invalid: not on curve")
|
mod(n * n) === crv(t) ? this : err('Point invalid: not on curve')
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
multiply(t) {
|
multiply(t) {
|
||||||
return this.mul(t);
|
return this.mul(t)
|
||||||
}
|
}
|
||||||
aff() {
|
aff() {
|
||||||
return this.toAffine();
|
return this.toAffine()
|
||||||
}
|
}
|
||||||
ok() {
|
ok() {
|
||||||
return this.assertValidity();
|
return this.assertValidity()
|
||||||
}
|
}
|
||||||
toHex(t = !0) {
|
toHex(t = !0) {
|
||||||
const { x: n, y: e } = this.aff();
|
const {x: n, y: e} = this.aff()
|
||||||
return (
|
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) {
|
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));
|
;((Point.BASE = new Point(Gx, Gy, 1n)), (Point.ZERO = new Point(0n, 1n, 0n)))
|
||||||
const {BASE: G, ZERO: I} = Point,
|
const {BASE: G, ZERO: I} = Point,
|
||||||
padh = (t, n) => t.toString(16).padStart(n, "0"),
|
padh = (t, n) => t.toString(16).padStart(n, '0'),
|
||||||
b2h = (t) =>
|
b2h = t =>
|
||||||
Array.from(t)
|
Array.from(t)
|
||||||
.map((t) => padh(t, 2))
|
.map(t => padh(t, 2))
|
||||||
.join(""),
|
.join(''),
|
||||||
h2b = (t) => {
|
h2b = t => {
|
||||||
const n = t.length;
|
const n = t.length
|
||||||
(!str(t) || n % 2) && err("hex invalid 1");
|
;(!str(t) || n % 2) && err('hex invalid 1')
|
||||||
const e = u8n(n / 2);
|
const e = u8n(n / 2)
|
||||||
for (let n = 0; n < e.length; n++) {
|
for (let n = 0; n < e.length; n++) {
|
||||||
const r = 2 * n,
|
const r = 2 * n,
|
||||||
o = t.slice(r, r + 2),
|
o = t.slice(r, r + 2),
|
||||||
i = Number.parseInt(o, 16);
|
i = Number.parseInt(o, 16)
|
||||||
(Number.isNaN(i) || i < 0) && err("hex invalid 2"), (e[n] = i);
|
;((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)),
|
slcNum = (t, n, e) => b2n(t.slice(n, e)),
|
||||||
n2b = (t) =>
|
n2b = t =>
|
||||||
big(t) && t >= 0n && t < B256 ? h2b(padh(t, 64)) : err("bigint expected"),
|
big(t) && t >= 0n && t < B256 ? h2b(padh(t, 64)) : err('bigint expected'),
|
||||||
n2h = (t) => b2h(n2b(t)),
|
n2h = t => b2h(n2b(t)),
|
||||||
concatB = (...t) => {
|
concatB = (...t) => {
|
||||||
const n = u8n(t.reduce((t, n) => t + au8(n).length, 0));
|
const n = u8n(t.reduce((t, n) => t + au8(n).length, 0))
|
||||||
let e = 0;
|
let e = 0
|
||||||
return (
|
return (
|
||||||
t.forEach((t) => {
|
t.forEach(t => {
|
||||||
n.set(t, e), (e += t.length);
|
;(n.set(t, e), (e += t.length))
|
||||||
}),
|
}),
|
||||||
n
|
n
|
||||||
);
|
)
|
||||||
},
|
},
|
||||||
inv = (t, n = P) => {
|
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),
|
let e = mod(t, n),
|
||||||
r = n,
|
r = n,
|
||||||
o = 0n,
|
o = 0n,
|
||||||
i = 1n,
|
i = 1n,
|
||||||
s = 1n,
|
s = 1n,
|
||||||
a = 0n;
|
a = 0n
|
||||||
for (; 0n !== e; ) {
|
for (; 0n !== e; ) {
|
||||||
const t = r / e,
|
const t = r / e,
|
||||||
n = r % e,
|
n = r % e,
|
||||||
c = o - s * t,
|
c = o - s * t,
|
||||||
d = i - a * t;
|
d = i - a * t
|
||||||
(r = e), (e = n), (o = s), (i = a), (s = c), (a = d);
|
;((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) => {
|
sqrt = t => {
|
||||||
let n = 1n;
|
let n = 1n
|
||||||
for (let e = t, r = (P + 1n) / 4n; r > 0n; r >>= 1n)
|
for (let e = t, r = (P + 1n) / 4n; r > 0n; r >>= 1n)
|
||||||
1n & r && (n = (n * e) % P), (e = (e * e) % P);
|
(1n & r && (n = (n * e) % P), (e = (e * e) % P))
|
||||||
return mod(n * n) === t ? n : err("sqrt invalid");
|
return mod(n * n) === t ? n : err('sqrt invalid')
|
||||||
},
|
},
|
||||||
toPriv = (t) => (
|
toPriv = t => (
|
||||||
big(t) || (t = b2n(toU8(t, 32))),
|
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,
|
moreThanHalfN = t => t > N >> 1n,
|
||||||
getPublicKey = (t, n = !0) => Point.fromPrivateKey(t).toRawBytes(n);
|
getPublicKey = (t, n = !0) => Point.fromPrivateKey(t).toRawBytes(n)
|
||||||
class Signature {
|
class Signature {
|
||||||
constructor(t, n, e) {
|
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) {
|
static fromCompact(t) {
|
||||||
return (
|
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() {
|
assertValidity() {
|
||||||
return ge(this.r) && ge(this.s) ? this : err();
|
return ge(this.r) && ge(this.s) ? this : err()
|
||||||
}
|
}
|
||||||
addRecoveryBit(t) {
|
addRecoveryBit(t) {
|
||||||
return new Signature(this.r, this.s, t);
|
return new Signature(this.r, this.s, t)
|
||||||
}
|
}
|
||||||
hasHighS() {
|
hasHighS() {
|
||||||
return moreThanHalfN(this.s);
|
return moreThanHalfN(this.s)
|
||||||
}
|
}
|
||||||
normalizeS() {
|
normalizeS() {
|
||||||
return this.hasHighS()
|
return this.hasHighS()
|
||||||
? new Signature(this.r, mod(this.s, N), this.recovery)
|
? new Signature(this.r, mod(this.s, N), this.recovery)
|
||||||
: this;
|
: this
|
||||||
}
|
}
|
||||||
recoverPublicKey(t) {
|
recoverPublicKey(t) {
|
||||||
const { r: n, s: e, recovery: r } = this;
|
const {r: n, s: e, recovery: r} = this
|
||||||
[0, 1, 2, 3].includes(r) || err("recovery id invalid");
|
;[0, 1, 2, 3].includes(r) || err('recovery id invalid')
|
||||||
const o = bits2int_modN(toU8(t, 32)),
|
const o = bits2int_modN(toU8(t, 32)),
|
||||||
i = 2 === r || 3 === r ? n + N : n;
|
i = 2 === r || 3 === r ? n + N : n
|
||||||
i >= P && err("q.x invalid");
|
i >= P && err('q.x invalid')
|
||||||
const s = 1 & r ? "03" : "02",
|
const s = 1 & r ? '03' : '02',
|
||||||
a = Point.fromHex(s + n2h(i)),
|
a = Point.fromHex(s + n2h(i)),
|
||||||
c = inv(i, N),
|
c = inv(i, N),
|
||||||
d = mod(-o * c, N),
|
d = mod(-o * c, N),
|
||||||
u = mod(e * c, N);
|
u = mod(e * c, N)
|
||||||
return G.mulAddQUns(a, d, u);
|
return G.mulAddQUns(a, d, u)
|
||||||
}
|
}
|
||||||
toCompactRawBytes() {
|
toCompactRawBytes() {
|
||||||
return h2b(this.toCompactHex());
|
return h2b(this.toCompactHex())
|
||||||
}
|
}
|
||||||
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,
|
const n = 8 * t.length - 256,
|
||||||
e = b2n(t);
|
e = b2n(t)
|
||||||
return n > 0 ? e >> BigInt(n) : e;
|
return n > 0 ? e >> BigInt(n) : e
|
||||||
},
|
},
|
||||||
bits2int_modN = (t) => mod(bits2int(t), N),
|
bits2int_modN = t => mod(bits2int(t), N),
|
||||||
i2o = (t) => n2b(t),
|
i2o = t => n2b(t),
|
||||||
cr = () =>
|
cr = () =>
|
||||||
"object" == typeof globalThis && "crypto" in globalThis
|
'object' == typeof globalThis && 'crypto' in globalThis
|
||||||
? globalThis.crypto
|
? globalThis.crypto
|
||||||
: void 0;
|
: void 0
|
||||||
let _hmacSync;
|
let _hmacSync
|
||||||
const optS = {lowS: !0},
|
const optS = {lowS: !0},
|
||||||
optV = {lowS: !0},
|
optV = {lowS: !0},
|
||||||
prepSig = (t, n, e = optS) => {
|
prepSig = (t, n, e = optS) => {
|
||||||
["der", "recovered", "canonical"].some((t) => t in e) &&
|
;['der', 'recovered', 'canonical'].some(t => t in e) &&
|
||||||
err("sign() legacy options not supported");
|
err('sign() legacy options not supported')
|
||||||
let { lowS: r } = e;
|
let {lowS: r} = e
|
||||||
null == r && (r = !0);
|
null == r && (r = !0)
|
||||||
const o = bits2int_modN(toU8(t)),
|
const o = bits2int_modN(toU8(t)),
|
||||||
i = i2o(o),
|
i = i2o(o),
|
||||||
s = toPriv(n),
|
s = toPriv(n),
|
||||||
a = [i2o(s), i];
|
a = [i2o(s), i]
|
||||||
let c = e.extraEntropy;
|
let c = e.extraEntropy
|
||||||
if (c) {
|
if (c) {
|
||||||
!0 === c && (c = etc.randomBytes(32));
|
!0 === c && (c = etc.randomBytes(32))
|
||||||
const t = toU8(c);
|
const t = toU8(c)
|
||||||
32 !== t.length && err(), a.push(t);
|
;(32 !== t.length && err(), a.push(t))
|
||||||
}
|
}
|
||||||
const d = o;
|
const d = o
|
||||||
return {
|
return {
|
||||||
seed: concatB(...a),
|
seed: concatB(...a),
|
||||||
k2sig: (t) => {
|
k2sig: t => {
|
||||||
const n = bits2int(t);
|
const n = bits2int(t)
|
||||||
if (!ge(n)) return;
|
if (!ge(n)) return
|
||||||
const e = inv(n, N),
|
const e = inv(n, N),
|
||||||
o = G.mul(n).aff(),
|
o = G.mul(n).aff(),
|
||||||
i = mod(o.x, N);
|
i = mod(o.x, N)
|
||||||
if (0n === i) return;
|
if (0n === i) return
|
||||||
const a = mod(e * mod(d + mod(s * i, N), N), N);
|
const a = mod(e * mod(d + mod(s * i, N), N), N)
|
||||||
if (0n === a) return;
|
if (0n === a) return
|
||||||
let c = a,
|
let c = a,
|
||||||
u = (o.x === i ? 0 : 2) | Number(1n & o.y);
|
u = (o.x === i ? 0 : 2) | Number(1n & o.y)
|
||||||
return (
|
return (
|
||||||
r && moreThanHalfN(a) && ((c = mod(-a, N)), (u ^= 1)),
|
r && moreThanHalfN(a) && ((c = mod(-a, N)), (u ^= 1)),
|
||||||
new Signature(i, c, u)
|
new Signature(i, c, u)
|
||||||
);
|
)
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
};
|
}
|
||||||
function hmacDrbg(t) {
|
function hmacDrbg(t) {
|
||||||
let n = u8n(32),
|
let n = u8n(32),
|
||||||
e = u8n(32),
|
e = u8n(32),
|
||||||
r = 0;
|
r = 0
|
||||||
const o = () => {
|
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) {
|
if (t) {
|
||||||
const t = (...t) => etc.hmacSha256Async(e, n, ...t),
|
const t = (...t) => etc.hmacSha256Async(e, n, ...t),
|
||||||
s = async (r = u8n()) => {
|
s = async (r = u8n()) => {
|
||||||
(e = await t(u8n([0]), r)),
|
;((e = await t(u8n([0]), r)),
|
||||||
(n = await t()),
|
(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) => {
|
return async (t, n) => {
|
||||||
let e;
|
let e
|
||||||
for (o(), await s(t); !(e = n(await a())); ) await s();
|
for (o(), await s(t); !(e = n(await a())); ) await s()
|
||||||
return o(), e;
|
return (o(), e)
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const t = (...t) => {
|
const t = (...t) => {
|
||||||
const r = _hmacSync;
|
const r = _hmacSync
|
||||||
return r || err("etc.hmacSha256Sync not set"), r(e, n, ...t);
|
return (r || err('etc.hmacSha256Sync not set'), r(e, n, ...t))
|
||||||
},
|
},
|
||||||
s = (r = u8n()) => {
|
s = (r = u8n()) => {
|
||||||
(e = t(u8n([0]), r)),
|
;((e = t(u8n([0]), r)),
|
||||||
(n = t()),
|
(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) => {
|
return (t, n) => {
|
||||||
let e;
|
let e
|
||||||
for (o(), s(t); !(e = n(a())); ) s();
|
for (o(), s(t); !(e = n(a())); ) s()
|
||||||
return o(), e;
|
return (o(), e)
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const signAsync = async (t, n, e = optS) => {
|
const signAsync = async (t, n, e = optS) => {
|
||||||
const { seed: r, k2sig: o } = prepSig(t, n, e);
|
const {seed: r, k2sig: o} = prepSig(t, n, e)
|
||||||
return hmacDrbg(!0)(r, o);
|
return hmacDrbg(!0)(r, o)
|
||||||
},
|
},
|
||||||
sign = (t, n, e = optS) => {
|
sign = (t, n, e = optS) => {
|
||||||
const { seed: r, k2sig: o } = prepSig(t, n, e);
|
const {seed: r, k2sig: o} = prepSig(t, n, e)
|
||||||
return hmacDrbg(!1)(r, o);
|
return hmacDrbg(!1)(r, o)
|
||||||
},
|
},
|
||||||
verify = (t, n, e, r = optV) => {
|
verify = (t, n, e, r = optV) => {
|
||||||
let o,
|
let o,
|
||||||
i,
|
i,
|
||||||
s,
|
s,
|
||||||
{ lowS: a } = r;
|
{lowS: a} = r
|
||||||
null == a && (a = !0),
|
;(null == a && (a = !0),
|
||||||
"strict" in r && err("verify() legacy options not supported");
|
'strict' in r && err('verify() legacy options not supported'))
|
||||||
const c = t && "object" == typeof t && "r" in t;
|
const c = t && 'object' == typeof t && 'r' in t
|
||||||
c || 64 === toU8(t).length || err("signature must be 64 bytes");
|
c || 64 === toU8(t).length || err('signature must be 64 bytes')
|
||||||
try {
|
try {
|
||||||
(o = c
|
;((o = c
|
||||||
? new Signature(t.r, t.s).assertValidity()
|
? new Signature(t.r, t.s).assertValidity()
|
||||||
: Signature.fromCompact(t)),
|
: Signature.fromCompact(t)),
|
||||||
(i = bits2int_modN(toU8(n))),
|
(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) {
|
} catch (t) {
|
||||||
return !1;
|
return !1
|
||||||
}
|
}
|
||||||
if (!o) return !1;
|
if (!o) return !1
|
||||||
const { r: d, s: u } = o;
|
const {r: d, s: u} = o
|
||||||
if (a && moreThanHalfN(u)) return !1;
|
if (a && moreThanHalfN(u)) return !1
|
||||||
let m;
|
let m
|
||||||
try {
|
try {
|
||||||
const t = inv(u, N),
|
const t = inv(u, N),
|
||||||
n = mod(i * t, N),
|
n = mod(i * t, N),
|
||||||
e = mod(d * t, N);
|
e = mod(d * t, N)
|
||||||
m = G.mulAddQUns(s, n, e).aff();
|
m = G.mulAddQUns(s, n, e).aff()
|
||||||
} catch (t) {
|
} catch (t) {
|
||||||
return !1;
|
return !1
|
||||||
}
|
}
|
||||||
if (!m) return !1;
|
if (!m) return !1
|
||||||
return mod(m.x, N) === d;
|
return mod(m.x, N) === d
|
||||||
},
|
},
|
||||||
getSharedSecret = (t, n, e = !0) =>
|
getSharedSecret = (t, n, e = !0) =>
|
||||||
Point.fromHex(n).mul(toPriv(t)).toRawBytes(e),
|
Point.fromHex(n).mul(toPriv(t)).toRawBytes(e),
|
||||||
hashToPrivateKey = (t) => {
|
hashToPrivateKey = t => {
|
||||||
((t = toU8(t)).length < 40 || t.length > 1024) &&
|
;((t = toU8(t)).length < 40 || t.length > 1024) &&
|
||||||
err("expected proper params");
|
err('expected proper params')
|
||||||
const n = mod(b2n(t), N - 1n) + 1n;
|
const n = mod(b2n(t), N - 1n) + 1n
|
||||||
return n2b(n);
|
return n2b(n)
|
||||||
},
|
},
|
||||||
etc = {
|
etc = {
|
||||||
hexToBytes: h2b,
|
hexToBytes: h2b,
|
||||||
|
|
@ -428,84 +429,84 @@ const signAsync = async (t, n, e = optS) => {
|
||||||
invert: inv,
|
invert: inv,
|
||||||
hmacSha256Async: async (t, ...n) => {
|
hmacSha256Async: async (t, ...n) => {
|
||||||
const e = cr(),
|
const e = cr(),
|
||||||
r = e && e.subtle;
|
r = e && e.subtle
|
||||||
if (!r) return err("etc.hmacSha256Async not set");
|
if (!r) return err('etc.hmacSha256Async not set')
|
||||||
const o = await r.importKey(
|
const o = await r.importKey(
|
||||||
"raw",
|
'raw',
|
||||||
t,
|
t,
|
||||||
{ name: "HMAC", hash: { name: "SHA-256" } },
|
{name: 'HMAC', hash: {name: 'SHA-256'}},
|
||||||
!1,
|
!1,
|
||||||
["sign"],
|
['sign']
|
||||||
);
|
)
|
||||||
return u8n(await r.sign("HMAC", o, concatB(...n)));
|
return u8n(await r.sign('HMAC', o, concatB(...n)))
|
||||||
},
|
},
|
||||||
hmacSha256Sync: _hmacSync,
|
hmacSha256Sync: _hmacSync,
|
||||||
hashToPrivateKey,
|
hashToPrivateKey,
|
||||||
randomBytes: (t = 32) => {
|
randomBytes: (t = 32) => {
|
||||||
const n = cr();
|
const n = cr()
|
||||||
return (
|
return (
|
||||||
(n && n.getRandomValues) ||
|
(n && n.getRandomValues) ||
|
||||||
err("crypto.getRandomValues must be defined"),
|
err('crypto.getRandomValues must be defined'),
|
||||||
n.getRandomValues(u8n(t))
|
n.getRandomValues(u8n(t))
|
||||||
);
|
)
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
utils = {
|
utils = {
|
||||||
normPrivateKeyToScalar: toPriv,
|
normPrivateKeyToScalar: toPriv,
|
||||||
isValidPrivateKey: (t) => {
|
isValidPrivateKey: t => {
|
||||||
try {
|
try {
|
||||||
return !!toPriv(t);
|
return !!toPriv(t)
|
||||||
} catch (t) {
|
} catch (t) {
|
||||||
return !1;
|
return !1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
randomPrivateKey: () => hashToPrivateKey(etc.randomBytes(48)),
|
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, {
|
Object.defineProperties(etc, {
|
||||||
hmacSha256Sync: {
|
hmacSha256Sync: {
|
||||||
configurable: !1,
|
configurable: !1,
|
||||||
get: () => _hmacSync,
|
get: () => _hmacSync,
|
||||||
set(t) {
|
set(t) {
|
||||||
_hmacSync || (_hmacSync = t);
|
_hmacSync || (_hmacSync = t)
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
const W = 8,
|
const W = 8,
|
||||||
precompute = () => {
|
precompute = () => {
|
||||||
const t = [];
|
const t = []
|
||||||
let n = G,
|
let n = G,
|
||||||
e = n;
|
e = n
|
||||||
for (let r = 0; r < 33; r++) {
|
for (let r = 0; r < 33; r++) {
|
||||||
(e = n), t.push(e);
|
;((e = n), t.push(e))
|
||||||
for (let r = 1; r < 128; r++) (e = e.add(n)), t.push(e);
|
for (let r = 1; r < 128; r++) ((e = e.add(n)), t.push(e))
|
||||||
n = e.double();
|
n = e.double()
|
||||||
}
|
}
|
||||||
return t;
|
return t
|
||||||
};
|
}
|
||||||
let Gpows;
|
let Gpows
|
||||||
const wNAF = (t) => {
|
const wNAF = t => {
|
||||||
const n = Gpows || (Gpows = precompute()),
|
const n = Gpows || (Gpows = precompute()),
|
||||||
e = (t, n) => {
|
e = (t, n) => {
|
||||||
let e = n.negate();
|
let e = n.negate()
|
||||||
return t ? e : n;
|
return t ? e : n
|
||||||
};
|
}
|
||||||
let r = I,
|
let r = I,
|
||||||
o = G;
|
o = G
|
||||||
const i = BigInt(255),
|
const i = BigInt(255),
|
||||||
s = BigInt(8);
|
s = BigInt(8)
|
||||||
for (let a = 0; a < 33; a++) {
|
for (let a = 0; a < 33; a++) {
|
||||||
const c = 128 * a;
|
const c = 128 * a
|
||||||
let d = Number(t & i);
|
let d = Number(t & i)
|
||||||
(t >>= s), d > 128 && ((d -= 256), (t += 1n));
|
;((t >>= s), d > 128 && ((d -= 256), (t += 1n)))
|
||||||
const u = c,
|
const u = c,
|
||||||
m = c + Math.abs(d) - 1,
|
m = c + Math.abs(d) - 1,
|
||||||
h = a % 2 != 0,
|
h = a % 2 != 0,
|
||||||
l = d < 0;
|
l = d < 0
|
||||||
0 === d ? (o = o.add(e(h, n[u]))) : (r = r.add(e(l, n[m])));
|
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 {
|
export {
|
||||||
getPublicKey,
|
getPublicKey,
|
||||||
sign,
|
sign,
|
||||||
|
|
@ -516,5 +517,5 @@ export {
|
||||||
etc,
|
etc,
|
||||||
utils,
|
utils,
|
||||||
Point as ProjectivePoint,
|
Point as ProjectivePoint,
|
||||||
Signature,
|
Signature
|
||||||
};
|
}
|
||||||
|
|
|
||||||
60
tasks.py
60
tasks.py
|
|
@ -1,7 +1,7 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import time
|
import time
|
||||||
from math import ceil
|
from math import ceil
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any
|
||||||
|
|
||||||
from bolt11 import decode as bolt11_decode
|
from bolt11 import decode as bolt11_decode
|
||||||
from lnbits.core.crud import get_payments, get_wallet, get_wallet_payment
|
from lnbits.core.crud import get_payments, get_wallet, get_wallet_payment
|
||||||
|
|
@ -35,7 +35,7 @@ from .paranoia import (
|
||||||
from .permission import nwc_permissions
|
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
|
# check
|
||||||
if not nwc:
|
if not nwc:
|
||||||
return {
|
return {
|
||||||
|
|
@ -46,8 +46,8 @@ async def _check(nwc: Optional[NWCKey], method: str) -> Optional[Dict]:
|
||||||
allowed = False
|
allowed = False
|
||||||
permissions = nwc.get_permissions()
|
permissions = nwc.get_permissions()
|
||||||
for p in permissions:
|
for p in permissions:
|
||||||
permissions_data: Dict[str, Any] = nwc_permissions.get(p, {})
|
permissions_data: dict[str, Any] = nwc_permissions.get(p, {})
|
||||||
allowed_methods: List[str] = permissions_data.get("methods", [])
|
allowed_methods: list[str] = permissions_data.get("methods", [])
|
||||||
if method in allowed_methods:
|
if method in allowed_methods:
|
||||||
allowed = True
|
allowed = True
|
||||||
break
|
break
|
||||||
|
|
@ -64,7 +64,7 @@ async def _process_invoice(
|
||||||
pubkey: str,
|
pubkey: str,
|
||||||
invoice: str,
|
invoice: str,
|
||||||
amount_msats: int,
|
amount_msats: int,
|
||||||
description: Optional[str] = None,
|
description: str | None = None,
|
||||||
):
|
):
|
||||||
|
|
||||||
# hardening #
|
# hardening #
|
||||||
|
|
@ -80,7 +80,7 @@ async def _process_invoice(
|
||||||
payment = await pay_invoice(
|
payment = await pay_invoice(
|
||||||
wallet_id=wallet_id,
|
wallet_id=wallet_id,
|
||||||
payment_request=invoice,
|
payment_request=invoice,
|
||||||
max_sat=int(ceil(amount_msats / 1000)),
|
max_sat=ceil(amount_msats / 1000),
|
||||||
description=description or "",
|
description=description or "",
|
||||||
)
|
)
|
||||||
return payment.payment_hash
|
return payment.payment_hash
|
||||||
|
|
@ -109,7 +109,7 @@ async def _process_invoice(
|
||||||
wait_for_preimage = (
|
wait_for_preimage = (
|
||||||
True # currently required by nip 47 specs, might change in future
|
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:
|
while wait_for_preimage:
|
||||||
payment_status = await check_transaction_status(wallet_id, payment_hash)
|
payment_status = await check_transaction_status(wallet_id, payment_hash)
|
||||||
if payment_status.success:
|
if payment_status.success:
|
||||||
|
|
@ -128,8 +128,8 @@ async def _process_invoice(
|
||||||
|
|
||||||
|
|
||||||
async def _on_pay_invoice(
|
async def _on_pay_invoice(
|
||||||
sp: NWCServiceProvider, pubkey: str, payload: Dict
|
sp: NWCServiceProvider, pubkey: str, payload: dict
|
||||||
) -> List[Tuple[Optional[Dict], Optional[Dict], List]]:
|
) -> list[tuple[dict | None, dict | None, list]]:
|
||||||
|
|
||||||
# hardening #
|
# hardening #
|
||||||
assert_valid_pubkey(pubkey)
|
assert_valid_pubkey(pubkey)
|
||||||
|
|
@ -169,8 +169,8 @@ async def _on_pay_invoice(
|
||||||
|
|
||||||
|
|
||||||
async def _on_multi_pay_invoice(
|
async def _on_multi_pay_invoice(
|
||||||
sp: NWCServiceProvider, pubkey: str, payload: Dict
|
sp: NWCServiceProvider, pubkey: str, payload: dict
|
||||||
) -> List[Tuple[Optional[Dict], Optional[Dict], List]]:
|
) -> list[tuple[dict | None, dict | None, list]]:
|
||||||
|
|
||||||
# hardening #
|
# hardening #
|
||||||
assert_valid_pubkey(pubkey)
|
assert_valid_pubkey(pubkey)
|
||||||
|
|
@ -184,7 +184,7 @@ async def _on_multi_pay_invoice(
|
||||||
raise Exception("Pubkey has no associated wallet")
|
raise Exception("Pubkey has no associated wallet")
|
||||||
params = payload.get("params", {})
|
params = payload.get("params", {})
|
||||||
invoices = params.get("invoices", [])
|
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
|
# Ensures all invoices are provided
|
||||||
for i in invoices:
|
for i in invoices:
|
||||||
|
|
@ -228,8 +228,8 @@ async def _on_multi_pay_invoice(
|
||||||
|
|
||||||
|
|
||||||
async def _on_make_invoice(
|
async def _on_make_invoice(
|
||||||
sp: NWCServiceProvider, pubkey: str, payload: Dict
|
sp: NWCServiceProvider, pubkey: str, payload: dict
|
||||||
) -> List[Tuple[Optional[Dict], Optional[Dict], List]]:
|
) -> list[tuple[dict | None, dict | None, list]]:
|
||||||
|
|
||||||
# hardening #
|
# hardening #
|
||||||
assert_valid_pubkey(pubkey)
|
assert_valid_pubkey(pubkey)
|
||||||
|
|
@ -298,8 +298,8 @@ async def _on_make_invoice(
|
||||||
|
|
||||||
|
|
||||||
async def _on_lookup_invoice(
|
async def _on_lookup_invoice(
|
||||||
sp: NWCServiceProvider, pubkey: str, payload: Dict
|
sp: NWCServiceProvider, pubkey: str, payload: dict
|
||||||
) -> List[Tuple[Optional[Dict], Optional[Dict], List]]:
|
) -> list[tuple[dict | None, dict | None, list]]:
|
||||||
|
|
||||||
# hardening #
|
# hardening #
|
||||||
assert_valid_pubkey(pubkey)
|
assert_valid_pubkey(pubkey)
|
||||||
|
|
@ -336,12 +336,12 @@ async def _on_lookup_invoice(
|
||||||
invoice_data = bolt11_decode(payment.bolt11)
|
invoice_data = bolt11_decode(payment.bolt11)
|
||||||
is_settled = not payment.pending
|
is_settled = not payment.pending
|
||||||
timestamp = int(payment.time.timestamp()) or int(invoice_data.date)
|
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 = (
|
preimage = (
|
||||||
payment.preimage
|
payment.preimage
|
||||||
or "0000000000000000000000000000000000000000000000000000000000000000"
|
or "0000000000000000000000000000000000000000000000000000000000000000"
|
||||||
)
|
)
|
||||||
res: Dict = {
|
res: dict = {
|
||||||
"type": "outgoing" if payment.is_out else "incoming",
|
"type": "outgoing" if payment.is_out else "incoming",
|
||||||
"invoice": payment.bolt11,
|
"invoice": payment.bolt11,
|
||||||
"description": (
|
"description": (
|
||||||
|
|
@ -363,8 +363,8 @@ async def _on_lookup_invoice(
|
||||||
|
|
||||||
|
|
||||||
async def _on_list_transactions(
|
async def _on_list_transactions(
|
||||||
sp: NWCServiceProvider, pubkey: str, payload: Dict
|
sp: NWCServiceProvider, pubkey: str, payload: dict
|
||||||
) -> List[Tuple[Optional[Dict], Optional[Dict], List]]:
|
) -> list[tuple[dict | None, dict | None, list]]:
|
||||||
# hardening #
|
# hardening #
|
||||||
assert_valid_pubkey(pubkey)
|
assert_valid_pubkey(pubkey)
|
||||||
# ## #
|
# ## #
|
||||||
|
|
@ -392,11 +392,9 @@ async def _on_list_transactions(
|
||||||
assert_sane_string(tx_type)
|
assert_sane_string(tx_type)
|
||||||
# ## #
|
# ## #
|
||||||
|
|
||||||
values = []
|
|
||||||
filters: Filters = Filters()
|
filters: Filters = Filters()
|
||||||
filters.where(["time <= ?"])
|
filters.where(["time <= :tuntil"])
|
||||||
values.append(tuntil)
|
filters.values({"tuntil": tuntil})
|
||||||
filters.values(values)
|
|
||||||
history = await get_payments(
|
history = await get_payments(
|
||||||
wallet_id=nwc.wallet,
|
wallet_id=nwc.wallet,
|
||||||
complete=True,
|
complete=True,
|
||||||
|
|
@ -409,7 +407,7 @@ async def _on_list_transactions(
|
||||||
limit=limit,
|
limit=limit,
|
||||||
offset=offset,
|
offset=offset,
|
||||||
)
|
)
|
||||||
transactions: List[Dict] = []
|
transactions: list[dict] = []
|
||||||
p: Payment
|
p: Payment
|
||||||
for p in history:
|
for p in history:
|
||||||
invoice_data = bolt11_decode(p.bolt11)
|
invoice_data = bolt11_decode(p.bolt11)
|
||||||
|
|
@ -435,8 +433,8 @@ async def _on_list_transactions(
|
||||||
|
|
||||||
|
|
||||||
async def _on_get_balance(
|
async def _on_get_balance(
|
||||||
sp: NWCServiceProvider, pubkey: str, payload: Dict
|
sp: NWCServiceProvider, pubkey: str, payload: dict
|
||||||
) -> List[Tuple[Optional[Dict], Optional[Dict], List]]:
|
) -> list[tuple[dict | None, dict | None, list]]:
|
||||||
|
|
||||||
# hardening #
|
# hardening #
|
||||||
assert_valid_pubkey(pubkey)
|
assert_valid_pubkey(pubkey)
|
||||||
|
|
@ -458,8 +456,8 @@ async def _on_get_balance(
|
||||||
|
|
||||||
|
|
||||||
async def _on_get_info(
|
async def _on_get_info(
|
||||||
sp: NWCServiceProvider, pubkey: str, payload: Dict
|
sp: NWCServiceProvider, pubkey: str, payload: dict
|
||||||
) -> List[Tuple[Optional[Dict], Optional[Dict], List]]:
|
) -> list[tuple[dict | None, dict | None, list]]:
|
||||||
|
|
||||||
# hardening #
|
# hardening #
|
||||||
assert_valid_pubkey(pubkey)
|
assert_valid_pubkey(pubkey)
|
||||||
|
|
@ -477,8 +475,8 @@ async def _on_get_info(
|
||||||
account_methods = []
|
account_methods = []
|
||||||
for spm in sp_methods:
|
for spm in sp_methods:
|
||||||
for p in permissions:
|
for p in permissions:
|
||||||
permissions_data: Dict[str, Any] = nwc_permissions.get(p, {})
|
permissions_data: dict[str, Any] = nwc_permissions.get(p, {})
|
||||||
allowed_methods: List[str] = permissions_data.get("methods", [])
|
allowed_methods: list[str] = permissions_data.get("methods", [])
|
||||||
if spm in allowed_methods:
|
if spm in allowed_methods:
|
||||||
account_methods.append(spm)
|
account_methods.append(spm)
|
||||||
break
|
break
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||||
%} {% block scripts %} {{ window_vars(user) }}
|
%} {% block scripts %} {{ window_vars(user) }}
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import * as NobleSecp256k1 from "/nwcprovider/static/js/noble-secp256k1.min.js";
|
import * as NobleSecp256k1 from '/nwcprovider/static/js/noble-secp256k1.min.js'
|
||||||
window.NobleSecp256k1 = NobleSecp256k1;
|
window.NobleSecp256k1 = NobleSecp256k1
|
||||||
</script>
|
</script>
|
||||||
<script src="{{ static_url_for('nwcprovider/static', path='js/index.js') }}"></script>
|
<script src="{{ static_url_for('nwcprovider/static', path='js/index.js') }}"></script>
|
||||||
{% endblock %} {% block page %}
|
{% endblock %} {% block page %}
|
||||||
|
|
|
||||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
|
|
@ -4,17 +4,17 @@ import hashlib
|
||||||
import json
|
import json
|
||||||
import random
|
import random
|
||||||
import time
|
import time
|
||||||
from typing import Dict, List, Optional, Union
|
from typing import Union
|
||||||
|
|
||||||
import bolt11
|
import bolt11
|
||||||
import httpx
|
import httpx
|
||||||
import pytest
|
import pytest
|
||||||
import secp256k1
|
import secp256k1
|
||||||
import websockets.client as websockets
|
|
||||||
from Cryptodome import Random
|
from Cryptodome import Random
|
||||||
from Cryptodome.Cipher import AES
|
from Cryptodome.Cipher import AES
|
||||||
from Cryptodome.Util.Padding import pad, unpad
|
from Cryptodome.Util.Padding import pad, unpad
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
from websockets.legacy.client import connect
|
||||||
|
|
||||||
wallets = {
|
wallets = {
|
||||||
"wallet1": {
|
"wallet1": {
|
||||||
|
|
@ -107,8 +107,8 @@ def gen_keypair():
|
||||||
async def create_nwc(
|
async def create_nwc(
|
||||||
w: str,
|
w: str,
|
||||||
desc: str,
|
desc: str,
|
||||||
permissions: List[str],
|
permissions: list[str],
|
||||||
budgets: List[Dict[str, int]],
|
budgets: list[dict[str, int]],
|
||||||
expiration: int = 0,
|
expiration: int = 0,
|
||||||
):
|
):
|
||||||
keypair = gen_keypair()
|
keypair = gen_keypair()
|
||||||
|
|
@ -210,7 +210,7 @@ class NWCWallet:
|
||||||
async def _run(self):
|
async def _run(self):
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
async with websockets.connect(self.relay) as ws:
|
async with connect(self.relay) as ws:
|
||||||
self.ws = ws
|
self.ws = ws
|
||||||
self.connected = True
|
self.connected = True
|
||||||
self.sub_id = self._get_new_subid()
|
self.sub_id = self._get_new_subid()
|
||||||
|
|
@ -244,7 +244,7 @@ class NWCWallet:
|
||||||
break
|
break
|
||||||
|
|
||||||
def _encrypt_content(
|
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:
|
) -> str:
|
||||||
pubkey = secp256k1.PublicKey(bytes.fromhex("02" + pubkey_hex), True)
|
pubkey = secp256k1.PublicKey(bytes.fromhex("02" + pubkey_hex), True)
|
||||||
shared = pubkey.tweak_mul(bytes.fromhex(self.private_key_hex)).serialize()[1:]
|
shared = pubkey.tweak_mul(bytes.fromhex(self.private_key_hex)).serialize()[1:]
|
||||||
|
|
@ -273,7 +273,7 @@ class NWCWallet:
|
||||||
decrypted = decrypted_bytes.decode("utf-8")
|
decrypted = decrypted_bytes.decode("utf-8")
|
||||||
return decrypted
|
return decrypted
|
||||||
|
|
||||||
async def _on_message(self, ws, message: str):
|
async def _on_message(self, _, message: str):
|
||||||
logger.debug("Received message: " + message)
|
logger.debug("Received message: " + message)
|
||||||
msg = json.loads(message)
|
msg = json.loads(message)
|
||||||
if msg[0] == "EVENT": # Event message
|
if msg[0] == "EVENT": # Event message
|
||||||
|
|
@ -292,12 +292,12 @@ class NWCWallet:
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
def _json_dumps(self, data: Union[Dict, list]) -> str:
|
def _json_dumps(self, data: Union[dict, list]) -> str:
|
||||||
if isinstance(data, Dict):
|
if isinstance(data, dict):
|
||||||
data = {k: v for k, v in data.items() if v is not None}
|
data = {k: v for k, v in data.items() if v is not None}
|
||||||
return json.dumps(data, separators=(",", ":"), ensure_ascii=False)
|
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(
|
signature_data = self._json_dumps(
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
|
|
@ -386,14 +386,14 @@ async def test_make_invoice():
|
||||||
await wallet1.send_event(
|
await wallet1.send_event(
|
||||||
"make_invoice", {"amount": 1, "description": "test 123", "expiry": 1000}
|
"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)
|
logger.info(error)
|
||||||
assert error, "Expected internal error, because amount is too low"
|
assert error, "Expected internal error, because amount is too low"
|
||||||
|
|
||||||
await wallet1.send_event(
|
await wallet1.send_event(
|
||||||
"make_invoice", {"amount": 123000, "description": "test 123", "expiry": 1000}
|
"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 not error
|
||||||
assert result["type"] == "incoming"
|
assert result["type"] == "incoming"
|
||||||
assert result["description"] == "test 123"
|
assert result["description"] == "test 123"
|
||||||
|
|
@ -424,7 +424,7 @@ async def test_lookup_invoice():
|
||||||
await wallet1.send_event(
|
await wallet1.send_event(
|
||||||
"make_invoice", {"amount": 123000, "description": "test 123", "expiry": 1000}
|
"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 not error
|
||||||
assert result["type"] == "incoming"
|
assert result["type"] == "incoming"
|
||||||
assert result["description"] == "test 123"
|
assert result["description"] == "test 123"
|
||||||
|
|
@ -440,7 +440,7 @@ async def test_lookup_invoice():
|
||||||
await wallet2.start()
|
await wallet2.start()
|
||||||
|
|
||||||
await wallet2.send_event("lookup_invoice", {"invoice": result["invoice"]})
|
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 not error
|
||||||
assert result["type"] == "incoming"
|
assert result["type"] == "incoming"
|
||||||
assert result["description"] == "test 123"
|
assert result["description"] == "test 123"
|
||||||
|
|
@ -465,7 +465,7 @@ async def test_get_info():
|
||||||
await wallet1.start()
|
await wallet1.start()
|
||||||
|
|
||||||
await wallet1.send_event("get_info", {})
|
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 not error
|
||||||
assert result["alias"] == "LNBits_NWC_SP"
|
assert result["alias"] == "LNBits_NWC_SP"
|
||||||
assert result["color"] == ""
|
assert result["color"] == ""
|
||||||
|
|
@ -492,33 +492,33 @@ async def test_permisions():
|
||||||
await wallet1.start()
|
await wallet1.start()
|
||||||
|
|
||||||
await wallet1.send_event("get_info", {})
|
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 not error
|
||||||
|
|
||||||
await wallet1.send_event(
|
await wallet1.send_event(
|
||||||
"make_invoice", {"amount": 123000, "description": "test 123", "expiry": 1000}
|
"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
|
assert error
|
||||||
|
|
||||||
await wallet1.close()
|
await wallet1.close()
|
||||||
await wallet2.start()
|
await wallet2.start()
|
||||||
|
|
||||||
await wallet2.send_event("get_info", {})
|
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
|
assert error
|
||||||
|
|
||||||
await wallet2.send_event(
|
await wallet2.send_event(
|
||||||
"make_invoice", {"amount": 123000, "description": "test 123", "expiry": 1000}
|
"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
|
assert not error
|
||||||
|
|
||||||
await wallet2.close()
|
await wallet2.close()
|
||||||
await wallet3.start()
|
await wallet3.start()
|
||||||
|
|
||||||
await wallet3.send_event("get_info", {})
|
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 not error
|
||||||
assert "make_invoice" in result["methods"]
|
assert "make_invoice" in result["methods"]
|
||||||
assert "pay_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"}
|
"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 not error
|
||||||
assert result["invoice"]
|
assert result["invoice"]
|
||||||
|
|
||||||
|
|
@ -557,7 +557,7 @@ async def test_pay_invoice_and_balance():
|
||||||
await wallet2.start()
|
await wallet2.start()
|
||||||
|
|
||||||
await wallet2.send_event("pay_invoice", {"invoice": invoice})
|
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 not error
|
||||||
assert result["preimage"]
|
assert result["preimage"]
|
||||||
|
|
||||||
|
|
@ -569,12 +569,12 @@ async def test_pay_invoice_and_balance():
|
||||||
assert wallet2_balance_new == wallet2_balance - 123000
|
assert wallet2_balance_new == wallet2_balance - 123000
|
||||||
|
|
||||||
await wallet1.send_event("get_balance", {})
|
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 not error
|
||||||
assert result["balance"] == wallet1_balance_new
|
assert result["balance"] == wallet1_balance_new
|
||||||
|
|
||||||
await wallet2.send_event("get_balance", {})
|
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 not error
|
||||||
assert result["balance"] == wallet2_balance_new
|
assert result["balance"] == wallet2_balance_new
|
||||||
|
|
||||||
|
|
@ -703,13 +703,13 @@ async def test_insufficient_balance():
|
||||||
await wallet2.send_event(
|
await wallet2.send_event(
|
||||||
"make_invoice", {"amount": amount_to_spend, "description": "test 123"}
|
"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 not error
|
||||||
assert result["invoice"]
|
assert result["invoice"]
|
||||||
invoice = result["invoice"]
|
invoice = result["invoice"]
|
||||||
|
|
||||||
await wallet1.send_event("pay_invoice", {"invoice": 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(error)
|
||||||
logger.info(result)
|
logger.info(result)
|
||||||
logger.info(amount_to_spend)
|
logger.info(amount_to_spend)
|
||||||
|
|
@ -735,7 +735,7 @@ async def test_expiry():
|
||||||
await wallet3.send_event(
|
await wallet3.send_event(
|
||||||
"make_invoice", {"amount": 123000, "description": "test 123"}
|
"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
|
||||||
assert (
|
assert (
|
||||||
error["code"] == "UNAUTHORIZED"
|
error["code"] == "UNAUTHORIZED"
|
||||||
|
|
@ -768,22 +768,22 @@ async def test_budget():
|
||||||
await wallet1.send_event(
|
await wallet1.send_event(
|
||||||
"make_invoice", {"amount": 101000, "description": "Invalid"}
|
"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
|
assert not error
|
||||||
|
|
||||||
await wallet3.send_event("pay_invoice", {"invoice": result["invoice"]})
|
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
|
||||||
assert (
|
assert (
|
||||||
error["code"] == "QUOTA_EXCEEDED"
|
error["code"] == "QUOTA_EXCEEDED"
|
||||||
), "Expected QUOTA_EXCEEDED error, because the budget was exceeded"
|
), "Expected QUOTA_EXCEEDED error, because the budget was exceeded"
|
||||||
|
|
||||||
await wallet1.send_event("make_invoice", {"amount": 99000, "description": "Valid"})
|
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
|
assert not error
|
||||||
|
|
||||||
await wallet3.send_event("pay_invoice", {"invoice": result["invoice"]})
|
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 not error, "Expected successful payment, because the budget was not exceeded"
|
||||||
assert result["preimage"]
|
assert result["preimage"]
|
||||||
|
|
||||||
|
|
@ -791,11 +791,11 @@ async def test_budget():
|
||||||
"make_invoice", {"amount": 100000 - 99000 + 1000, "description": "Invalid"}
|
"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
|
assert not error
|
||||||
|
|
||||||
await wallet3.send_event("pay_invoice", {"invoice": result["invoice"]})
|
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
|
||||||
assert (
|
assert (
|
||||||
error["code"] == "QUOTA_EXCEEDED"
|
error["code"] == "QUOTA_EXCEEDED"
|
||||||
|
|
@ -824,21 +824,21 @@ async def test_budget_refresh():
|
||||||
await wallet1.send_event(
|
await wallet1.send_event(
|
||||||
"make_invoice", {"amount": 100000, "description": "Invalid"}
|
"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
|
assert not error
|
||||||
|
|
||||||
await wallet1.send_event(
|
await wallet1.send_event(
|
||||||
"make_invoice", {"amount": 100000, "description": "Invalid"}
|
"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
|
assert not error
|
||||||
|
|
||||||
await wallet3.send_event("pay_invoice", {"invoice": result["invoice"]})
|
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 not error, "Expected successful payment, because the budget was not exceeded"
|
||||||
|
|
||||||
await wallet3.send_event("pay_invoice", {"invoice": result2["invoice"]})
|
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
|
||||||
assert (
|
assert (
|
||||||
error["code"] == "QUOTA_EXCEEDED"
|
error["code"] == "QUOTA_EXCEEDED"
|
||||||
|
|
@ -846,11 +846,11 @@ async def test_budget_refresh():
|
||||||
|
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
await wallet1.send_event("make_invoice", {"amount": 100000, "description": "Valid"})
|
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
|
assert not error
|
||||||
|
|
||||||
await wallet3.send_event("pay_invoice", {"invoice": result["invoice"]})
|
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"
|
assert not error, "Expected successful payment, because the budget was refreshed"
|
||||||
|
|
||||||
await wallet3.close()
|
await wallet3.close()
|
||||||
|
|
@ -916,6 +916,7 @@ async def test_invalid_invoice_handling():
|
||||||
# Send invalid invoice
|
# Send invalid invoice
|
||||||
await wallet.send_event("pay_invoice", {"invoice": "invalid_lninvoice"})
|
await wallet.send_event("pay_invoice", {"invoice": "invalid_lninvoice"})
|
||||||
_, _, error = await wallet.wait_for("pay_invoice")
|
_, _, error = await wallet.wait_for("pay_invoice")
|
||||||
|
assert error
|
||||||
assert error["code"] == "INTERNAL"
|
assert error["code"] == "INTERNAL"
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -935,6 +936,7 @@ async def test_replay_attack():
|
||||||
# Replay same message
|
# Replay same message
|
||||||
await wallet.send_event("pay_invoice", {"invoice": valid_invoice})
|
await wallet.send_event("pay_invoice", {"invoice": valid_invoice})
|
||||||
_, _, error = await wallet.wait_for("pay_invoice")
|
_, _, error = await wallet.wait_for("pay_invoice")
|
||||||
|
assert error
|
||||||
assert error["code"] == "PAYMENT_FAILED"
|
assert error["code"] == "PAYMENT_FAILED"
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -967,6 +969,7 @@ async def test_budget_bypass():
|
||||||
invoice2 = await create_valid_invoice(wallet, 60000)
|
invoice2 = await create_valid_invoice(wallet, 60000)
|
||||||
await wallet.send_event("pay_invoice", {"invoice": invoice2})
|
await wallet.send_event("pay_invoice", {"invoice": invoice2})
|
||||||
_, _, error = await wallet.wait_for("pay_invoice")
|
_, _, error = await wallet.wait_for("pay_invoice")
|
||||||
|
assert error
|
||||||
assert error["code"] == "QUOTA_EXCEEDED"
|
assert error["code"] == "QUOTA_EXCEEDED"
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -992,19 +995,28 @@ async def create_valid_invoice(wallet, amount=1000):
|
||||||
await wallet.send_event(
|
await wallet.send_event(
|
||||||
"make_invoice", {"amount": amount, "description": "test invoice"}
|
"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:
|
if error:
|
||||||
raise Exception(f"Failed to create invoice: {error}")
|
raise Exception(f"Failed to create invoice: {error}")
|
||||||
return result["invoice"]
|
return result["invoice"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_list_transactions():
|
async def test_list_transactions():
|
||||||
# Create wallets with required permissions
|
# Create wallets with required permissions
|
||||||
nwc1 = await create_nwc(
|
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(
|
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"])
|
wallet1 = NWCWallet(nwc1["pairing"])
|
||||||
|
|
|
||||||
0
tests/unit/__init__.py
Normal file
0
tests/unit/__init__.py
Normal file
|
|
@ -1,18 +1,11 @@
|
||||||
import json
|
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 random
|
||||||
import string
|
import string
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
from nwcp import NWCServiceProvider
|
from ...nwcp import NWCServiceProvider
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
|
||||||
37
views_api.py
37
views_api.py
|
|
@ -1,5 +1,4 @@
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import Dict, List, Optional
|
|
||||||
|
|
||||||
import secp256k1
|
import secp256k1
|
||||||
from fastapi import APIRouter, Depends, Request
|
from fastapi import APIRouter, Depends, Request
|
||||||
|
|
@ -38,20 +37,18 @@ nwcprovider_api_router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
# Get supported permissions
|
# Get supported permissions
|
||||||
@nwcprovider_api_router.get("/api/v1/permissions", status_code=HTTPStatus.OK)
|
@nwcprovider_api_router.get("/api/v1/permissions")
|
||||||
async def api_get_permissions() -> Dict:
|
async def api_get_permissions() -> dict:
|
||||||
return nwc_permissions
|
return nwc_permissions
|
||||||
|
|
||||||
|
|
||||||
## Get nwc keys associated with the wallet
|
## Get nwc keys associated with the wallet
|
||||||
@nwcprovider_api_router.get(
|
@nwcprovider_api_router.get("/api/v1/nwc")
|
||||||
"/api/v1/nwc", status_code=HTTPStatus.OK, response_model=List[NWCGetResponse]
|
|
||||||
)
|
|
||||||
async def api_get_nwcs(
|
async def api_get_nwcs(
|
||||||
include_expired: bool = False,
|
include_expired: bool = False,
|
||||||
calculate_spent_budget: bool = False,
|
calculate_spent_budget: bool = False,
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
):
|
) -> list[NWCGetResponse]:
|
||||||
wallet_id = wallet.wallet.id
|
wallet_id = wallet.wallet.id
|
||||||
|
|
||||||
# hardening #
|
# hardening #
|
||||||
|
|
@ -74,9 +71,7 @@ async def api_get_nwcs(
|
||||||
|
|
||||||
|
|
||||||
# Get a nwc key
|
# Get a nwc key
|
||||||
@nwcprovider_api_router.get(
|
@nwcprovider_api_router.get("/api/v1/nwc/{pubkey}")
|
||||||
"/api/v1/nwc/{pubkey}", status_code=HTTPStatus.OK, response_model=NWCGetResponse
|
|
||||||
)
|
|
||||||
async def api_get_nwc(
|
async def api_get_nwc(
|
||||||
pubkey: str,
|
pubkey: str,
|
||||||
include_expired: bool = False,
|
include_expired: bool = False,
|
||||||
|
|
@ -102,22 +97,20 @@ async def api_get_nwc(
|
||||||
|
|
||||||
|
|
||||||
# Get pairing url for given secret
|
# Get pairing url for given secret
|
||||||
@nwcprovider_api_router.get(
|
@nwcprovider_api_router.get("/api/v1/pairing/{secret}")
|
||||||
"/api/v1/pairing/{secret}", status_code=HTTPStatus.OK, response_model=str
|
|
||||||
)
|
|
||||||
async def api_get_pairing_url(req: Request, secret: str) -> str:
|
async def api_get_pairing_url(req: Request, secret: str) -> str:
|
||||||
|
|
||||||
# hardening #
|
# hardening #
|
||||||
assert_sane_string(secret)
|
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:
|
if not pprivkey:
|
||||||
raise Exception("Extension is not configured")
|
raise Exception("Extension is not configured")
|
||||||
relay = await get_config_nwc("relay")
|
relay = await get_config_nwc("relay")
|
||||||
if not relay:
|
if not relay:
|
||||||
raise Exception("Extension is not configured")
|
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:
|
if relay_alias:
|
||||||
relay = relay_alias
|
relay = relay_alias
|
||||||
else:
|
else:
|
||||||
|
|
@ -147,13 +140,12 @@ async def api_get_pairing_url(req: Request, secret: str) -> str:
|
||||||
@nwcprovider_api_router.put(
|
@nwcprovider_api_router.put(
|
||||||
"/api/v1/nwc/{pubkey}",
|
"/api/v1/nwc/{pubkey}",
|
||||||
status_code=HTTPStatus.CREATED,
|
status_code=HTTPStatus.CREATED,
|
||||||
response_model=NWCGetResponse,
|
|
||||||
)
|
)
|
||||||
async def api_register_nwc(
|
async def api_register_nwc(
|
||||||
pubkey: str,
|
pubkey: str,
|
||||||
data: NWCRegistrationRequest,
|
data: NWCRegistrationRequest,
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
):
|
) -> NWCGetResponse:
|
||||||
wallet_id = wallet.wallet.id
|
wallet_id = wallet.wallet.id
|
||||||
|
|
||||||
# hardening #
|
# hardening #
|
||||||
|
|
@ -177,7 +169,7 @@ async def api_register_nwc(
|
||||||
|
|
||||||
|
|
||||||
# Delete a nwc key
|
# 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(
|
async def api_delete_nwc(
|
||||||
pubkey: str, wallet: WalletTypeInfo = Depends(require_admin_key)
|
pubkey: str, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||||
):
|
):
|
||||||
|
|
@ -193,9 +185,7 @@ async def api_delete_nwc(
|
||||||
|
|
||||||
|
|
||||||
# Get config
|
# Get config
|
||||||
@nwcprovider_api_router.get(
|
@nwcprovider_api_router.get("/api/v1/config", dependencies=[Depends(check_admin)])
|
||||||
"/api/v1/config", status_code=HTTPStatus.OK, dependencies=[Depends(check_admin)]
|
|
||||||
)
|
|
||||||
async def api_get_all_config_nwc():
|
async def api_get_all_config_nwc():
|
||||||
config = await get_all_config_nwc()
|
config = await get_all_config_nwc()
|
||||||
return config
|
return config
|
||||||
|
|
@ -204,7 +194,6 @@ async def api_get_all_config_nwc():
|
||||||
# Get config
|
# Get config
|
||||||
@nwcprovider_api_router.get(
|
@nwcprovider_api_router.get(
|
||||||
"/api/v1/config/{key}",
|
"/api/v1/config/{key}",
|
||||||
status_code=HTTPStatus.OK,
|
|
||||||
dependencies=[Depends(check_admin)],
|
dependencies=[Depends(check_admin)],
|
||||||
)
|
)
|
||||||
async def api_get_config_nwc(key: str):
|
async def api_get_config_nwc(key: str):
|
||||||
|
|
@ -215,9 +204,7 @@ async def api_get_config_nwc(key: str):
|
||||||
|
|
||||||
|
|
||||||
# Set config
|
# Set config
|
||||||
@nwcprovider_api_router.post(
|
@nwcprovider_api_router.post("/api/v1/config", dependencies=[Depends(check_admin)])
|
||||||
"/api/v1/config", status_code=HTTPStatus.OK, dependencies=[Depends(check_admin)]
|
|
||||||
)
|
|
||||||
async def api_set_config_nwc(req: Request):
|
async def api_set_config_nwc(req: Request):
|
||||||
data = await req.json()
|
data = await req.json()
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue