code quality fixes and cleanup
This commit is contained in:
parent
29b1b17967
commit
2bbbcd4d65
32 changed files with 4943 additions and 1368 deletions
|
|
@ -1,33 +1,28 @@
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": "lnbits_nwc_provider",
|
"name": "lnbits_nwc_provider",
|
||||||
"image": "mcr.microsoft.com/devcontainers/python:1-3.9-bullseye",
|
"image": "mcr.microsoft.com/devcontainers/python:1-3.9-bullseye",
|
||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/devcontainers-contrib/features/poetry:2": {
|
"ghcr.io/devcontainers-contrib/features/poetry:2": {}
|
||||||
}
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"mounts": [
|
"mounts": [
|
||||||
"source=${localWorkspaceFolder}/.devcontainer/start.sh,target=/start-lnbits.sh,type=bind",
|
"source=${localWorkspaceFolder}/.devcontainer/start.sh,target=/start-lnbits.sh,type=bind",
|
||||||
"source=${localWorkspaceFolder}/.devcontainer/setup.sh,target=/setup.sh,type=bind",
|
"source=${localWorkspaceFolder}/.devcontainer/setup.sh,target=/setup.sh,type=bind",
|
||||||
"source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind"
|
"source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind"
|
||||||
|
|
||||||
],
|
],
|
||||||
"containerEnv": {
|
"containerEnv": {
|
||||||
"IS_DEV_CONTAINER": "true",
|
"IS_DEV_CONTAINER": "true"
|
||||||
},
|
},
|
||||||
"postCreateCommand": "/bin/bash /setup.sh ${containerWorkspaceFolder}",
|
"postCreateCommand": "/bin/bash /setup.sh ${containerWorkspaceFolder}",
|
||||||
"postStartCommand": "/bin/bash /start-lnbits.sh",
|
"postStartCommand": "/bin/bash /start-lnbits.sh",
|
||||||
"forwardPorts": [
|
"forwardPorts": [5000],
|
||||||
5000
|
|
||||||
],
|
|
||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
"settings": {
|
"settings": {
|
||||||
|
"python.analysis.extraPaths": ["/home/vscode/lnbits"],
|
||||||
|
"python.pythonPath": "/home/vscode/python"
|
||||||
// "python.pythonPath": "/opt/python/bin/python3.9"
|
// "python.pythonPath": "/opt/python/bin/python3.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"runArgs": ["--network", "host"]
|
"runArgs": ["--network", "host"]
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
echo $PYTHONPATH
|
||||||
CONTAINER_WORKSPACE_FOLDER=$1
|
CONTAINER_WORKSPACE_FOLDER=$1
|
||||||
cd $CONTAINER_WORKSPACE_FOLDER
|
cd $CONTAINER_WORKSPACE_FOLDER
|
||||||
sudo apt update -y
|
sudo apt update -y
|
||||||
|
|
@ -8,14 +9,18 @@ sudo apt-get install -y docker.io
|
||||||
curl -fsSL https://deb.nodesource.com/setup_20.x -o /tmp/nodesource_setup.sh
|
curl -fsSL https://deb.nodesource.com/setup_20.x -o /tmp/nodesource_setup.sh
|
||||||
sudo bash /tmp/nodesource_setup.sh
|
sudo bash /tmp/nodesource_setup.sh
|
||||||
sudo apt-get install -y nodejs
|
sudo apt-get install -y nodejs
|
||||||
if [ ! -d lnbits ] ; then
|
|
||||||
git clone https://github.com/lnbits/lnbits.git;
|
cd $HOME
|
||||||
|
echo $PWD
|
||||||
|
if [ ! -d ./lnbits ] ; then
|
||||||
|
git clone https://github.com/lnbits/lnbits.git lnbits
|
||||||
fi
|
fi
|
||||||
cd lnbits
|
cd lnbits
|
||||||
|
echo $PWD
|
||||||
git checkout 0.12.8
|
git checkout 0.12.8
|
||||||
poetry env use python3.9
|
poetry env use python3.9
|
||||||
# export VENV_PATH=$(poetry env info -p)
|
POETRY_PYTHON_PATH=$(poetry env info -p)/bin/python
|
||||||
# sudo ln -s $VENV_PATH /opt/python
|
ln -sf $POETRY_PYTHON_PATH /home/vscode/python
|
||||||
make bundle
|
make bundle
|
||||||
poetry install --no-interaction
|
poetry install --no-interaction
|
||||||
mkdir -p lnbits/extensions/
|
mkdir -p lnbits/extensions/
|
||||||
|
|
@ -23,4 +28,7 @@ if [ ! -d lnbits/extensions/nwcprovider ] ; then
|
||||||
ln -s $CONTAINER_WORKSPACE_FOLDER lnbits/extensions/nwcprovider
|
ln -s $CONTAINER_WORKSPACE_FOLDER lnbits/extensions/nwcprovider
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
cd $CONTAINER_WORKSPACE_FOLDER
|
||||||
|
poetry install --no-interaction
|
||||||
|
npm i prettier
|
||||||
|
npm i pyright
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
ln -s $PWD/.devcontainer/.env ./lnbits/.env
|
ln -s $PWD/.devcontainer/.env $HOME/lnbits/.env
|
||||||
cd ./lnbits
|
cd $HOME/lnbits
|
||||||
poetry run lnbits
|
poetry run lnbits
|
||||||
|
|
|
||||||
3
.github/workflows/main.yml
vendored
3
.github/workflows/main.yml
vendored
|
|
@ -4,7 +4,6 @@ on:
|
||||||
- "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:
|
||||||
|
|
@ -23,7 +22,7 @@ jobs:
|
||||||
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: |
|
||||||
|
|
|
||||||
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
|
|
@ -4,7 +4,6 @@ on:
|
||||||
- "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:
|
||||||
|
|
@ -23,7 +22,7 @@ jobs:
|
||||||
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: |
|
||||||
|
|
|
||||||
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
|
|
@ -4,7 +4,6 @@ on:
|
||||||
push:
|
push:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
@ -16,7 +15,7 @@ jobs:
|
||||||
- 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.9"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -27,7 +26,6 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
/bin/bash .devcontainer/setup.sh ${{ github.workspace }}
|
/bin/bash .devcontainer/setup.sh ${{ github.workspace }}
|
||||||
|
|
||||||
|
|
||||||
- name: Run unit tests
|
- name: Run unit tests
|
||||||
run: |
|
run: |
|
||||||
cd lnbits
|
cd lnbits
|
||||||
|
|
@ -45,4 +43,4 @@ jobs:
|
||||||
poetry_env_path=$(poetry env info --path)
|
poetry_env_path=$(poetry env info --path)
|
||||||
source $poetry_env_path/bin/activate
|
source $poetry_env_path/bin/activate
|
||||||
cd ..
|
cd ..
|
||||||
pytest tests/unit/*.py -s
|
pytest tests/integration/*.py -s
|
||||||
|
|
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -2,3 +2,7 @@ __pycache__
|
||||||
lnbits
|
lnbits
|
||||||
tests/integration/strfry-data
|
tests/integration/strfry-data
|
||||||
tests/integration/lnbits_itest_data
|
tests/integration/lnbits_itest_data
|
||||||
|
node_modules
|
||||||
|
.pytest_cache
|
||||||
|
.ruff_cache
|
||||||
|
.mypy_cache
|
||||||
1
.vscode/settings.json
vendored
Normal file
1
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{}
|
||||||
47
Makefile
Normal file
47
Makefile
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
all: format check
|
||||||
|
|
||||||
|
format: prettier black ruff
|
||||||
|
|
||||||
|
check: mypy pyright checkblack checkruff checkprettier
|
||||||
|
|
||||||
|
prettier:
|
||||||
|
poetry run ./node_modules/.bin/prettier --write .
|
||||||
|
pyright:
|
||||||
|
poetry run ./node_modules/.bin/pyright
|
||||||
|
|
||||||
|
mypy:
|
||||||
|
poetry run mypy .
|
||||||
|
|
||||||
|
black:
|
||||||
|
poetry run black .
|
||||||
|
|
||||||
|
ruff:
|
||||||
|
poetry run ruff check . --fix
|
||||||
|
|
||||||
|
checkruff:
|
||||||
|
poetry run ruff check .
|
||||||
|
|
||||||
|
checkprettier:
|
||||||
|
poetry run ./node_modules/.bin/prettier --check .
|
||||||
|
|
||||||
|
checkblack:
|
||||||
|
poetry run black --check .
|
||||||
|
|
||||||
|
checkeditorconfig:
|
||||||
|
editorconfig-checker
|
||||||
|
|
||||||
|
test:
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
DEBUG=true \
|
||||||
|
poetry run pytest tests/unit/*.py -s
|
||||||
|
install-pre-commit-hook:
|
||||||
|
@echo "Installing pre-commit hook to git"
|
||||||
|
@echo "Uninstall the hook with poetry run pre-commit uninstall"
|
||||||
|
poetry run pre-commit install
|
||||||
|
|
||||||
|
pre-commit:
|
||||||
|
poetry run pre-commit run --all-files
|
||||||
|
|
||||||
|
|
||||||
|
checkbundle:
|
||||||
|
@echo "skipping checkbundle"
|
||||||
|
|
@ -13,7 +13,7 @@ Configure the extension from the "Settings" page in the top right menu when logg
|
||||||
### Configuration Options:
|
### Configuration Options:
|
||||||
|
|
||||||
| Key | Description | Default |
|
| Key | Description | Default |
|
||||||
|---------------|------------------------------------------------------------------------------------------------------------------------------------|--------------------------------|
|
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- |
|
||||||
| relay | URL of the nostr relay for dispatching and receiving NWC events. Use public relays or a custom one. Specify `nostrclient` to connect to the [nostrclient extension](https://github.com/lnbits/nostrclient). | nostrclient |
|
| relay | URL of the nostr relay for dispatching and receiving NWC events. Use public relays or a custom one. Specify `nostrclient` to connect to the [nostrclient extension](https://github.com/lnbits/nostrclient). | nostrclient |
|
||||||
| provider_key | Nostr secret key of the NWC Service Provider. | Random key generated on install |
|
| provider_key | Nostr secret key of the NWC Service Provider. | Random key generated on install |
|
||||||
| relay_alias | Relay URL to display in pairing URLs. Set if different from `relay`. | Empty (uses the `relay` value) |
|
| relay_alias | Relay URL to display in pairing URLs. Set if different from `relay`. | Empty (uses the `relay` value) |
|
||||||
|
|
|
||||||
38
__init__.py
38
__init__.py
|
|
@ -1,18 +1,22 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from lnbits.db import Database
|
|
||||||
from lnbits.helpers import template_renderer
|
|
||||||
from lnbits.tasks import create_permanent_unique_task
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
from lnbits.tasks import create_permanent_unique_task
|
||||||
|
|
||||||
|
from .crud import db
|
||||||
|
from .tasks import handle_execution_queue, handle_nwc
|
||||||
|
from .views import nwcprovider_router
|
||||||
|
from .views_api import nwcprovider_api_router
|
||||||
|
|
||||||
db = Database("ext_nwcprovider")
|
|
||||||
execution_queue = asyncio.Queue()
|
|
||||||
scheduled_tasks: list[asyncio.Task] = []
|
scheduled_tasks: list[asyncio.Task] = []
|
||||||
|
|
||||||
nwcprovider_ext: APIRouter = APIRouter(
|
nwcprovider_ext: APIRouter = APIRouter(
|
||||||
prefix="/nwcprovider", tags=["NWC Service Provider"]
|
prefix="/nwcprovider", tags=["NWC Service Provider"]
|
||||||
)
|
)
|
||||||
|
nwcprovider_ext.include_router(nwcprovider_router)
|
||||||
|
nwcprovider_ext.include_router(nwcprovider_api_router)
|
||||||
|
|
||||||
nwcprovider_static_files = [
|
nwcprovider_static_files = [
|
||||||
{
|
{
|
||||||
|
|
@ -22,25 +26,6 @@ nwcprovider_static_files = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def nwcprovider_renderer():
|
|
||||||
return template_renderer(["nwcprovider/templates"])
|
|
||||||
|
|
||||||
|
|
||||||
async def enqueue(action):
|
|
||||||
future = asyncio.Future()
|
|
||||||
execution_queue.put_nowait({
|
|
||||||
"action": action,
|
|
||||||
"future": future
|
|
||||||
})
|
|
||||||
return await future
|
|
||||||
|
|
||||||
from .views import *
|
|
||||||
from .views_api import *
|
|
||||||
from .tasks import handle_nwc, handle_execution_queue
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def nwcprovider_stop():
|
def nwcprovider_stop():
|
||||||
for task in scheduled_tasks:
|
for task in scheduled_tasks:
|
||||||
try:
|
try:
|
||||||
|
|
@ -53,6 +38,9 @@ def nwcprovider_start():
|
||||||
task = create_permanent_unique_task("ext_nwcprovider", handle_nwc)
|
task = create_permanent_unique_task("ext_nwcprovider", handle_nwc)
|
||||||
scheduled_tasks.append(task)
|
scheduled_tasks.append(task)
|
||||||
task = create_permanent_unique_task(
|
task = create_permanent_unique_task(
|
||||||
"ext_nwcprovider_execution_queue", handle_execution_queue)
|
"ext_nwcprovider_execution_queue", handle_execution_queue
|
||||||
|
)
|
||||||
scheduled_tasks.append(task)
|
scheduled_tasks.append(task)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["db"]
|
||||||
|
|
|
||||||
155
crud.py
155
crud.py
|
|
@ -1,39 +1,64 @@
|
||||||
from typing import Optional, Any, List
|
|
||||||
from . import db
|
|
||||||
from .models import NWCKey, NWCBudget, NWCLog, NWCNewBudget
|
|
||||||
import time
|
import time
|
||||||
from . import enqueue
|
from typing import List, Optional
|
||||||
import json
|
|
||||||
|
from lnbits.db import Database
|
||||||
|
|
||||||
|
from .execution_queue import enqueue
|
||||||
|
from .models import NWCBudget, NWCKey, NWCNewBudget
|
||||||
|
|
||||||
|
db = Database("ext_nwcprovider")
|
||||||
|
|
||||||
|
|
||||||
async def create_nwc(
|
async def create_nwc(
|
||||||
pubkey: str,
|
pubkey: str,
|
||||||
wallet_id:str,
|
wallet_id: str,
|
||||||
description: str,
|
description: str,
|
||||||
expires_at: int ,
|
expires_at: int,
|
||||||
permissions:List[str] ,
|
permissions: List[str],
|
||||||
budgets: Optional[List[NWCNewBudget]] = None
|
budgets: Optional[List[NWCNewBudget]] = None,
|
||||||
) -> NWCKey:
|
) -> NWCKey:
|
||||||
# Check if the key already exists
|
# Check if the key already exists
|
||||||
if await get_nwc(pubkey, None, True):
|
if await get_nwc(pubkey, None, True):
|
||||||
raise Exception("Public key already used")
|
raise Exception("Public key already used")
|
||||||
# If not, create it
|
# If not, create it
|
||||||
now=int(time.time())
|
now = int(time.time())
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO nwcprovider.keys (pubkey, wallet, description, permissions, created_at, expires_at, last_used)
|
INSERT INTO nwcprovider.keys (
|
||||||
|
pubkey,
|
||||||
|
wallet,
|
||||||
|
description,
|
||||||
|
permissions,
|
||||||
|
created_at,
|
||||||
|
expires_at,
|
||||||
|
last_used
|
||||||
|
)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(pubkey, wallet_id, description, " ".join(permissions), now,int(expires_at) if expires_at else 0, now)
|
(
|
||||||
|
pubkey,
|
||||||
|
wallet_id,
|
||||||
|
description,
|
||||||
|
" ".join(permissions),
|
||||||
|
now,
|
||||||
|
int(expires_at) if expires_at else 0,
|
||||||
|
now,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
# Add budgets
|
# Add budgets
|
||||||
if budgets:
|
if budgets:
|
||||||
for budget in budgets:
|
for budget in budgets:
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO nwcprovider.budgets (pubkey, budget_msats, refresh_window, created_at)
|
INSERT INTO nwcprovider.budgets (
|
||||||
|
pubkey,
|
||||||
|
budget_msats,
|
||||||
|
refresh_window,
|
||||||
|
created_at
|
||||||
|
)
|
||||||
VALUES (?, ?, ?, ?)
|
VALUES (?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(pubkey, budget.budget_msats, budget.refresh_window, budget.created_at)
|
(pubkey, budget.budget_msats, budget.refresh_window, budget.created_at),
|
||||||
)
|
)
|
||||||
# Return the created key
|
# Return the created key
|
||||||
return NWCKey(
|
return NWCKey(
|
||||||
|
|
@ -43,14 +68,11 @@ async def create_nwc(
|
||||||
expires_at=expires_at,
|
expires_at=expires_at,
|
||||||
permissions=" ".join(permissions),
|
permissions=" ".join(permissions),
|
||||||
created_at=now,
|
created_at=now,
|
||||||
last_used=now
|
last_used=now,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def delete_nwc(
|
async def delete_nwc(pubkey: str, wallet_id: str):
|
||||||
pubkey: str,
|
|
||||||
wallet_id:str
|
|
||||||
):
|
|
||||||
nwc = await get_nwc(pubkey, wallet_id)
|
nwc = await get_nwc(pubkey, wallet_id)
|
||||||
if not nwc:
|
if not nwc:
|
||||||
raise Exception("Public key does not exist")
|
raise Exception("Public key does not exist")
|
||||||
|
|
@ -58,17 +80,19 @@ async def delete_nwc(
|
||||||
"""
|
"""
|
||||||
DELETE FROM nwcprovider.keys WHERE pubkey = ? AND wallet = ?
|
DELETE FROM nwcprovider.keys WHERE pubkey = ? AND wallet = ?
|
||||||
""",
|
""",
|
||||||
(pubkey, wallet_id)
|
(pubkey, wallet_id),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def get_wallet_nwcs(
|
async def get_wallet_nwcs(
|
||||||
wallet_id: str,
|
wallet_id: str, include_expired: Optional[bool] = False
|
||||||
include_expired: Optional[bool] = False
|
|
||||||
) -> List[NWCKey]:
|
) -> List[NWCKey]:
|
||||||
rows = await db.fetchall(
|
rows = await db.fetchall(
|
||||||
"SELECT * FROM nwcprovider.keys WHERE wallet = ? AND (expires_at = 0 OR expires_at > ?)", (wallet_id, int(time.time()) if not include_expired else -1 )
|
"""
|
||||||
|
SELECT * FROM nwcprovider.keys
|
||||||
|
WHERE wallet = ? AND (expires_at = 0 OR expires_at > ?)
|
||||||
|
""",
|
||||||
|
(wallet_id, int(time.time()) if not include_expired else -1),
|
||||||
)
|
)
|
||||||
return [NWCKey(**row) for row in rows]
|
return [NWCKey(**row) for row in rows]
|
||||||
|
|
||||||
|
|
@ -77,16 +101,24 @@ async def get_nwc(
|
||||||
pubkey: str,
|
pubkey: str,
|
||||||
wallet_id: Optional[str] = None,
|
wallet_id: Optional[str] = None,
|
||||||
include_expired: Optional[bool] = False,
|
include_expired: Optional[bool] = False,
|
||||||
refresh_last_used: Optional[bool] = False
|
refresh_last_used: Optional[bool] = False,
|
||||||
) -> Optional[NWCKey]:
|
) -> Optional[NWCKey]:
|
||||||
# expires_at = 0 means it never expires
|
# expires_at = 0 means it never expires
|
||||||
if wallet_id:
|
if wallet_id:
|
||||||
row = await db.fetchone(
|
row = await db.fetchone(
|
||||||
"SELECT * FROM nwcprovider.keys WHERE pubkey = ? AND wallet = ? AND (expires_at = 0 OR expires_at > ?)", (pubkey, wallet_id, int(time.time()) if not include_expired else -1 )
|
"""
|
||||||
|
SELECT * FROM nwcprovider.keys
|
||||||
|
WHERE pubkey = ? AND wallet = ? AND (expires_at = 0 OR expires_at > ?)
|
||||||
|
""",
|
||||||
|
(pubkey, wallet_id, int(time.time()) if not include_expired else -1),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
row = await db.fetchone(
|
row = await db.fetchone(
|
||||||
"SELECT * FROM nwcprovider.keys WHERE pubkey = ? AND (expires_at = 0 OR expires_at > ?)", (pubkey, int(time.time()) if not include_expired else -1 )
|
"""
|
||||||
|
SELECT * FROM nwcprovider.keys
|
||||||
|
WHERE pubkey = ? AND (expires_at = 0 OR expires_at > ?)
|
||||||
|
""",
|
||||||
|
(pubkey, int(time.time()) if not include_expired else -1),
|
||||||
)
|
)
|
||||||
if not row:
|
if not row:
|
||||||
return None
|
return None
|
||||||
|
|
@ -95,7 +127,7 @@ async def get_nwc(
|
||||||
"""
|
"""
|
||||||
UPDATE nwcprovider.keys SET last_used = ? WHERE pubkey = ?
|
UPDATE nwcprovider.keys SET last_used = ? WHERE pubkey = ?
|
||||||
""",
|
""",
|
||||||
(int(time.time()), pubkey)
|
(int(time.time()), pubkey),
|
||||||
)
|
)
|
||||||
return NWCKey(**row)
|
return NWCKey(**row)
|
||||||
|
|
||||||
|
|
@ -110,47 +142,35 @@ async def get_budgets_nwc(pubkey, calculate_spent=False):
|
||||||
last_cycle, next_cycle = budget.get_timestamp_range()
|
last_cycle, next_cycle = budget.get_timestamp_range()
|
||||||
tot_spent_in_range_msats = await db.fetchone(
|
tot_spent_in_range_msats = await db.fetchone(
|
||||||
"""
|
"""
|
||||||
SELECT SUM(amount_msats) FROM nwcprovider.spent WHERE pubkey = ? AND created_at >= ? AND created_at < ?
|
SELECT SUM(amount_msats) FROM nwcprovider.spent
|
||||||
|
WHERE pubkey = ? AND created_at >= ? AND created_at < ?
|
||||||
""",
|
""",
|
||||||
(pubkey, last_cycle, next_cycle)
|
(pubkey, last_cycle, next_cycle),
|
||||||
)
|
)
|
||||||
tot_spent_in_range_msats = tot_spent_in_range_msats[0] or 0
|
tot_spent_in_range_msats = tot_spent_in_range_msats[0] or 0
|
||||||
budget.used_budget_msats = tot_spent_in_range_msats
|
budget.used_budget_msats = tot_spent_in_range_msats
|
||||||
return budgets
|
return budgets
|
||||||
|
|
||||||
|
|
||||||
# async def log_nwc(
|
async def tracked_spend_nwc(pubkey: str, amount_msats: int, action):
|
||||||
# pubkey: str,
|
|
||||||
# payload:Optional[Any] = None
|
|
||||||
# ):
|
|
||||||
# if not payload: payload=""
|
|
||||||
# payload = json.dumps(payload)
|
|
||||||
# await db.execute(
|
|
||||||
# """
|
|
||||||
# INSERT INTO nwcprovider.logs (pubkey, payload, created_at)
|
|
||||||
# VALUES (?, ?, ?)
|
|
||||||
# """,
|
|
||||||
# (pubkey, payload, int(time.time()))
|
|
||||||
# )
|
|
||||||
|
|
||||||
|
|
||||||
async def tracked_spend_nwc(
|
|
||||||
pubkey: str,
|
|
||||||
amount_msats: int,
|
|
||||||
action
|
|
||||||
):
|
|
||||||
async def r():
|
async def r():
|
||||||
created_at = int(time.time())
|
created_at = int(time.time())
|
||||||
budgets = await get_budgets_nwc(pubkey)
|
budgets = await get_budgets_nwc(pubkey)
|
||||||
in_budget = True
|
in_budget = True
|
||||||
for budget in budgets:
|
for budget in budgets:
|
||||||
last_cycle, next_cycle = budget.get_timestamp_range()
|
last_cycle, next_cycle = budget.get_timestamp_range()
|
||||||
tot_spent_in_range_msats =(await db.fetchone(
|
tot_spent_in_range_msats = (
|
||||||
|
(
|
||||||
|
await db.fetchone(
|
||||||
"""
|
"""
|
||||||
SELECT SUM(amount_msats) FROM nwcprovider.spent WHERE pubkey = ? AND created_at >= ? AND created_at < ?
|
SELECT SUM(amount_msats) FROM nwcprovider.spent
|
||||||
|
WHERE pubkey = ? AND created_at >= ? AND created_at < ?
|
||||||
""",
|
""",
|
||||||
(pubkey, last_cycle, next_cycle)
|
(pubkey, last_cycle, next_cycle),
|
||||||
))[0] or 0
|
)
|
||||||
|
)[0]
|
||||||
|
or 0
|
||||||
|
)
|
||||||
if tot_spent_in_range_msats + amount_msats > budget.budget_msats:
|
if tot_spent_in_range_msats + amount_msats > budget.budget_msats:
|
||||||
in_budget = False
|
in_budget = False
|
||||||
break
|
break
|
||||||
|
|
@ -162,40 +182,37 @@ async def tracked_spend_nwc(
|
||||||
INSERT INTO nwcprovider.spent (pubkey, amount_msats, created_at)
|
INSERT INTO nwcprovider.spent (pubkey, amount_msats, created_at)
|
||||||
VALUES (?, ?, ?)
|
VALUES (?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(pubkey, amount_msats, created_at)
|
(pubkey, amount_msats, created_at),
|
||||||
)
|
)
|
||||||
return True, out
|
return True, out
|
||||||
|
|
||||||
return await enqueue(r)
|
return await enqueue(r)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_config_nwc(key: str):
|
||||||
async def get_config_nwc(key:str):
|
row = await db.fetchone("SELECT * FROM nwcprovider.config WHERE key = ?", (key,))
|
||||||
row = await db.fetchone(
|
|
||||||
"SELECT * FROM nwcprovider.config WHERE key = ?", (key)
|
|
||||||
)
|
|
||||||
if not row:
|
if not row:
|
||||||
return None
|
return None
|
||||||
return row["value"]
|
return row["value"]
|
||||||
|
|
||||||
async def get_all_config_nwc():
|
|
||||||
rows = await db.fetchall(
|
|
||||||
"SELECT * FROM nwcprovider.config"
|
|
||||||
)
|
|
||||||
return {row["key"]:row["value"] for row in rows}
|
|
||||||
|
|
||||||
async def set_config_nwc(key:str, value:str):
|
async def get_all_config_nwc():
|
||||||
|
rows = await db.fetchall("SELECT * FROM nwcprovider.config")
|
||||||
|
return {row["key"]: row["value"] for row in rows}
|
||||||
|
|
||||||
|
|
||||||
|
async def set_config_nwc(key: str, value: str):
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
DELETE FROM nwcprovider.config
|
DELETE FROM nwcprovider.config
|
||||||
WHERE key = ?
|
WHERE key = ?
|
||||||
""",
|
""",
|
||||||
(key,)
|
(key,),
|
||||||
)
|
)
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO nwcprovider.config (key, value)
|
INSERT INTO nwcprovider.config (key, value)
|
||||||
VALUES (?, ?)
|
VALUES (?, ?)
|
||||||
""",
|
""",
|
||||||
(key, value)
|
(key, value),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
0
data/logs/debug.log
Normal file
0
data/logs/debug.log
Normal file
0
data/logs/lnbits.log
Normal file
0
data/logs/lnbits.log
Normal file
10
execution_queue.py
Normal file
10
execution_queue.py
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import asyncio
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
execution_queue: asyncio.Queue[Dict[str, Any]] = asyncio.Queue()
|
||||||
|
|
||||||
|
|
||||||
|
async def enqueue(action):
|
||||||
|
future = asyncio.Future()
|
||||||
|
execution_queue.put_nowait({"action": action, "future": future})
|
||||||
|
return await future
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"repos": [
|
"repos": [
|
||||||
{
|
{
|
||||||
"id": "nwcprovider",
|
"id": "nwcprovider",
|
||||||
"organisation": "riccardobl",
|
"organisation": "lnbits",
|
||||||
"repository": "nwcprovider"
|
"repository": "nwcprovider"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import secp256k1
|
import secp256k1
|
||||||
|
|
||||||
|
|
||||||
async def m001_initial(db):
|
async def m001_initial(db):
|
||||||
"""
|
"""
|
||||||
Initial tables
|
Initial tables
|
||||||
|
|
@ -24,7 +25,9 @@ async def m001_initial(db):
|
||||||
pubkey TEXT NOT NULL,
|
pubkey TEXT NOT NULL,
|
||||||
amount_msats INTEGER NOT NULL,
|
amount_msats INTEGER NOT NULL,
|
||||||
created_at INTEGER NOT NULL,
|
created_at INTEGER NOT NULL,
|
||||||
FOREIGN KEY(pubkey) REFERENCES {db.references_schema}keys(pubkey) ON DELETE CASCADE
|
FOREIGN KEY(pubkey)
|
||||||
|
REFERENCES {db.references_schema}keys(pubkey)
|
||||||
|
ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
@ -36,7 +39,9 @@ async def m001_initial(db):
|
||||||
pubkey TEXT NOT NULL,
|
pubkey TEXT NOT NULL,
|
||||||
payload TEXT NOT NULL,
|
payload TEXT NOT NULL,
|
||||||
created_at INTEGER NOT NULL,
|
created_at INTEGER NOT NULL,
|
||||||
FOREIGN KEY(pubkey) REFERENCES {db.references_schema}keys(pubkey) ON DELETE CASCADE
|
FOREIGN KEY(pubkey)
|
||||||
|
REFERENCES {db.references_schema}keys(pubkey)
|
||||||
|
ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
@ -49,13 +54,14 @@ async def m001_initial(db):
|
||||||
budget_msats INTEGER NOT NULL,
|
budget_msats INTEGER NOT NULL,
|
||||||
refresh_window INTEGER NOT NULL,
|
refresh_window INTEGER NOT NULL,
|
||||||
created_at INTEGER NOT NULL,
|
created_at INTEGER NOT NULL,
|
||||||
FOREIGN KEY(pubkey) REFERENCES {db.references_schema}keys(pubkey) ON DELETE CASCADE
|
FOREIGN KEY(pubkey)
|
||||||
|
REFERENCES {db.references_schema}keys(pubkey)
|
||||||
|
ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def m002_config(db):
|
async def m002_config(db):
|
||||||
"""
|
"""
|
||||||
Config table
|
Config table
|
||||||
|
|
@ -69,6 +75,7 @@ async def m002_config(db):
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def m003_default_config(db):
|
async def m003_default_config(db):
|
||||||
"""
|
"""
|
||||||
Default config
|
Default config
|
||||||
|
|
@ -83,11 +90,10 @@ async def m003_default_config(db):
|
||||||
"""
|
"""
|
||||||
INSERT INTO nwcprovider.config (key, value) VALUES ('provider_key', ?);
|
INSERT INTO nwcprovider.config (key, value) VALUES ('provider_key', ?);
|
||||||
""",
|
""",
|
||||||
(new_private_key,)
|
(new_private_key,),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def m004_default_config2(db):
|
async def m004_default_config2(db):
|
||||||
"""
|
"""
|
||||||
Default config
|
Default config
|
||||||
|
|
@ -97,7 +103,7 @@ async def m004_default_config2(db):
|
||||||
"""
|
"""
|
||||||
INSERT INTO nwcprovider.config (key, value) VALUES ('relay_alias', ?);
|
INSERT INTO nwcprovider.config (key, value) VALUES ('relay_alias', ?);
|
||||||
""",
|
""",
|
||||||
('',)
|
("",),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
32
models.py
32
models.py
|
|
@ -1,12 +1,10 @@
|
||||||
# Data models for your extension
|
# Data models for your extension
|
||||||
|
|
||||||
from sqlite3 import Row
|
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
import time
|
import time
|
||||||
import json
|
from sqlite3 import Row
|
||||||
from typing import List, Dict, Any
|
from typing import Any, Dict, List
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class NWCKey(BaseModel):
|
class NWCKey(BaseModel):
|
||||||
|
|
@ -18,17 +16,17 @@ class NWCKey(BaseModel):
|
||||||
created_at: int
|
created_at: int
|
||||||
last_used: int
|
last_used: int
|
||||||
|
|
||||||
def getPermissions(cls) -> List[str]:
|
def get_permissions(self) -> List[str]:
|
||||||
try:
|
try:
|
||||||
return cls.permissions.split(" ")
|
return self.permissions.split(" ")
|
||||||
except:
|
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)
|
||||||
|
|
||||||
|
|
||||||
class NWCBudget(BaseModel):
|
class NWCBudget(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
pubkey: str
|
pubkey: str
|
||||||
|
|
@ -37,16 +35,16 @@ class NWCBudget(BaseModel):
|
||||||
created_at: int
|
created_at: int
|
||||||
used_budget_msats: int = 0
|
used_budget_msats: int = 0
|
||||||
|
|
||||||
def get_timestamp_range(cls):
|
def get_timestamp_range(self) -> tuple[int, int]:
|
||||||
c = int(time.time())
|
c = int(time.time())
|
||||||
if cls.refresh_window <= 0: # never refresh
|
if self.refresh_window <= 0: # never refresh
|
||||||
# return a timestamp in the future
|
# return a timestamp in the future
|
||||||
return c + 21000000
|
return c, c + 21000000
|
||||||
# calculate the next refresh timestamp
|
# calculate the next refresh timestamp
|
||||||
elapsed = c - cls.created_at
|
elapsed = c - self.created_at
|
||||||
passed_cycles = elapsed // cls.refresh_window
|
passed_cycles = elapsed // self.refresh_window
|
||||||
last_cycle = cls.created_at + (passed_cycles * cls.refresh_window)
|
last_cycle = self.created_at + (passed_cycles * self.refresh_window)
|
||||||
next_cycle = last_cycle + cls.refresh_window
|
next_cycle = last_cycle + self.refresh_window
|
||||||
return last_cycle, next_cycle
|
return last_cycle, next_cycle
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -81,5 +79,3 @@ class NWCRegistrationRequest(BaseModel):
|
||||||
class NWCGetResponse(BaseModel):
|
class NWCGetResponse(BaseModel):
|
||||||
data: NWCKey
|
data: NWCKey
|
||||||
budgets: List[NWCBudget]
|
budgets: List[NWCBudget]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,58 +1,59 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
from typing import Dict
|
import random
|
||||||
import secp256k1
|
|
||||||
from loguru import logger
|
|
||||||
from lnbits.settings import settings
|
|
||||||
import time
|
import time
|
||||||
import websockets
|
from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple, Union
|
||||||
|
|
||||||
|
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
|
||||||
import base64
|
|
||||||
import random
|
|
||||||
from typing import Union, List, Callable, Tuple
|
|
||||||
from lnbits.app import settings
|
|
||||||
from lnbits.helpers import encrypt_internal_message
|
|
||||||
from urllib.parse import quote
|
|
||||||
from Cryptodome.Util.Padding import pad, unpad
|
from Cryptodome.Util.Padding import pad, unpad
|
||||||
from typing import List, Dict, Optional
|
from lnbits.helpers import encrypt_internal_message
|
||||||
|
from lnbits.settings import settings
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
|
||||||
class MainSubscription:
|
class MainSubscription:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.requests_sub_id = None
|
self.requests_sub_id: Optional[str] = None
|
||||||
self.responses_sub_id = None
|
self.responses_sub_id: Optional[str] = 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 getStale(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.
|
||||||
"""
|
"""
|
||||||
pending_events = []
|
pending_events = []
|
||||||
for [id, event] in self.events.items():
|
for [event_id, event] in self.events.items():
|
||||||
if not id in self.responses:
|
if event_id not in self.responses:
|
||||||
pending_events.append(event)
|
pending_events.append(event)
|
||||||
return pending_events
|
return pending_events
|
||||||
|
|
||||||
def registerResponse(self, event_id:str):
|
def register_response(self, event_id: str):
|
||||||
"""
|
"""
|
||||||
Register a response for a request event (not stale anymore)
|
Register a response for a request event (not stale anymore)
|
||||||
"""
|
"""
|
||||||
if not event_id in self.responses:
|
if event_id not in self.responses:
|
||||||
self.responses.append(event_id)
|
self.responses.append(event_id)
|
||||||
|
|
||||||
|
|
||||||
class NWCServiceProvider:
|
class NWCServiceProvider:
|
||||||
def __init__(self, private_key:str=None, relay:str=None):
|
def __init__(self, private_key: Optional[str] = None, relay: Optional[str] = None):
|
||||||
if not relay: # Connect to nostrclient
|
if not relay: # Connect to nostrclient
|
||||||
relay = "nostrclient"
|
relay = "nostrclient"
|
||||||
if relay == "nostrclient":
|
if relay == "nostrclient":
|
||||||
relay=f"ws://localhost:{settings.port}/nostrclient/api/v1/relay"
|
relay = f"ws://localhost:{settings.port}/nostrclient/api/v1/relay"
|
||||||
elif relay == "nostrclient:private":
|
elif relay == "nostrclient:private":
|
||||||
relay_endpoint = encrypt_internal_message("relay")
|
relay_endpoint = encrypt_internal_message("relay")
|
||||||
relay=f"ws://localhost:{settings.port}/nostrclient/api/v1/{relay_endpoint}"
|
relay = (
|
||||||
|
f"ws://localhost:{settings.port}/nostrclient/api/v1/{relay_endpoint}"
|
||||||
|
)
|
||||||
self.relay = relay
|
self.relay = relay
|
||||||
|
|
||||||
if not private_key: # Create random key
|
if not private_key: # Create random key
|
||||||
|
|
@ -61,16 +62,24 @@ class NWCServiceProvider:
|
||||||
self.private_key = secp256k1.PrivateKey(bytes.fromhex(private_key))
|
self.private_key = secp256k1.PrivateKey(bytes.fromhex(private_key))
|
||||||
self.private_key_hex = private_key
|
self.private_key_hex = private_key
|
||||||
self.public_key = self.private_key.pubkey
|
self.public_key = self.private_key.pubkey
|
||||||
|
if not self.public_key:
|
||||||
|
raise Exception("Invalid public key")
|
||||||
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 = []
|
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 = 0
|
self.subscriptions_count: int = 0
|
||||||
|
|
||||||
# Request listeners, listen to specific methods
|
# Request listeners, listen to specific methods
|
||||||
self.request_listeners = {}
|
self.request_listeners: Dict[
|
||||||
|
str,
|
||||||
|
Callable[
|
||||||
|
["NWCServiceProvider", str, Dict],
|
||||||
|
Awaitable[List[Tuple[Optional[Dict], Optional[Dict], List]]],
|
||||||
|
],
|
||||||
|
] = {}
|
||||||
|
|
||||||
# Reconnect task (if the connection is lost)
|
# Reconnect task (if the connection is lost)
|
||||||
self.reconnect_task = None
|
self.reconnect_task = None
|
||||||
|
|
@ -87,26 +96,39 @@ class NWCServiceProvider:
|
||||||
# if True the instance is shutting down
|
# if True the instance is shutting down
|
||||||
self.shutdown = False
|
self.shutdown = False
|
||||||
|
|
||||||
logger.info("NWC Service is ready. relay: "+str(self.relay)+" pubkey: " +
|
logger.info(
|
||||||
self.public_key_hex)
|
"NWC Service is ready. relay: "
|
||||||
|
+ str(self.relay)
|
||||||
|
+ " pubkey: "
|
||||||
|
+ self.public_key_hex
|
||||||
|
)
|
||||||
|
|
||||||
def getSupportedMethods(self):
|
def get_supported_methods(self):
|
||||||
"""
|
"""
|
||||||
Returns the list of supported methods by this service provider.
|
Returns the list of supported methods by this service provider.
|
||||||
"""
|
"""
|
||||||
return self.supported_methods
|
return self.supported_methods
|
||||||
|
|
||||||
def addRequestListener(self, method: str, l: Callable[["NWCServiceProvider", str, Dict], List[Tuple[Dict, Dict, List]]]):
|
def add_request_listener(
|
||||||
|
self,
|
||||||
|
method: str,
|
||||||
|
listener: Callable[
|
||||||
|
["NWCServiceProvider", str, Dict],
|
||||||
|
Awaitable[List[Tuple[Optional[Dict], Optional[Dict], List]]],
|
||||||
|
],
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Adds a request listener for a specific method.
|
Adds a request listener for a specific method.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
method (str): The method name.
|
method (str): The method name.
|
||||||
l (Callable[["NWCServiceProvider", str, Dict], List[Tuple[Dict, Dict]]]): The listener function
|
listener (Callable[
|
||||||
|
["NWCServiceProvider", str, Dict], List[Tuple[Dict, Dict]]
|
||||||
|
]): The listener function
|
||||||
"""
|
"""
|
||||||
if not method in self.supported_methods:
|
if method not in self.supported_methods:
|
||||||
self.supported_methods.append(method)
|
self.supported_methods.append(method)
|
||||||
self.request_listeners[method] = l
|
self.request_listeners[method] = listener
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -114,7 +136,6 @@ class NWCServiceProvider:
|
||||||
"""
|
"""
|
||||||
self.reconnect_task = asyncio.create_task(self._connect_to_relay())
|
self.reconnect_task = asyncio.create_task(self._connect_to_relay())
|
||||||
|
|
||||||
|
|
||||||
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.
|
||||||
|
|
@ -127,8 +148,7 @@ class NWCServiceProvider:
|
||||||
"""
|
"""
|
||||||
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 _is_shutting_down(self) -> bool:
|
def _is_shutting_down(self) -> bool:
|
||||||
"""
|
"""
|
||||||
|
|
@ -136,14 +156,15 @@ 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: Dict):
|
|
||||||
"""
|
"""
|
||||||
Sends data to the relay.
|
Sends data to the relay.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
data (Dict): The data to be sent.
|
data (Dict): The data to be sent.
|
||||||
"""
|
"""
|
||||||
|
if not self.ws:
|
||||||
|
raise Exception("Websocket connection is not established")
|
||||||
if self._is_shutting_down():
|
if self._is_shutting_down():
|
||||||
logger.warning("Trying to send data while shutting down")
|
logger.warning("Trying to send data while shutting down")
|
||||||
return
|
return
|
||||||
|
|
@ -151,7 +172,6 @@ class NWCServiceProvider:
|
||||||
tx = self._json_dumps(data)
|
tx = self._json_dumps(data)
|
||||||
await self.ws.send(tx)
|
await self.ws.send(tx)
|
||||||
|
|
||||||
|
|
||||||
def _get_new_subid(self) -> str:
|
def _get_new_subid(self) -> str:
|
||||||
"""
|
"""
|
||||||
Generates a unique subscription id.
|
Generates a unique subscription id.
|
||||||
|
|
@ -159,17 +179,16 @@ class NWCServiceProvider:
|
||||||
Returns:
|
Returns:
|
||||||
str: The generated 64 characters long subscription id (eg. lnbits0abc...)
|
str: The generated 64 characters long subscription id (eg. lnbits0abc...)
|
||||||
"""
|
"""
|
||||||
subid = "lnbitsnwcs"+str(self.subscriptions_count)
|
subid = "lnbitsnwcs" + str(self.subscriptions_count)
|
||||||
self.subscriptions_count += 1
|
self.subscriptions_count += 1
|
||||||
maxLength = 64
|
max_length = 64
|
||||||
chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
n = maxLength - len(subid)
|
n = max_length - len(subid)
|
||||||
if n > 0:
|
if n > 0:
|
||||||
for i in range(n):
|
for _ in range(n):
|
||||||
subid += chars[random.randint(0, len(chars) - 1)]
|
subid += chars[random.randint(0, len(chars) - 1)]
|
||||||
return subid
|
return subid
|
||||||
|
|
||||||
|
|
||||||
async def _wait_for_connection(self):
|
async def _wait_for_connection(self):
|
||||||
"""
|
"""
|
||||||
Waits until the connection is established.
|
Waits until the connection is established.
|
||||||
|
|
@ -180,7 +199,6 @@ class NWCServiceProvider:
|
||||||
logger.debug("Waiting for connection...")
|
logger.debug("Waiting for connection...")
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
async def _subscribe(self):
|
async def _subscribe(self):
|
||||||
"""
|
"""
|
||||||
[Re]Subscribe to receive nip 47 requests and responses from the relay
|
[Re]Subscribe to receive nip 47 requests and responses from the relay
|
||||||
|
|
@ -191,40 +209,37 @@ class NWCServiceProvider:
|
||||||
"kinds": [23194],
|
"kinds": [23194],
|
||||||
"#p": [self.public_key_hex],
|
"#p": [self.public_key_hex],
|
||||||
# Since the last 3 hours (handles reboots)
|
# Since the last 3 hours (handles reboots)
|
||||||
"since": int(time.time()) - 3*60*60
|
"since": int(time.time()) - 3 * 60 * 60,
|
||||||
}
|
}
|
||||||
self.sub.requests_sub_id = self._get_new_subid()
|
self.sub.requests_sub_id = self._get_new_subid()
|
||||||
# Create responses subscription (needed to track previosly responded requests)
|
# Create responses subscription (needed to track previosly responded requests)
|
||||||
res_filter = {
|
res_filter = {
|
||||||
"kinds": [23195],
|
"kinds": [23195],
|
||||||
"authors": [self.public_key_hex],
|
"authors": [self.public_key_hex],
|
||||||
"since": int(time.time()) - 3*60*60
|
"since": int(time.time()) - 3 * 60 * 60,
|
||||||
}
|
}
|
||||||
self.sub.responses_sub_id = self._get_new_subid()
|
self.sub.responses_sub_id = self._get_new_subid()
|
||||||
# Subscribe
|
# Subscribe
|
||||||
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,ws):
|
|
||||||
"""
|
"""
|
||||||
On connection callback, announce the service provider methods and subscribe to nip67 events.
|
On connection callback, announce the service provider
|
||||||
|
methods and subscribe to nip67 events.
|
||||||
"""
|
"""
|
||||||
# Send info event
|
# Send info event
|
||||||
event = {
|
event = {
|
||||||
"kind": 13194,
|
"kind": 13194,
|
||||||
"content": " ".join(self.supported_methods),
|
"content": " ".join(self.supported_methods),
|
||||||
"created_at": int(time.time()),
|
"created_at": int(time.time()),
|
||||||
"tags": [
|
"tags": [["p", self.public_key_hex]],
|
||||||
["p", self.public_key_hex]
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
self._sign_event(event)
|
self._sign_event(event)
|
||||||
await self._send(["EVENT", event])
|
await self._send(["EVENT", event])
|
||||||
# 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
|
||||||
|
|
@ -237,34 +252,30 @@ class NWCServiceProvider:
|
||||||
content = json.loads(content)
|
content = json.loads(content)
|
||||||
# Handle request
|
# Handle request
|
||||||
method = content["method"]
|
method = content["method"]
|
||||||
l = self.request_listeners.get(method, None)
|
listener = self.request_listeners.get(method, None)
|
||||||
outs = []
|
outs: List[Dict[str, Any]] = []
|
||||||
if not l:
|
if not listener:
|
||||||
outs.append({
|
outs.append(
|
||||||
|
{
|
||||||
"error": {
|
"error": {
|
||||||
"code": "NOT_IMPLEMENTED",
|
"code": "NOT_IMPLEMENTED",
|
||||||
"message": "Method "+method+" is not implemented by this service provider"
|
"message": "Method "
|
||||||
|
+ method
|
||||||
|
+ " is not implemented by this service provider",
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
results = await l(self, nwc_pubkey, content)
|
results = await listener(self, nwc_pubkey, content)
|
||||||
for result in results:
|
for result in results:
|
||||||
r = result[0]
|
r = result[0]
|
||||||
e = result[1]
|
e = result[1]
|
||||||
t = result[2] if len(result) > 2 else None
|
t = result[2] if len(result) > 2 else None
|
||||||
out = {}
|
out = {"result": r, "error": e, "tags": t}
|
||||||
if r: out["result"] = r
|
|
||||||
if e: out["error"] = e
|
|
||||||
if t: out["tags"] = t
|
|
||||||
outs.append(out)
|
outs.append(out)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
outs.append({
|
outs.append({"error": {"code": "INTERNAL", "message": str(e)}})
|
||||||
"error":{
|
|
||||||
"code": "INTERNAL",
|
|
||||||
"message": str(e)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
sent_events = []
|
sent_events = []
|
||||||
for out in outs:
|
for out in outs:
|
||||||
# Finalize output
|
# Finalize output
|
||||||
|
|
@ -275,7 +286,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 = {
|
res: Dict = {
|
||||||
"kind": 23195,
|
"kind": 23195,
|
||||||
"created_at": int(time.time()),
|
"created_at": int(time.time()),
|
||||||
"tags": out.get("tags", []),
|
"tags": out.get("tags", []),
|
||||||
|
|
@ -286,16 +297,94 @@ class NWCServiceProvider:
|
||||||
# Reference user
|
# Reference user
|
||||||
res["tags"].append(["p", nwc_pubkey])
|
res["tags"].append(["p", nwc_pubkey])
|
||||||
# Finalize response event
|
# Finalize response event
|
||||||
|
print(res)
|
||||||
res["content"] = self._encrypt_content(res["content"], nwc_pubkey)
|
res["content"] = self._encrypt_content(res["content"], nwc_pubkey)
|
||||||
self._sign_event(res)
|
self._sign_event(res)
|
||||||
|
|
||||||
# Register response for this request, so we knows it is not stale
|
# Register response for this request, so we knows it is not stale
|
||||||
if self.sub: self.sub.registerResponse(event["id"])
|
if self.sub:
|
||||||
|
self.sub.register_response(event["id"])
|
||||||
# Send response event
|
# Send response event
|
||||||
await self._send(["EVENT", res])
|
await self._send(["EVENT", res])
|
||||||
# Track sent events
|
# Track sent events
|
||||||
sent_events.append(res)
|
sent_events.append(res)
|
||||||
return sent_events
|
return sent_events
|
||||||
|
|
||||||
|
async def _on_event_message(self, msg):
|
||||||
|
if not self.sub:
|
||||||
|
return
|
||||||
|
sub_id = msg[1]
|
||||||
|
event = msg[2]
|
||||||
|
# Ensure the event is valid (do not trust relays)
|
||||||
|
if not self._verify_event(event):
|
||||||
|
raise Exception("Invalid event signature")
|
||||||
|
tags = event["tags"]
|
||||||
|
expiration = int(next((tag for tag in tags if tag[0] == "expiration"), -1))
|
||||||
|
# Handle event expiration if the relay doesn't support nip 40
|
||||||
|
if expiration > 0 and expiration < int(time.time()):
|
||||||
|
logger.debug("Event expired")
|
||||||
|
return
|
||||||
|
if event["kind"] == 23194 and sub_id == self.sub.requests_sub_id:
|
||||||
|
# Ensure the request is for this service provider
|
||||||
|
valid_p = any(
|
||||||
|
tag[0] == "p" and tag[1] == self.public_key_hex for tag in tags
|
||||||
|
)
|
||||||
|
if not valid_p:
|
||||||
|
raise Exception("Unexpected request from another service")
|
||||||
|
# Track request
|
||||||
|
self.sub.events[event["id"]] = event
|
||||||
|
# if eose was received for both subscriptions, we handle the request
|
||||||
|
# in realtime if not, we do nothing since the request may be
|
||||||
|
# already handled or stale, all stale requests will be handled
|
||||||
|
# later when eose is received
|
||||||
|
if self.sub.requests_eose and self.sub.responses_eose:
|
||||||
|
await self._handle_request(event)
|
||||||
|
elif event["kind"] == 23195 and sub_id == self.sub.responses_sub_id:
|
||||||
|
# Ensure the response is from this service provider
|
||||||
|
if event["pubkey"] != self.public_key_hex:
|
||||||
|
raise Exception("Unexpected response from another service")
|
||||||
|
# Register as response for each e tag (request event id)
|
||||||
|
# Note: usually we expect only one "e" tag, but we are handling
|
||||||
|
# multiple "e" tags just in case
|
||||||
|
etag = next((tag[1] for tag in tags if tag[0] == "e"), None)
|
||||||
|
if etag:
|
||||||
|
self.sub.register_response(etag)
|
||||||
|
|
||||||
|
async def _on_eose_message(self, msg):
|
||||||
|
if not self.sub:
|
||||||
|
return
|
||||||
|
sub_id = msg[1]
|
||||||
|
# Track EOSE
|
||||||
|
if sub_id == self.sub.requests_sub_id:
|
||||||
|
self.sub.requests_eose = True
|
||||||
|
elif sub_id == self.sub.responses_sub_id:
|
||||||
|
self.sub.responses_eose = True
|
||||||
|
# When both EOSE are receives, handle all the stale requests
|
||||||
|
# Note: All the requests that were received prior to the
|
||||||
|
# service connection and do not have a response yet,
|
||||||
|
# are considered stale, we will process them now
|
||||||
|
if self.sub.requests_eose and self.sub.responses_eose:
|
||||||
|
stales = self.sub.get_stale()
|
||||||
|
for stale in stales:
|
||||||
|
await self._handle_request(stale)
|
||||||
|
|
||||||
|
async def _on_closed_message(self, msg):
|
||||||
|
if not self.sub:
|
||||||
|
return
|
||||||
|
# Subscription was closed remotely.
|
||||||
|
sub_id = msg[1]
|
||||||
|
info = msg[2] or "" if len(msg) > 2 else ""
|
||||||
|
# Resubscribe if one of the main subscriptions was closed
|
||||||
|
if sub_id == self.sub.requests_sub_id or sub_id == self.sub.responses_sub_id:
|
||||||
|
logger.warning(
|
||||||
|
"Subscription "
|
||||||
|
+ sub_id
|
||||||
|
+ " was closed remotely: "
|
||||||
|
+ info
|
||||||
|
+ " ... resubscribing..."
|
||||||
|
)
|
||||||
|
await self._subscribe()
|
||||||
|
|
||||||
async def _on_message(self, ws, message: str):
|
async def _on_message(self, ws, message: str):
|
||||||
"""
|
"""
|
||||||
Handle incoming messages from the relay.
|
Handle incoming messages from the relay.
|
||||||
|
|
@ -303,113 +392,63 @@ class NWCServiceProvider:
|
||||||
try:
|
try:
|
||||||
msg = json.loads(message)
|
msg = json.loads(message)
|
||||||
if msg[0] == "EVENT": # Event message
|
if msg[0] == "EVENT": # Event message
|
||||||
sub_id = msg[1]
|
await self._on_event_message(msg)
|
||||||
event = msg[2]
|
|
||||||
# Ensure the event is valid (do not trust relays)
|
|
||||||
if not self._verify_event(event):
|
|
||||||
raise Exception("Invalid event signature")
|
|
||||||
tags = event["tags"]
|
|
||||||
expiration = -1
|
|
||||||
for tag in tags:
|
|
||||||
if tag[0] == "expiration":
|
|
||||||
expiration = int(tag[1])
|
|
||||||
break
|
|
||||||
# Handle event expiration if the relay doesn't support nip 40
|
|
||||||
if expiration > 0 and expiration < int(time.time()):
|
|
||||||
logger.debug("Event expired")
|
|
||||||
return
|
|
||||||
if event["kind"] == 23194 and sub_id == self.sub.requests_sub_id:
|
|
||||||
# Ensure the request is for this service provider
|
|
||||||
valid_p = False
|
|
||||||
for tag in tags:
|
|
||||||
if tag[0] == "p" and tag[1] == self.public_key_hex:
|
|
||||||
valid_p = True
|
|
||||||
break
|
|
||||||
if not valid_p:
|
|
||||||
raise Exception("Unexpected request from another service")
|
|
||||||
# Track request
|
|
||||||
self.sub.events[event["id"]] = event
|
|
||||||
# if eose was received for both subscriptions, we handle the request in realtime
|
|
||||||
# if not, we do nothing since the request may be already handled or stale,
|
|
||||||
# all stale requests will be handled later when eose is received
|
|
||||||
if self.sub.requests_eose and self.sub.responses_eose:
|
|
||||||
await self._handle_request(event)
|
|
||||||
elif event["kind"] == 23195 and sub_id == self.sub.responses_sub_id:
|
|
||||||
# Ensure the response is from this service provider
|
|
||||||
if event["pubkey"] != self.public_key_hex:
|
|
||||||
raise Exception("Unexpected response from another service")
|
|
||||||
# Register as response for each e tag (request event id)
|
|
||||||
# Note: usually we expect only one "e" tag, but we are handling multiple "e" tags just in case
|
|
||||||
for tag in tags:
|
|
||||||
if tag[0] == "e":
|
|
||||||
self.sub.registerResponse(tag[1])
|
|
||||||
elif msg[0] == "EOSE":
|
elif msg[0] == "EOSE":
|
||||||
sub_id = msg[1]
|
await self._on_eose_message(msg)
|
||||||
# Track EOSE
|
|
||||||
if sub_id == self.sub.requests_sub_id:
|
|
||||||
self.sub.requests_eose = True
|
|
||||||
elif sub_id == self.sub.responses_sub_id:
|
|
||||||
self.sub.responses_eose = True
|
|
||||||
# When both EOSE are receives, handle all the stale requests
|
|
||||||
# Note: All the requests that were received prior to the service connection
|
|
||||||
# and do not have a response yet, are considered stale, we will process them now
|
|
||||||
if self.sub.requests_eose and self.sub.responses_eose:
|
|
||||||
stales = self.sub.getStale()
|
|
||||||
for stale in stales:
|
|
||||||
await self._handle_request(stale)
|
|
||||||
elif msg[0] == "CLOSED":
|
elif msg[0] == "CLOSED":
|
||||||
# Subscription was closed remotely.
|
await self._on_closed_message(msg)
|
||||||
sub_id = msg[1]
|
|
||||||
info = msg[2] or "" if len(msg) > 2 else ""
|
|
||||||
# Resubscribe if one of the main subscriptions was closed
|
|
||||||
if sub_id == self.sub.requests_sub_id or sub_id == self.sub.responses_sub_id:
|
|
||||||
logger.warning("Subscription "+sub_id+" was closed remotely: "+info+" ... resubscribing...")
|
|
||||||
self._subscribe()
|
|
||||||
elif msg[0] == "NOTICE":
|
elif msg[0] == "NOTICE":
|
||||||
# A message from the relay, mostly useless, but we log it anyway
|
# A message from the relay, mostly useless, but we log it anyway
|
||||||
logger.info("Notice from relay "+self.relay+": "+str(msg[1]))
|
logger.info("Notice from relay " + self.relay + ": " + str(msg[1]))
|
||||||
elif msg[0] == "OK":
|
elif msg[0] == "OK":
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
raise Exception("Unknown message type")
|
raise Exception("Unknown message type")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Error parsing event: "+str(e))
|
logger.error("Error parsing event: " + str(e))
|
||||||
|
|
||||||
|
|
||||||
async def _connect_to_relay(self):
|
async def _connect_to_relay(self):
|
||||||
"""
|
"""
|
||||||
Initiate websocket connection to the relay.
|
Initiate websocket connection to the relay.
|
||||||
"""
|
"""
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
logger.debug("Connecting to NWC relay "+self.relay)
|
logger.debug("Connecting to NWC relay " + self.relay)
|
||||||
while not self._is_shutting_down(): # Reconnect until the instance is shutting down
|
while (
|
||||||
logger.debug('Creating new connection...')
|
not self._is_shutting_down()
|
||||||
|
): # Reconnect until the instance is shutting down
|
||||||
|
logger.debug("Creating new connection...")
|
||||||
try:
|
try:
|
||||||
async with websockets.connect(self.relay) as ws:
|
async with websockets.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)
|
||||||
while not self._is_shutting_down(): # receive messages until the instance is shutting down
|
while (
|
||||||
|
not self._is_shutting_down()
|
||||||
|
): # receive messages until the instance is shutting down
|
||||||
try:
|
try:
|
||||||
reply = await ws.recv()
|
reply = await ws.recv()
|
||||||
|
if isinstance(reply, bytes):
|
||||||
|
reply = reply.decode("utf-8")
|
||||||
await self._on_message(ws, reply)
|
await self._on_message(ws, reply)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug("Error receiving message: " + str(e))
|
logger.debug("Error receiving message: " + str(e))
|
||||||
break
|
break
|
||||||
logger.debug("Connection to NWC relay closed")
|
logger.debug("Connection to NWC relay closed")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Error connecting to NWC relay: "+str(e))
|
logger.error("Error connecting to NWC relay: " + str(e))
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
# the connection was closed, so we set the connected flag to False
|
# the connection was closed, so we set the connected flag to False
|
||||||
# this will make the methods calling _wait_for_connection() to wait until the connection is re-established
|
# this will make the methods calling _wait_for_connection() to wait
|
||||||
|
# until the connection is re-established
|
||||||
self.connected = False
|
self.connected = False
|
||||||
if not self._is_shutting_down():
|
if not self._is_shutting_down():
|
||||||
# Wait some time before reconnecting
|
# Wait some time before reconnecting
|
||||||
logger.debug("Reconnecting to NWC relay in 5 seconds...")
|
logger.debug("Reconnecting to NWC relay in 5 seconds...")
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
|
def _encrypt_content(
|
||||||
def _encrypt_content(self, content: str, pubkey_hex:str, iv_seed: Optional[int]=None) -> str:
|
self, content: str, pubkey_hex: str, iv_seed: Optional[int] = None
|
||||||
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Encrypts the content for the given public key
|
Encrypts the content for the given public key
|
||||||
|
|
||||||
|
|
@ -420,16 +459,14 @@ class NWCServiceProvider:
|
||||||
Returns:
|
Returns:
|
||||||
str: The encrypted content.
|
str: The encrypted content.
|
||||||
"""
|
"""
|
||||||
pubkey = secp256k1.PublicKey(
|
pubkey = secp256k1.PublicKey(bytes.fromhex("02" + pubkey_hex), True)
|
||||||
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:]
|
|
||||||
# random iv (16B)
|
# random iv (16B)
|
||||||
if not iv_seed:
|
if not iv_seed:
|
||||||
iv = Random.new().read(AES.block_size)
|
iv = Random.new().read(AES.block_size)
|
||||||
else:
|
else:
|
||||||
iv = hashlib.sha256(iv_seed.to_bytes(32, byteorder='big')).digest()
|
iv = hashlib.sha256(iv_seed.to_bytes(32, byteorder="big")).digest()
|
||||||
iv = iv[:AES.block_size]
|
iv = iv[: AES.block_size]
|
||||||
|
|
||||||
aes = AES.new(shared, AES.MODE_CBC, iv)
|
aes = AES.new(shared, AES.MODE_CBC, iv)
|
||||||
|
|
||||||
|
|
@ -438,14 +475,12 @@ class NWCServiceProvider:
|
||||||
# padding
|
# padding
|
||||||
content_bytes = pad(content_bytes, AES.block_size)
|
content_bytes = pad(content_bytes, AES.block_size)
|
||||||
|
|
||||||
encrypted_b64 = base64.b64encode(
|
encrypted_b64 = base64.b64encode(aes.encrypt(content_bytes)).decode("ascii")
|
||||||
aes.encrypt(content_bytes)).decode("ascii")
|
iv_b64 = base64.b64encode(iv).decode("ascii")
|
||||||
ivB64 = base64.b64encode(iv).decode("ascii")
|
encrypted_content = encrypted_b64 + "?iv=" + iv_b64
|
||||||
encrypted_content = encrypted_b64 + "?iv=" + ivB64
|
|
||||||
return encrypted_content
|
return encrypted_content
|
||||||
|
|
||||||
|
def _decrypt_content(self, content: str, pubkey_hex: str) -> str:
|
||||||
def _decrypt_content(self, content: str , pubkey_hex:str) -> str:
|
|
||||||
"""
|
"""
|
||||||
Decrypts the content for the given public key
|
Decrypts the content for the given public key
|
||||||
|
|
||||||
|
|
@ -456,15 +491,12 @@ class NWCServiceProvider:
|
||||||
Returns:
|
Returns:
|
||||||
str: The decrypted content.
|
str: The decrypted content.
|
||||||
"""
|
"""
|
||||||
pubkey = secp256k1.PublicKey(
|
pubkey = secp256k1.PublicKey(bytes.fromhex("02" + pubkey_hex), True)
|
||||||
bytes.fromhex("02" + pubkey_hex), True)
|
|
||||||
|
|
||||||
shared = pubkey.tweak_mul(bytes.fromhex(
|
shared = pubkey.tweak_mul(bytes.fromhex(self.private_key_hex)).serialize()[1:]
|
||||||
self.private_key_hex)).serialize()[1:]
|
|
||||||
# extract iv and content
|
# extract iv and content
|
||||||
(encrypted_content_b64, iv_b64) = content.split("?iv=")
|
(encrypted_content_b64, iv_b64) = content.split("?iv=")
|
||||||
encrypted_content = base64.b64decode(
|
encrypted_content = base64.b64decode(encrypted_content_b64.encode("ascii"))
|
||||||
encrypted_content_b64.encode("ascii"))
|
|
||||||
iv = base64.b64decode(iv_b64.encode("ascii"))
|
iv = base64.b64decode(iv_b64.encode("ascii"))
|
||||||
# Decrypt
|
# Decrypt
|
||||||
aes = AES.new(shared, AES.MODE_CBC, iv)
|
aes = AES.new(shared, AES.MODE_CBC, iv)
|
||||||
|
|
@ -473,7 +505,6 @@ 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
|
||||||
|
|
@ -484,24 +515,27 @@ class NWCServiceProvider:
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if the event signature is valid, False otherwise.
|
bool: True if the event signature is valid, False otherwise.
|
||||||
"""
|
"""
|
||||||
signature_data = self._json_dumps([
|
signature_data = self._json_dumps(
|
||||||
|
[
|
||||||
0,
|
0,
|
||||||
event["pubkey"],
|
event["pubkey"],
|
||||||
event["created_at"],
|
event["created_at"],
|
||||||
event["kind"],
|
event["kind"],
|
||||||
event["tags"],
|
event["tags"],
|
||||||
event["content"]
|
event["content"],
|
||||||
])
|
]
|
||||||
|
)
|
||||||
event_id = hashlib.sha256(signature_data.encode()).hexdigest()
|
event_id = hashlib.sha256(signature_data.encode()).hexdigest()
|
||||||
if event_id != event["id"]: # Invalid event id
|
if event_id != event["id"]: # Invalid event id
|
||||||
return False
|
return False
|
||||||
pubkeyHex = event["pubkey"]
|
pubkey_hex = event["pubkey"]
|
||||||
pubkey = secp256k1.PublicKey(bytes.fromhex("02" + pubkeyHex), True)
|
pubkey = secp256k1.PublicKey(bytes.fromhex("02" + pubkey_hex), True)
|
||||||
if not pubkey.schnorr_verify(bytes.fromhex(event_id), bytes.fromhex(event["sig"]), None, raw=True):
|
if not pubkey.schnorr_verify(
|
||||||
|
bytes.fromhex(event_id), bytes.fromhex(event["sig"]), None, raw=True
|
||||||
|
):
|
||||||
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)
|
||||||
|
|
@ -512,25 +546,27 @@ class NWCServiceProvider:
|
||||||
Returns:
|
Returns:
|
||||||
Dict: The input event with the signature added.
|
Dict: The input event with the signature added.
|
||||||
"""
|
"""
|
||||||
signature_data = self._json_dumps([
|
signature_data = self._json_dumps(
|
||||||
|
[
|
||||||
0,
|
0,
|
||||||
self.public_key_hex,
|
self.public_key_hex,
|
||||||
event["created_at"],
|
event["created_at"],
|
||||||
event["kind"],
|
event["kind"],
|
||||||
event["tags"],
|
event["tags"],
|
||||||
event["content"]
|
event["content"],
|
||||||
])
|
]
|
||||||
|
)
|
||||||
|
|
||||||
event_id = hashlib.sha256(signature_data.encode()).hexdigest()
|
event_id = hashlib.sha256(signature_data.encode()).hexdigest()
|
||||||
event["id"] = event_id
|
event["id"] = event_id
|
||||||
event["pubkey"] = self.public_key_hex
|
event["pubkey"] = self.public_key_hex
|
||||||
|
|
||||||
signature = (self.private_key.schnorr_sign(
|
signature = (
|
||||||
bytes.fromhex(event_id), None, raw=True)).hex()
|
self.private_key.schnorr_sign(bytes.fromhex(event_id), None, raw=True)
|
||||||
|
).hex()
|
||||||
event["sig"] = signature
|
event["sig"] = signature
|
||||||
return event
|
return event
|
||||||
|
|
||||||
|
|
||||||
async def cleanup(self):
|
async def cleanup(self):
|
||||||
logger.debug("Closing NWC Service Provider connection")
|
logger.debug("Closing NWC Service Provider connection")
|
||||||
self.shutdown = True # Mark for shutdown
|
self.shutdown = True # Mark for shutdown
|
||||||
|
|
@ -539,10 +575,10 @@ class NWCServiceProvider:
|
||||||
if self.reconnect_task:
|
if self.reconnect_task:
|
||||||
self.reconnect_task.cancel()
|
self.reconnect_task.cancel()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("Error closing reconnection task: "+str(e))
|
logger.warning("Error closing reconnection task: " + str(e))
|
||||||
# close the websocket
|
# close the websocket
|
||||||
try:
|
try:
|
||||||
if self.ws:
|
if self.ws:
|
||||||
await self.ws.close()
|
await self.ws.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("Error closing websocket connection: "+str(e))
|
logger.warning("Error closing websocket connection: " + str(e))
|
||||||
61
package-lock.json
generated
Normal file
61
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
{
|
||||||
|
"name": "nwcprovider",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "nwcprovider",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"prettier": "^3.3.3",
|
||||||
|
"pyright": "^1.1.379"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
|
||||||
|
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
|
||||||
|
"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.379",
|
||||||
|
"resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.379.tgz",
|
||||||
|
"integrity": "sha512-n0X+IMqot6zL5b54vfU9GattS8jM9IOh8TRFho1k/6VoyjrpzQ7TnU6PtZzwEZNJaZi5izoLIDeMnGmbin8n8Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"pyright": "index.js",
|
||||||
|
"pyright-langserver": "langserver.index.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
package.json
Normal file
8
package.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"name": "nwcprovider",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"prettier": "^3.3.3",
|
||||||
|
"pyright": "^1.1.379"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,49 +1,33 @@
|
||||||
|
|
||||||
|
|
||||||
nwc_permissions = {
|
nwc_permissions = {
|
||||||
"pay":{
|
"pay": {
|
||||||
"name":"Send payments",
|
"name": "Send payments",
|
||||||
"methods":[
|
"methods": [
|
||||||
"multi_pay_invoice",
|
"multi_pay_invoice",
|
||||||
"pay_invoice",
|
"pay_invoice",
|
||||||
"pay_keysend",
|
"pay_keysend",
|
||||||
"multi_pay_keysend",
|
"multi_pay_keysend",
|
||||||
],
|
],
|
||||||
"default": True
|
"default": True,
|
||||||
},
|
},
|
||||||
"invoice":{
|
"invoice": {
|
||||||
"name":"Create invoices",
|
"name": "Create invoices",
|
||||||
"methods":[
|
"methods": ["make_invoice"],
|
||||||
"make_invoice"
|
"default": True,
|
||||||
],
|
|
||||||
"default": True
|
|
||||||
},
|
},
|
||||||
"lookup":{
|
"lookup": {
|
||||||
"name":"Lookup status of invoice",
|
"name": "Lookup status of invoice",
|
||||||
"methods":[
|
"methods": ["lookup_invoice"],
|
||||||
"lookup_invoice"
|
"default": True,
|
||||||
],
|
|
||||||
"default": True
|
|
||||||
},
|
},
|
||||||
"history": {
|
"history": {
|
||||||
"name": "Read transaction history",
|
"name": "Read transaction history",
|
||||||
"methods": [
|
"methods": ["list_transactions"],
|
||||||
"list_transactions"
|
"default": True,
|
||||||
],
|
|
||||||
"default": True
|
|
||||||
},
|
},
|
||||||
"balance": {
|
"balance": {
|
||||||
"name": "Read wallet balance",
|
"name": "Read wallet balance",
|
||||||
"methods": [
|
"methods": ["get_balance"],
|
||||||
"get_balance"
|
"default": True,
|
||||||
],
|
|
||||||
"default": True
|
|
||||||
},
|
},
|
||||||
"info": {
|
"info": {"name": "Read account info", "methods": ["get_info"], "default": True},
|
||||||
"name": "Read account info",
|
|
||||||
"methods": [
|
|
||||||
"get_info"
|
|
||||||
],
|
|
||||||
"default": True
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
2512
poetry.lock
generated
Normal file
2512
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
99
pyproject.toml
Normal file
99
pyproject.toml
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
[tool.poetry]
|
||||||
|
name = "nwcprovider"
|
||||||
|
version = "0.0.0"
|
||||||
|
description = "A NWC service provider for LNbits."
|
||||||
|
authors = ["Riccardo Balbo <oc@rblb.it>"]
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.10 | ^3.9"
|
||||||
|
lnbits = "*"
|
||||||
|
psycopg2-binary = "^2.9.9"
|
||||||
|
|
||||||
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
black = "^24.3.0"
|
||||||
|
pytest-asyncio = "^0.21.0"
|
||||||
|
pytest = "^7.3.2"
|
||||||
|
mypy = "^1.5.1"
|
||||||
|
pre-commit = "^3.2.2"
|
||||||
|
ruff = "^0.3.2"
|
||||||
|
types-cffi = "^1.16.0.20240331"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core>=1.0.0"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
exclude = "(tests/*)"
|
||||||
|
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = [
|
||||||
|
"lnbits.*",
|
||||||
|
"lnurl.*",
|
||||||
|
"loguru.*",
|
||||||
|
"fastapi.*",
|
||||||
|
"pydantic.*",
|
||||||
|
"pyqrcode.*",
|
||||||
|
"shortuuid.*",
|
||||||
|
"httpx.*",
|
||||||
|
"websocket.*",
|
||||||
|
"secp256k1.*",
|
||||||
|
]
|
||||||
|
ignore_missing_imports = "True"
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
log_cli = false
|
||||||
|
testpaths = [
|
||||||
|
"tests"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.black]
|
||||||
|
line-length = 88
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
# Same as Black. + 10% rule of black
|
||||||
|
line-length = 88
|
||||||
|
# exclude = [
|
||||||
|
# "nostr",
|
||||||
|
# ]
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
# Enable:
|
||||||
|
# F - pyflakes
|
||||||
|
# E - pycodestyle errors
|
||||||
|
# W - pycodestyle warnings
|
||||||
|
# I - isort
|
||||||
|
# A - flake8-builtins
|
||||||
|
# C - mccabe
|
||||||
|
# N - naming
|
||||||
|
# UP - pyupgrade
|
||||||
|
# RUF - ruff
|
||||||
|
# B - bugbear
|
||||||
|
select = ["F", "E", "W", "I", "A", "C", "N", "UP", "RUF", "B"]
|
||||||
|
ignore = ["C901"]
|
||||||
|
|
||||||
|
# Allow autofix for all enabled rules (when `--fix`) is provided.
|
||||||
|
fixable = ["ALL"]
|
||||||
|
unfixable = []
|
||||||
|
|
||||||
|
# Allow unused variables when underscore-prefixed.
|
||||||
|
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||||
|
|
||||||
|
# needed for pydantic
|
||||||
|
[tool.ruff.lint.pep8-naming]
|
||||||
|
classmethod-decorators = [
|
||||||
|
"root_validator",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Ignore unused imports in __init__.py files.
|
||||||
|
# [tool.ruff.lint.extend-per-file-ignores]
|
||||||
|
# "__init__.py" = ["F401", "F403"]
|
||||||
|
|
||||||
|
# [tool.ruff.lint.mccabe]
|
||||||
|
# max-complexity = 10
|
||||||
|
|
||||||
|
[tool.ruff.lint.flake8-bugbear]
|
||||||
|
# Allow default arguments like, e.g., `data: List[str] = fastapi.Query(None)`.
|
||||||
|
extend-immutable-calls = [
|
||||||
|
"fastapi.Depends",
|
||||||
|
"fastapi.Query",
|
||||||
|
]
|
||||||
520
static/js/noble-secp256k1.min.js
vendored
520
static/js/noble-secp256k1.min.js
vendored
File diff suppressed because one or more lines are too long
293
tasks.py
293
tasks.py
|
|
@ -1,112 +1,131 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import time
|
||||||
|
from math import ceil
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
from bolt11 import decode as bolt11_decode
|
||||||
|
from lnbits.core.crud import get_payments, get_wallet, get_wallet_payment
|
||||||
|
from lnbits.core.models import Payment
|
||||||
|
from lnbits.core.services import (
|
||||||
|
check_transaction_status,
|
||||||
|
create_invoice,
|
||||||
|
pay_invoice,
|
||||||
|
)
|
||||||
|
from lnbits.db import Filters
|
||||||
|
from lnbits.exceptions import PaymentError
|
||||||
|
from lnbits.settings import settings
|
||||||
|
from lnbits.wallets.base import PaymentStatus
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.core.models import Payment
|
from .crud import get_config_nwc, get_nwc, tracked_spend_nwc
|
||||||
from lnbits.core.services import create_invoice, pay_invoice, check_transaction_status, PaymentError
|
|
||||||
from bolt11 import decode as bolt11_decode
|
|
||||||
from lnbits.core.crud import get_wallet_payment, get_payments, get_wallet
|
|
||||||
from .crud import get_nwc, tracked_spend_nwc, get_config_nwc
|
|
||||||
#from .crud import log_nwc
|
|
||||||
from . import execution_queue
|
|
||||||
from .NWCServiceProvider import NWCServiceProvider
|
|
||||||
from typing import Dict, List, Tuple
|
|
||||||
from math import ceil
|
|
||||||
import time
|
|
||||||
from lnbits.db import Filters
|
|
||||||
from .models import NWCKey
|
|
||||||
from typing import Optional
|
|
||||||
from .permission import nwc_permissions
|
|
||||||
from lnbits.settings import settings
|
|
||||||
|
|
||||||
async def _check(nwc: Optional[NWCKey], method: str, payload: Dict):
|
# from .crud import log_nwc
|
||||||
|
from .execution_queue import execution_queue
|
||||||
|
from .models import NWCKey
|
||||||
|
from .nwcp import NWCServiceProvider
|
||||||
|
from .permission import nwc_permissions
|
||||||
|
|
||||||
|
|
||||||
|
async def _check(nwc: Optional[NWCKey], method: str, payload: Dict) -> Optional[Dict]:
|
||||||
# check
|
# check
|
||||||
if not nwc:
|
if not nwc:
|
||||||
return {
|
return {
|
||||||
"code": "UNAUTHORIZED",
|
"code": "UNAUTHORIZED",
|
||||||
"message": "This public key has no wallet connected."
|
"message": "This public key has no wallet connected.",
|
||||||
}
|
}
|
||||||
# check permissions
|
# check permissions
|
||||||
allowed = False
|
allowed = False
|
||||||
permissions = nwc.getPermissions()
|
permissions = nwc.get_permissions()
|
||||||
for p in permissions:
|
for p in permissions:
|
||||||
allowed_methods = nwc_permissions.get(p, {}).get("methods", [])
|
permissions_data: Dict[str, Any] = nwc_permissions.get(p, {})
|
||||||
|
allowed_methods: List[str] = permissions_data.get("methods", [])
|
||||||
if method in allowed_methods:
|
if method in allowed_methods:
|
||||||
allowed = True
|
allowed = True
|
||||||
break
|
break
|
||||||
if not allowed:
|
if not allowed:
|
||||||
return {
|
return {
|
||||||
"code": "RESTRICTED",
|
"code": "RESTRICTED",
|
||||||
"message": "This public key is not allowed to do this operation."
|
"message": "This public key is not allowed to do this operation.",
|
||||||
}
|
}
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def _process_invoice(
|
||||||
async def _process_invoice(wallet_id:str, pubkey:str, invoice:str, amount_msats:int, description:Optional[str]=None):
|
wallet_id: str,
|
||||||
|
pubkey: str,
|
||||||
|
invoice: str,
|
||||||
|
amount_msats: int,
|
||||||
|
description: Optional[str] = None,
|
||||||
|
):
|
||||||
async def execute_payment():
|
async def execute_payment():
|
||||||
return await pay_invoice(wallet_id=wallet_id, payment_request=invoice,
|
return await pay_invoice(
|
||||||
max_sat=int(ceil(amount_msats/1000)),
|
wallet_id=wallet_id,
|
||||||
description=description or ""
|
payment_request=invoice,
|
||||||
|
max_sat=int(ceil(amount_msats / 1000)),
|
||||||
|
description=description or "",
|
||||||
)
|
)
|
||||||
|
|
||||||
payment_hash = None
|
payment_hash = None
|
||||||
try:
|
try:
|
||||||
in_budget, payment_hash = await tracked_spend_nwc(pubkey, amount_msats, execute_payment)
|
in_budget, payment_hash = await tracked_spend_nwc(
|
||||||
|
pubkey, amount_msats, execute_payment
|
||||||
|
)
|
||||||
if not in_budget:
|
if not in_budget:
|
||||||
error = {
|
error = {
|
||||||
"code":"QUOTA_EXCEEDED",
|
"code": "QUOTA_EXCEEDED",
|
||||||
"message": "The wallet has exceeded its spending quota."
|
"message": "The wallet has exceeded its spending quota.",
|
||||||
}
|
|
||||||
return {
|
|
||||||
"error": error,
|
|
||||||
"in_budget": False
|
|
||||||
}
|
}
|
||||||
|
return {"error": error, "in_budget": False}
|
||||||
except PaymentError as e:
|
except PaymentError as e:
|
||||||
status = e.status
|
status = e.status
|
||||||
message = e.message
|
message = e.message
|
||||||
if status == "failed":
|
if status == "failed":
|
||||||
error = {
|
error = {"code": "PAYMENT_FAILED", "message": message}
|
||||||
"code": "PAYMENT_FAILED",
|
return {"error": error, "in_budget": False}
|
||||||
"message": message
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
"error": error,
|
|
||||||
"in_budget": False
|
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
raise e
|
raise e
|
||||||
if not payment_hash:
|
if not payment_hash:
|
||||||
raise Exception("Payment hash not found")
|
raise Exception("Payment hash not found")
|
||||||
wait_for_preimage = True # currently required by nip 47 specs, might change in future
|
wait_for_preimage = (
|
||||||
payment_status = None
|
True # currently required by nip 47 specs, might change in future
|
||||||
|
)
|
||||||
|
payment_status: Optional[PaymentStatus] = 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:
|
||||||
break
|
break
|
||||||
await asyncio.sleep(0.05)
|
await asyncio.sleep(0.05)
|
||||||
|
if not payment_status:
|
||||||
|
raise Exception("Payment status not found")
|
||||||
return {
|
return {
|
||||||
"preimage": payment_status.preimage or "0000000000000000000000000000000000000000000000000000000000000000",
|
"preimage": payment_status.preimage
|
||||||
|
or "0000000000000000000000000000000000000000000000000000000000000000",
|
||||||
"fee_msats": payment_status.fee_msat,
|
"fee_msats": payment_status.fee_msat,
|
||||||
"paid": payment_status.paid,
|
"paid": payment_status.paid,
|
||||||
"payment_hash": payment_hash,
|
"payment_hash": payment_hash,
|
||||||
"in_budget": in_budget
|
"in_budget": in_budget,
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def _on_pay_invoice(sp: NWCServiceProvider, pubkey: str, payload: Dict) -> List[Tuple[Dict, Dict, List]]:
|
async def _on_pay_invoice(
|
||||||
|
sp: NWCServiceProvider, pubkey: str, payload: Dict
|
||||||
|
) -> List[Tuple[Optional[Dict], Optional[Dict], List]]:
|
||||||
nwc = await get_nwc(pubkey, None, False, True)
|
nwc = await get_nwc(pubkey, None, False, True)
|
||||||
error = await _check(nwc, "pay_invoice", payload)
|
error = await _check(nwc, "pay_invoice", payload)
|
||||||
if error:
|
if error:
|
||||||
return [(None,error, [])]
|
return [(None, error, [])]
|
||||||
|
if not nwc:
|
||||||
|
raise Exception("Pubkey has no associated wallet")
|
||||||
params = payload.get("params", {})
|
params = payload.get("params", {})
|
||||||
invoice = params.get("invoice", None)
|
invoice = params.get("invoice", None)
|
||||||
# Ensures invoice is provided
|
# Ensures invoice is provided
|
||||||
if not invoice:
|
if not invoice:
|
||||||
raise Exception("Missing invoice")
|
raise Exception("Missing invoice")
|
||||||
invoice_data = bolt11_decode(invoice)
|
invoice_data = bolt11_decode(invoice)
|
||||||
amount_msats = invoice_data.amount_msat
|
amount_msats = int(invoice_data.amount_msat or 0)
|
||||||
res = await _process_invoice(nwc.wallet, pubkey, invoice, amount_msats, invoice_data.description)
|
res = await _process_invoice(
|
||||||
|
nwc.wallet, pubkey, invoice, amount_msats, invoice_data.description
|
||||||
|
)
|
||||||
error = res.get("error")
|
error = res.get("error")
|
||||||
if error:
|
if error:
|
||||||
return [(None, error, [])]
|
return [(None, error, [])]
|
||||||
|
|
@ -114,32 +133,38 @@ async def _on_pay_invoice(sp: NWCServiceProvider, pubkey: str, payload: Dict) ->
|
||||||
out = {
|
out = {
|
||||||
"preimage": preimage,
|
"preimage": preimage,
|
||||||
}
|
}
|
||||||
#await log_nwc(pubkey, payload)
|
# await log_nwc(pubkey, payload)
|
||||||
return [(out,None, [])]
|
return [(out, None, [])]
|
||||||
|
|
||||||
|
|
||||||
async def _on_multi_pay_invoice(sp: NWCServiceProvider, pubkey: str, payload: Dict) -> List[Tuple[Dict, Dict, List]]:
|
async def _on_multi_pay_invoice(
|
||||||
|
sp: NWCServiceProvider, pubkey: str, payload: Dict
|
||||||
|
) -> List[Tuple[Optional[Dict], Optional[Dict], List]]:
|
||||||
nwc = await get_nwc(pubkey, None, False, True)
|
nwc = await get_nwc(pubkey, None, False, True)
|
||||||
error = await _check(nwc, "multi_pay_invoice", payload)
|
error = await _check(nwc, "multi_pay_invoice", payload)
|
||||||
if error:
|
if error:
|
||||||
return [(None, error, [])]
|
return [(None, error, [])]
|
||||||
|
if not nwc:
|
||||||
|
raise Exception("Pubkey has no associated wallet")
|
||||||
params = payload.get("params", {})
|
params = payload.get("params", {})
|
||||||
invoices = params.get("invoices", [])
|
invoices = params.get("invoices", [])
|
||||||
results = []
|
results: List[Tuple[Optional[Dict], Optional[Dict], List]] = []
|
||||||
|
|
||||||
# Ensures all invoices are provided
|
# Ensures all invoices are provided
|
||||||
for i in invoices:
|
for i in invoices:
|
||||||
invoice = i.get("invoice",None)
|
invoice = i.get("invoice", None)
|
||||||
if not invoice:
|
if not invoice:
|
||||||
raise Exception("Missing invoice")
|
raise Exception("Missing invoice")
|
||||||
|
|
||||||
for i in invoices:
|
for i in invoices:
|
||||||
try:
|
try:
|
||||||
id = i.get("id", None)
|
invoice_id = i.get("id", None)
|
||||||
invoice = i.get("invoice", None)
|
invoice = i.get("invoice", None)
|
||||||
invoice_data = bolt11_decode(invoice)
|
invoice_data = bolt11_decode(invoice)
|
||||||
amount_msats = invoice_data.amount_msat
|
amount_msats = int(invoice_data.amount_msat or 0)
|
||||||
res = await _process_invoice(nwc.wallet, pubkey, invoice, amount_msats, invoice_data.description)
|
res = await _process_invoice(
|
||||||
|
nwc.wallet, pubkey, invoice, amount_msats, invoice_data.description
|
||||||
|
)
|
||||||
error = res.get("error")
|
error = res.get("error")
|
||||||
if error:
|
if error:
|
||||||
results.append((None, error, []))
|
results.append((None, error, []))
|
||||||
|
|
@ -149,25 +174,24 @@ async def _on_multi_pay_invoice(sp: NWCServiceProvider, pubkey: str, payload: Di
|
||||||
"preimage": res.get("preimage"),
|
"preimage": res.get("preimage"),
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
[
|
[["d", invoice_id if invoice_id else res.get("payment_hash")]],
|
||||||
["d", id if id else res.get("payment_hash")]
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
results.append(r)
|
results.append(r)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
results.append((None, {
|
results.append((None, {"code": "INTERNAL", "message": str(e)}, []))
|
||||||
"code": "INTERNAL",
|
# await log_nwc(pubkey, payload)
|
||||||
"message": str(e)
|
|
||||||
}))
|
|
||||||
#await log_nwc(pubkey, payload)
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
async def _on_make_invoice(sp: NWCServiceProvider, pubkey: str, payload: Dict) -> List[Tuple[Dict, Dict, List]]:
|
async def _on_make_invoice(
|
||||||
|
sp: NWCServiceProvider, pubkey: str, payload: Dict
|
||||||
|
) -> List[Tuple[Optional[Dict], Optional[Dict], List]]:
|
||||||
nwc = await get_nwc(pubkey, None, False, True)
|
nwc = await get_nwc(pubkey, None, False, True)
|
||||||
error = await _check(nwc, "make_invoice", payload)
|
error = await _check(nwc, "make_invoice", payload)
|
||||||
if error:
|
if error:
|
||||||
return [(None, error, [])]
|
return [(None, error, [])]
|
||||||
|
if not nwc:
|
||||||
|
raise Exception("Pubkey has no associated wallet")
|
||||||
params = payload.get("params", {})
|
params = payload.get("params", {})
|
||||||
amount_msats = params.get("amount", None)
|
amount_msats = params.get("amount", None)
|
||||||
# Ensures amount is provided
|
# Ensures amount is provided
|
||||||
|
|
@ -175,18 +199,23 @@ async def _on_make_invoice(sp: NWCServiceProvider, pubkey: str, payload: Dict) -
|
||||||
raise Exception("Missing amount")
|
raise Exception("Missing amount")
|
||||||
description = params.get("description", "")
|
description = params.get("description", "")
|
||||||
description_hash = params.get("description_hash", None)
|
description_hash = params.get("description_hash", None)
|
||||||
expiry = params.get("expiry" , None)
|
expiry = params.get("expiry", None)
|
||||||
payment_hash, payment_request = await create_invoice(
|
payment_hash, payment_request = await create_invoice(
|
||||||
wallet_id=nwc.wallet,
|
wallet_id=nwc.wallet,
|
||||||
amount=int(amount_msats/1000),
|
amount=int(amount_msats / 1000),
|
||||||
currency="sat",
|
currency="sat",
|
||||||
memo=description,
|
memo=description,
|
||||||
description_hash=bytes.fromhex(description_hash) if description_hash else None,
|
description_hash=bytes.fromhex(description_hash) if description_hash else None,
|
||||||
unhashed_description=description.encode("utf-8"),
|
unhashed_description=description.encode("utf-8"),
|
||||||
expiry=expiry)
|
expiry=expiry,
|
||||||
payment_status = await check_transaction_status(wallet_id=nwc.wallet, payment_hash=payment_hash)
|
)
|
||||||
|
payment_status = await check_transaction_status(
|
||||||
|
wallet_id=nwc.wallet, payment_hash=payment_hash
|
||||||
|
)
|
||||||
preimage = payment_status.preimage
|
preimage = payment_status.preimage
|
||||||
if not preimage: # Some backend do not return a preimage (eg. FakeWallet), so we fake it
|
if (
|
||||||
|
not preimage
|
||||||
|
): # Some backend do not return a preimage (eg. FakeWallet), so we fake it
|
||||||
preimage = "0000000000000000000000000000000000000000000000000000000000000000"
|
preimage = "0000000000000000000000000000000000000000000000000000000000000000"
|
||||||
res = {
|
res = {
|
||||||
"type": "incoming",
|
"type": "incoming",
|
||||||
|
|
@ -196,21 +225,25 @@ async def _on_make_invoice(sp: NWCServiceProvider, pubkey: str, payload: Dict) -
|
||||||
"preimage": preimage,
|
"preimage": preimage,
|
||||||
"payment_hash": payment_hash,
|
"payment_hash": payment_hash,
|
||||||
"amount": amount_msats,
|
"amount": amount_msats,
|
||||||
#"fees_paid":None,
|
# "fees_paid":None,
|
||||||
"created_at": int(time.time()),
|
"created_at": int(time.time()),
|
||||||
"metadata": {}
|
"metadata": {},
|
||||||
}
|
}
|
||||||
if expiry:
|
if expiry:
|
||||||
res["expires_at"] = int(time.time()) + int(expiry)
|
res["expires_at"] = int(time.time()) + int(expiry)
|
||||||
#await log_nwc(pubkey, payload)
|
# await log_nwc(pubkey, payload)
|
||||||
return [(res, None, [])]
|
return [(res, None, [])]
|
||||||
|
|
||||||
|
|
||||||
async def _on_lookup_invoice(sp: NWCServiceProvider, pubkey: str, payload: Dict) -> List[Tuple[Dict, Dict, List]]:
|
async def _on_lookup_invoice(
|
||||||
|
sp: NWCServiceProvider, pubkey: str, payload: Dict
|
||||||
|
) -> List[Tuple[Optional[Dict], Optional[Dict], List]]:
|
||||||
nwc = await get_nwc(pubkey, None, False, True)
|
nwc = await get_nwc(pubkey, None, False, True)
|
||||||
error = await _check(nwc, "lookup_invoice", payload)
|
error = await _check(nwc, "lookup_invoice", payload)
|
||||||
if error:
|
if error:
|
||||||
return [(None, error, [])]
|
return [(None, error, [])]
|
||||||
|
if not nwc:
|
||||||
|
raise Exception("Pubkey has no associated wallet")
|
||||||
params = payload.get("params", {})
|
params = payload.get("params", {})
|
||||||
payment_hash = params.get("payment_hash", None)
|
payment_hash = params.get("payment_hash", None)
|
||||||
invoice = params.get("invoice", None)
|
invoice = params.get("invoice", None)
|
||||||
|
|
@ -228,59 +261,66 @@ async def _on_lookup_invoice(sp: NWCServiceProvider, pubkey: str, payload: Dict)
|
||||||
invoice_data = bolt11_decode(payment.bolt11)
|
invoice_data = bolt11_decode(payment.bolt11)
|
||||||
is_settled = not payment.pending
|
is_settled = not payment.pending
|
||||||
timestamp = payment.time or invoice_data.date
|
timestamp = payment.time or invoice_data.date
|
||||||
res = {
|
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": invoice_data.description if invoice_data.description else payment.memo,
|
"description": (
|
||||||
|
invoice_data.description if invoice_data.description else payment.memo
|
||||||
|
),
|
||||||
"preimage": payment.preimage if is_settled or payment.is_in else None,
|
"preimage": payment.preimage if is_settled or payment.is_in else None,
|
||||||
"payment_hash": payment.payment_hash,
|
"payment_hash": payment.payment_hash,
|
||||||
"amount": abs(payment.msat),
|
"amount": abs(payment.msat),
|
||||||
"fees_paid": abs(payment.fee),
|
"fees_paid": abs(payment.fee),
|
||||||
"created_at": timestamp,
|
"created_at": timestamp,
|
||||||
"expires_at": payment.expiry if payment.expiry else timestamp+3600,
|
"expires_at": payment.expiry if payment.expiry else timestamp + 3600,
|
||||||
"settled_at": timestamp if is_settled else None,
|
"settled_at": timestamp if is_settled else None,
|
||||||
"metadata": {}
|
"metadata": {},
|
||||||
}
|
}
|
||||||
if invoice_data.description_hash:
|
if invoice_data.description_hash:
|
||||||
res["description_hash"] = invoice_data.description_hash
|
res["description_hash"] = invoice_data.description_hash
|
||||||
#await log_nwc(pubkey, payload)
|
# await log_nwc(pubkey, payload)
|
||||||
return [(res, None, [])]
|
return [(res, None, [])]
|
||||||
|
|
||||||
|
|
||||||
async def _on_list_transactions(sp: NWCServiceProvider, pubkey: str, payload: Dict) -> List[Tuple[Dict, Dict, List]]:
|
async def _on_list_transactions(
|
||||||
|
sp: NWCServiceProvider, pubkey: str, payload: Dict
|
||||||
|
) -> List[Tuple[Optional[Dict], Optional[Dict], List]]:
|
||||||
nwc = await get_nwc(pubkey, None, False, True)
|
nwc = await get_nwc(pubkey, None, False, True)
|
||||||
error = await _check(nwc, "list_transactions", payload)
|
error = await _check(nwc, "list_transactions", payload)
|
||||||
if error:
|
if error:
|
||||||
return [(None, error, [])]
|
return [(None, error, [])]
|
||||||
|
if not nwc:
|
||||||
|
raise Exception("Pubkey has no associated wallet")
|
||||||
tfrom = payload.get("from", 0)
|
tfrom = payload.get("from", 0)
|
||||||
tto = payload.get("to", int(time.time()))
|
tto = payload.get("to", int(time.time()))
|
||||||
limit = payload.get("limit", 10)
|
limit = payload.get("limit", 10)
|
||||||
offset = payload.get("offset", 0)
|
offset = payload.get("offset", 0)
|
||||||
unpaid = payload.get("unpaid", False)
|
unpaid = payload.get("unpaid", False)
|
||||||
type = payload.get("type", None)
|
tx_type = payload.get("type", None)
|
||||||
values = []
|
values = []
|
||||||
filters = Filters()
|
filters: Filters = Filters()
|
||||||
filters.where("time <= ?")
|
filters.where(["time <= ?"])
|
||||||
values.append(tto)
|
values.append(tto)
|
||||||
filters.values(values)
|
filters.values(values)
|
||||||
history = await get_payments(
|
history = await get_payments(
|
||||||
wallet_id=nwc.wallet,
|
wallet_id=nwc.wallet,
|
||||||
complete=True,
|
complete=True,
|
||||||
pending = unpaid,
|
pending=unpaid,
|
||||||
outgoing=not type or type == "outgoing",
|
outgoing=not tx_type or tx_type == "outgoing",
|
||||||
incoming =not type or type=="incoming",
|
incoming=not tx_type or tx_type == "incoming",
|
||||||
since=tfrom,
|
since=tfrom,
|
||||||
exclude_uncheckable=False,
|
exclude_uncheckable=False,
|
||||||
filters=filters,
|
filters=filters,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
offset=offset
|
offset=offset,
|
||||||
)
|
)
|
||||||
transactions = []
|
transactions: List[Dict] = []
|
||||||
for p in history:
|
|
||||||
p: Payment
|
p: Payment
|
||||||
|
for p in history:
|
||||||
invoice_data = bolt11_decode(p.bolt11)
|
invoice_data = bolt11_decode(p.bolt11)
|
||||||
is_settled = not p.pending
|
is_settled = not p.pending
|
||||||
transactions.append({
|
transactions.append(
|
||||||
|
{
|
||||||
"type": "outgoing" if p.is_out else "incoming",
|
"type": "outgoing" if p.is_out else "incoming",
|
||||||
"invoice": p.bolt11,
|
"invoice": p.bolt11,
|
||||||
"description": invoice_data.description,
|
"description": invoice_data.description,
|
||||||
|
|
@ -291,68 +331,79 @@ async def _on_list_transactions(sp: NWCServiceProvider, pubkey: str, payload: Di
|
||||||
"fees_paid": p.fee,
|
"fees_paid": p.fee,
|
||||||
"created_at": p.time,
|
"created_at": p.time,
|
||||||
"settled_at": p.time if is_settled else None,
|
"settled_at": p.time if is_settled else None,
|
||||||
"metadata": {}
|
"metadata": {},
|
||||||
})
|
}
|
||||||
#await log_nwc(pubkey, payload)
|
)
|
||||||
return [({
|
# await log_nwc(pubkey, payload)
|
||||||
"transactions": transactions
|
return [({"transactions": transactions}, None, [])]
|
||||||
}, None, [])]
|
|
||||||
|
|
||||||
|
|
||||||
async def _on_get_balance(sp: NWCServiceProvider, pubkey: str, payload: Dict) -> List[Tuple[Dict, Dict, List]]:
|
async def _on_get_balance(
|
||||||
|
sp: NWCServiceProvider, pubkey: str, payload: Dict
|
||||||
|
) -> List[Tuple[Optional[Dict], Optional[Dict], List]]:
|
||||||
nwc = await get_nwc(pubkey, None, False, True)
|
nwc = await get_nwc(pubkey, None, False, True)
|
||||||
error = await _check(nwc, "get_balance", payload)
|
error = await _check(nwc, "get_balance", payload)
|
||||||
if error:
|
if error:
|
||||||
return [(None, error, [])]
|
return [(None, error, [])]
|
||||||
|
if not nwc:
|
||||||
|
raise Exception("Pubkey has no associated wallet")
|
||||||
balance = 0
|
balance = 0
|
||||||
wallet = await get_wallet(nwc.wallet)
|
wallet = await get_wallet(nwc.wallet)
|
||||||
if not wallet:
|
if not wallet:
|
||||||
raise Exception("Wallet not found")
|
raise Exception("Wallet not found")
|
||||||
balance = wallet.balance_msat
|
balance = wallet.balance_msat
|
||||||
#await log_nwc(pubkey, payload)
|
# await log_nwc(pubkey, payload)
|
||||||
return [({
|
return [({"balance": balance}, None, [])]
|
||||||
"balance": balance
|
|
||||||
}, None, [])]
|
|
||||||
|
|
||||||
|
|
||||||
async def _on_get_info(sp: NWCServiceProvider, pubkey: str, payload: Dict) -> List[Tuple[Dict, Dict, List]]:
|
async def _on_get_info(
|
||||||
|
sp: NWCServiceProvider, pubkey: str, payload: Dict
|
||||||
|
) -> List[Tuple[Optional[Dict], Optional[Dict], List]]:
|
||||||
nwc = await get_nwc(pubkey, None, False, True)
|
nwc = await get_nwc(pubkey, None, False, True)
|
||||||
error = await _check(nwc, "get_info", payload)
|
error = await _check(nwc, "get_info", payload)
|
||||||
if error:
|
if error:
|
||||||
return [(None, error, [])]
|
return [(None, error, [])]
|
||||||
sp_methods = sp.getSupportedMethods()
|
if not nwc:
|
||||||
permissions = nwc.getPermissions()
|
raise Exception("Pubkey has no associated wallet")
|
||||||
|
sp_methods = sp.get_supported_methods()
|
||||||
|
permissions = nwc.get_permissions()
|
||||||
# Filter only methods supported by the extension and allowed by the permissions
|
# Filter only methods supported by the extension and allowed by the permissions
|
||||||
account_methods = []
|
account_methods = []
|
||||||
for spm in sp_methods:
|
for spm in sp_methods:
|
||||||
for p in permissions:
|
for p in permissions:
|
||||||
allowed_methods = nwc_permissions.get(p, {}).get("methods", [])
|
permissions_data: Dict[str, Any] = nwc_permissions.get(p, {})
|
||||||
|
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
|
||||||
#await log_nwc(pubkey, payload)
|
# await log_nwc(pubkey, payload)
|
||||||
return [({
|
return [
|
||||||
|
(
|
||||||
|
{
|
||||||
"alias": settings.lnbits_site_title,
|
"alias": settings.lnbits_site_title,
|
||||||
"color": "",
|
"color": "",
|
||||||
"network": "mainnet",
|
"network": "mainnet",
|
||||||
"block_height": 0,
|
"block_height": 0,
|
||||||
"block_hash": "",
|
"block_hash": "",
|
||||||
"methods": account_methods
|
"methods": account_methods,
|
||||||
}, None, [])]
|
},
|
||||||
|
None,
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
async def handle_nwc():
|
async def handle_nwc():
|
||||||
priv_key = await get_config_nwc("provider_key")
|
priv_key = await get_config_nwc("provider_key")
|
||||||
relay = await get_config_nwc("relay")
|
relay = await get_config_nwc("relay")
|
||||||
nwcsp = NWCServiceProvider(priv_key, relay)
|
nwcsp = NWCServiceProvider(priv_key, relay)
|
||||||
nwcsp.addRequestListener("pay_invoice", _on_pay_invoice)
|
nwcsp.add_request_listener("pay_invoice", _on_pay_invoice)
|
||||||
nwcsp.addRequestListener("multi_pay_invoice", _on_multi_pay_invoice)
|
nwcsp.add_request_listener("multi_pay_invoice", _on_multi_pay_invoice)
|
||||||
nwcsp.addRequestListener("make_invoice", _on_make_invoice)
|
nwcsp.add_request_listener("make_invoice", _on_make_invoice)
|
||||||
nwcsp.addRequestListener("lookup_invoice", _on_lookup_invoice)
|
nwcsp.add_request_listener("lookup_invoice", _on_lookup_invoice)
|
||||||
nwcsp.addRequestListener("list_transactions", _on_list_transactions)
|
nwcsp.add_request_listener("list_transactions", _on_list_transactions)
|
||||||
nwcsp.addRequestListener("get_balance", _on_get_balance)
|
nwcsp.add_request_listener("get_balance", _on_get_balance)
|
||||||
nwcsp.addRequestListener("get_info", _on_get_info)
|
nwcsp.add_request_listener("get_info", _on_get_info)
|
||||||
# currently not supported by lnbits
|
# currently not supported by lnbits
|
||||||
# nwcsp.addRequestListener("pay_keysend", _on_pay_keysend)
|
# nwcsp.addRequestListener("pay_keysend", _on_pay_keysend)
|
||||||
# nwcsp.addRequestListener("multi_pay_keysend", _on_multi_pay_keysend)
|
# nwcsp.addRequestListener("multi_pay_keysend", _on_multi_pay_keysend)
|
||||||
|
|
@ -373,9 +424,13 @@ async def handle_execution_queue():
|
||||||
action = task.get("action")
|
action = task.get("action")
|
||||||
future = task.get("future")
|
future = task.get("future")
|
||||||
try:
|
try:
|
||||||
|
if not action:
|
||||||
|
raise Exception("Invalid action")
|
||||||
res = await action()
|
res = await action()
|
||||||
|
if future:
|
||||||
future.set_result(res)
|
future.set_result(res)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
if future:
|
||||||
future.set_exception(e)
|
future.set_exception(e)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(str(e))
|
logger.error(str(e))
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,9 @@
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div class="row items-center no-wrap q-mb-md">
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h5 class="text-subtitle1 q-my-none">NWC Service Provider - Config</h5>
|
<h5 class="text-subtitle1 q-my-none">
|
||||||
|
NWC Service Provider - Config
|
||||||
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -14,9 +16,7 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
<q-tr v-for="entry in entries" :key="entry.key">
|
<q-tr v-for="entry in entries" :key="entry.key">
|
||||||
<q-td>
|
<q-td>
|
||||||
<span>
|
<span> ${entry.key} </span>
|
||||||
${entry.key}
|
|
||||||
</span>
|
|
||||||
</q-td>
|
</q-td>
|
||||||
<q-td>
|
<q-td>
|
||||||
<q-input v-model="entry.value" />
|
<q-input v-model="entry.value" />
|
||||||
|
|
@ -25,36 +25,56 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</q-markup-table>
|
</q-markup-table>
|
||||||
</template>
|
</template>
|
||||||
<q-btn @click="saveConfig" unelevated label="Save" color="primary" class="q-mt-md full-width" />
|
<q-btn
|
||||||
|
@click="saveConfig"
|
||||||
|
unelevated
|
||||||
|
label="Save"
|
||||||
|
color="primary"
|
||||||
|
class="q-mt-md full-width"
|
||||||
|
/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
el: '#vue',
|
el: "#vue",
|
||||||
mixins: [windowMixin],
|
mixins: [windowMixin],
|
||||||
delimiters: ['${', '}'],
|
delimiters: ["${", "}"],
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
entries: [],
|
entries: [],
|
||||||
columns: [
|
columns: [
|
||||||
{ name: 'key', required: true, label: 'Key', align: 'left', field: row => row.key, sortable: true },
|
{
|
||||||
{ name: 'value', required: true, label: 'Value', align: 'left', field: row => row.value, sortable: true },
|
name: "key",
|
||||||
]
|
required: true,
|
||||||
|
label: "Key",
|
||||||
}
|
align: "left",
|
||||||
|
field: (row) => row.key,
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "value",
|
||||||
|
required: true,
|
||||||
|
label: "Value",
|
||||||
|
align: "left",
|
||||||
|
field: (row) => row.value,
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
fetchConfig() {
|
fetchConfig() {
|
||||||
this.entries = [];
|
this.entries = [];
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request('GET', '/nwcprovider/api/v1/config', this.g.user.wallets[0].adminkey)
|
.request(
|
||||||
|
"GET",
|
||||||
|
"/nwcprovider/api/v1/config",
|
||||||
|
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)) {
|
||||||
|
|
@ -65,7 +85,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
this.entries = newEntries;
|
this.entries = newEntries;
|
||||||
|
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
console.error("Error fetching config:", error);
|
console.error("Error fetching config:", error);
|
||||||
|
|
@ -77,25 +96,28 @@
|
||||||
data[entry.key] = entry.value;
|
data[entry.key] = entry.value;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const response = await LNbits.api
|
const response = await LNbits.api.request(
|
||||||
.request('POST', '/nwcprovider/api/v1/config', this.g.user.wallets[0].adminkey, data)
|
"POST",
|
||||||
|
"/nwcprovider/api/v1/config",
|
||||||
|
this.g.user.wallets[0].adminkey,
|
||||||
|
data,
|
||||||
|
);
|
||||||
this.$q.notify({
|
this.$q.notify({
|
||||||
message: 'Config saved, please restart the server',
|
message: "Config saved, please restart the server",
|
||||||
color: 'positive',
|
color: "positive",
|
||||||
position: 'top',
|
position: "top",
|
||||||
timeout: 2000,
|
timeout: 2000,
|
||||||
actions: [{ icon: 'close', color: 'white' }]
|
actions: [{ icon: "close", color: "white" }],
|
||||||
})
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error saving config:", error);
|
console.error("Error saving config:", error);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
created: function () {
|
created: function () {
|
||||||
this.fetchConfig()
|
this.fetchConfig();
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
@ -8,8 +8,14 @@
|
||||||
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<q-select v-model="selectedWallet" filled dense emit-value :options="g.user.walletOptions"
|
<q-select
|
||||||
label="Wallet *"></q-select>
|
v-model="selectedWallet"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
emit-value
|
||||||
|
:options="g.user.walletOptions"
|
||||||
|
label="Wallet *"
|
||||||
|
></q-select>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
||||||
|
|
@ -19,16 +25,28 @@
|
||||||
<div class="col items-center">
|
<div class="col items-center">
|
||||||
<h5 class="text-subtitle1">Connected Apps</h5>
|
<h5 class="text-subtitle1">Connected Apps</h5>
|
||||||
</div>
|
</div>
|
||||||
<q-btn @click="showConnectDialog()" aria-label="Connect" icon="add" color="primary" size="sm" round />
|
<q-btn
|
||||||
|
@click="showConnectDialog()"
|
||||||
|
aria-label="Connect"
|
||||||
|
icon="add"
|
||||||
|
color="primary"
|
||||||
|
size="sm"
|
||||||
|
round
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<q-table dense flat :data="nwcEntries" row-key="id" :columns="nwcsTable.columns"
|
<q-table
|
||||||
:pagination.sync="nwcsTable.pagination">
|
dense
|
||||||
|
flat
|
||||||
|
:data="nwcEntries"
|
||||||
|
row-key="id"
|
||||||
|
:columns="nwcsTable.columns"
|
||||||
|
:pagination.sync="nwcsTable.pagination"
|
||||||
|
>
|
||||||
<nwcprovider v-slot:header="props">
|
<nwcprovider v-slot:header="props">
|
||||||
<q-tr :props="props">
|
<q-tr :props="props">
|
||||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||||
${ col.label }
|
${ col.label }
|
||||||
</q-th>
|
</q-th>
|
||||||
|
|
||||||
</q-tr>
|
</q-tr>
|
||||||
</nwcprovider>
|
</nwcprovider>
|
||||||
|
|
||||||
|
|
@ -38,20 +56,30 @@
|
||||||
<div>${ col.value }</div>
|
<div>${ col.value }</div>
|
||||||
</q-td>
|
</q-td>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<q-td>
|
<q-td>
|
||||||
<q-btn flat dense size="xs" @click="openConnectionInfoDialog(props.row)" icon="info" color="primary">
|
<q-btn
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
@click="openConnectionInfoDialog(props.row)"
|
||||||
|
icon="info"
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
<q-tooltip> Info </q-tooltip>
|
<q-tooltip> Info </q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
<q-btn flat dense size="xs" @click="deleteNWC(props.row.pubkey)" icon="cancel" color="pink">
|
<q-btn
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
@click="deleteNWC(props.row.pubkey)"
|
||||||
|
icon="cancel"
|
||||||
|
color="pink"
|
||||||
|
>
|
||||||
<q-tooltip> Delete </q-tooltip>
|
<q-tooltip> Delete </q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</q-td>
|
</q-td>
|
||||||
|
|
||||||
</q-tr>
|
</q-tr>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
</q-table>
|
</q-table>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
@ -62,80 +90,101 @@
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<h6 class="text-subtitle1 q-my-none">NWC Service provider</h6>
|
<h6 class="text-subtitle1 q-my-none">NWC Service provider</h6>
|
||||||
<p>
|
<p>
|
||||||
Nostr Wallet Connect (NWC) are a way for applications to connect to a remote lightning wallet.
|
Nostr Wallet Connect (NWC) are a way for applications to connect to a
|
||||||
|
remote lightning wallet.
|
||||||
</p>
|
</p>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<q-separator></q-separator>
|
<q-separator></q-separator>
|
||||||
<q-list>
|
<q-list>
|
||||||
|
<q-item
|
||||||
<q-item clickable @click="go('../docs#/NWC%20Service%20Provider')"
|
clickable
|
||||||
class="q-flex items-center justify-between">
|
@click="go('../docs#/NWC%20Service%20Provider')"
|
||||||
|
class="q-flex items-center justify-between"
|
||||||
|
>
|
||||||
<q-icon name="swap_vertical_circle" class="q-mr-md"></q-icon>
|
<q-icon name="swap_vertical_circle" class="q-mr-md"></q-icon>
|
||||||
<q-item-section>Swagger API</q-item-section>
|
<q-item-section>Swagger API</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-separator></q-separator>
|
<q-separator></q-separator>
|
||||||
<q-item v-if="this.g.user.admin" clickable @click="go('/nwcprovider/admin')"
|
<q-item
|
||||||
class="q-flex items-center justify-between">
|
v-if="this.g.user.admin"
|
||||||
|
clickable
|
||||||
|
@click="go('/nwcprovider/admin')"
|
||||||
|
class="q-flex items-center justify-between"
|
||||||
|
>
|
||||||
<q-icon name="settings" class="q-mr-md"></q-icon>
|
<q-icon name="settings" class="q-mr-md"></q-icon>
|
||||||
<q-item-section>Settings</q-item-section>
|
<q-item-section>Settings</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|
||||||
</q-list>
|
</q-list>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<q-dialog
|
||||||
<q-dialog v-model="connectionInfoDialog.show" @hide="closeConnectionInfoDialog">
|
v-model="connectionInfoDialog.show"
|
||||||
|
@hide="closeConnectionInfoDialog"
|
||||||
|
>
|
||||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||||
<q-card-section class="q-ma-none">
|
<q-card-section class="q-ma-none">
|
||||||
<h3 class="q-ma-none q-mb-md text-center">Info</h3>
|
<h3 class="q-ma-none q-mb-md text-center">Info</h3>
|
||||||
<q-list>
|
<q-list>
|
||||||
|
|
||||||
<q-item>
|
<q-item>
|
||||||
<q-item-section>Description</q-item-section>
|
<q-item-section>Description</q-item-section>
|
||||||
<q-item-section side>${connectionInfoDialog.data.description}</q-item-section>
|
<q-item-section side
|
||||||
|
>${connectionInfoDialog.data.description}</q-item-section
|
||||||
|
>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-item>
|
<q-item>
|
||||||
<q-item-section>Last used</q-item-section>
|
<q-item-section>Last used</q-item-section>
|
||||||
<q-item-section side>${connectionInfoDialog.data.last_used}</q-item-section>
|
<q-item-section side
|
||||||
|
>${connectionInfoDialog.data.last_used}</q-item-section
|
||||||
|
>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-item>
|
<q-item>
|
||||||
<q-item-section>Expires</q-item-section>
|
<q-item-section>Expires</q-item-section>
|
||||||
<q-item-section side>${connectionInfoDialog.data.expires_at}</q-item-section>
|
<q-item-section side
|
||||||
|
>${connectionInfoDialog.data.expires_at}</q-item-section
|
||||||
|
>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-item>
|
<q-item>
|
||||||
<q-item-section>Created</q-item-section>
|
<q-item-section>Created</q-item-section>
|
||||||
<q-item-section side>${connectionInfoDialog.data.created_at}</q-item-section>
|
<q-item-section side
|
||||||
|
>${connectionInfoDialog.data.created_at}</q-item-section
|
||||||
|
>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|
||||||
<q-item>
|
<q-item>
|
||||||
<q-item-section>Permissions</q-item-section>
|
<q-item-section>Permissions</q-item-section>
|
||||||
<q-item-section side>
|
<q-item-section side>
|
||||||
|
|
||||||
${connectionInfoDialog.data.permissions}
|
${connectionInfoDialog.data.permissions}
|
||||||
|
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-item>
|
<q-item>
|
||||||
<q-item-section>Limits
|
<q-item-section
|
||||||
|
>Limits
|
||||||
<q-markup-table class="q-mt-md">
|
<q-markup-table class="q-mt-md">
|
||||||
<thead>
|
<thead>
|
||||||
<q-tr>
|
<q-tr>
|
||||||
<q-th>
|
<q-th>
|
||||||
Budget (sats)
|
Budget (sats)
|
||||||
<div style="font-size: smaller;">used/max</div>
|
<div style="font-size: smaller">used/max</div>
|
||||||
</q-th>
|
</q-th>
|
||||||
<q-th>Refresh</q-th>
|
<q-th>Refresh</q-th>
|
||||||
</q-tr>
|
</q-tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<q-tr v-if="!connectionInfoDialog.data.budgets || connectionInfoDialog.data.budgets.length === 0">
|
<q-tr
|
||||||
|
v-if="!connectionInfoDialog.data.budgets || connectionInfoDialog.data.budgets.length === 0"
|
||||||
|
>
|
||||||
<q-td colspan="2" class="text-center">No limits</q-td>
|
<q-td colspan="2" class="text-center">No limits</q-td>
|
||||||
</q-tr>
|
</q-tr>
|
||||||
<q-tr v-else v-for="budget in connectionInfoDialog.data.budgets">
|
<q-tr
|
||||||
<q-td class="text-center">${budget.used_budget_sats} / ${budget.budget_sats}</q-td>
|
v-else
|
||||||
|
v-for="budget in connectionInfoDialog.data.budgets"
|
||||||
|
>
|
||||||
|
<q-td class="text-center"
|
||||||
|
>${budget.used_budget_sats} / ${budget.budget_sats}</q-td
|
||||||
|
>
|
||||||
<q-td class="text-center">${budget.refresh_window}</q-td>
|
<q-td class="text-center">${budget.refresh_window}</q-td>
|
||||||
</q-tr>
|
</q-tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
@ -144,27 +193,44 @@
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
<q-card-actions class="q-mt-md">
|
<q-card-actions class="q-mt-md">
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto full-width " @click="closePairingDialog">Close</q-btn>
|
<q-btn
|
||||||
|
v-close-popup
|
||||||
|
flat
|
||||||
|
color="grey"
|
||||||
|
class="q-ml-auto full-width"
|
||||||
|
@click="closePairingDialog"
|
||||||
|
>Close</q-btn
|
||||||
|
>
|
||||||
</q-card-actions>
|
</q-card-actions>
|
||||||
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
||||||
|
|
||||||
<q-dialog v-model="pairingQrDialog.show" @hide="closePairingQrDialog">
|
<q-dialog v-model="pairingQrDialog.show" @hide="closePairingQrDialog">
|
||||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||||
<q-card-section class="q-ma-none">
|
<q-card-section class="q-ma-none">
|
||||||
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
||||||
<lnbits-qrcode :value="pairingQrDialog.data.pairingUrl"></lnbits-qrcode>
|
<lnbits-qrcode
|
||||||
|
:value="pairingQrDialog.data.pairingUrl"
|
||||||
|
></lnbits-qrcode>
|
||||||
</q-responsive>
|
</q-responsive>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-section class="q-ma-none">
|
<q-card-section class="q-ma-none">
|
||||||
<p class=" q-ma-none text-center">Please scan this QR code with a supported app</p>
|
<p class="q-ma-none text-center">
|
||||||
<p class=" q-ma-none text-center text-caption ">Connect only with app you trust!</p>
|
Please scan this QR code with a supported app
|
||||||
|
</p>
|
||||||
|
<p class="q-ma-none text-center text-caption">
|
||||||
|
Connect only with app you trust!
|
||||||
|
</p>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-actions>
|
<q-card-actions>
|
||||||
<q-btn label="Close" flat color="grey" class="full-width" @click="closePairingQrDialog"></q-btn>
|
<q-btn
|
||||||
|
label="Close"
|
||||||
|
flat
|
||||||
|
color="grey"
|
||||||
|
class="full-width"
|
||||||
|
@click="closePairingQrDialog"
|
||||||
|
></q-btn>
|
||||||
</q-card-actions>
|
</q-card-actions>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
@ -173,33 +239,58 @@
|
||||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||||
<q-card-section class="q-ma-none">
|
<q-card-section class="q-ma-none">
|
||||||
<h3 class="q-ma-none q-mb-md text-center">Pairing</h3>
|
<h3 class="q-ma-none q-mb-md text-center">Pairing</h3>
|
||||||
<p class=" q-ma-none text-center">Complete the last step of the setup by pasting or scanning your connection's
|
<p class="q-ma-none text-center">
|
||||||
pairing secret in the desired
|
Complete the last step of the setup by pasting or scanning your
|
||||||
app to
|
connection's pairing secret in the desired app to finalise the
|
||||||
finalise the connection.</p>
|
connection.
|
||||||
<p class=" q-ma-none text-center text-caption ">Connect only with app you trust!</p>
|
</p>
|
||||||
|
<p class="q-ma-none text-center text-caption">
|
||||||
|
Connect only with app you trust!
|
||||||
|
</p>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-actions>
|
<q-card-actions>
|
||||||
<q-btn label="Open in Supported App" color="primary" class="full-width" icon="launch"
|
<q-btn
|
||||||
@click="openPairingUrl()"></q-btn>
|
label="Open in Supported App"
|
||||||
|
color="primary"
|
||||||
|
class="full-width"
|
||||||
|
icon="launch"
|
||||||
|
@click="openPairingUrl()"
|
||||||
|
></q-btn>
|
||||||
</q-card-actions>
|
</q-card-actions>
|
||||||
<q-card-section class="q-ma-none">
|
<q-card-section class="q-ma-none">
|
||||||
<h5 class="q-ma-none text-center">Advanced</h5>
|
<h5 class="q-ma-none text-center">Advanced</h5>
|
||||||
|
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<q-card-actions>
|
<q-card-actions>
|
||||||
<q-btn label="Copy Pairing URL" flat color="grey" class="full-width" icon="content_copy"
|
<q-btn
|
||||||
@click="copyPairingUrl()"></q-btn>
|
label="Copy Pairing URL"
|
||||||
|
flat
|
||||||
|
color="grey"
|
||||||
|
class="full-width"
|
||||||
|
icon="content_copy"
|
||||||
|
@click="copyPairingUrl()"
|
||||||
|
></q-btn>
|
||||||
</q-card-actions>
|
</q-card-actions>
|
||||||
<q-card-actions>
|
<q-card-actions>
|
||||||
|
<q-btn
|
||||||
<q-btn label="Show QR Code" flat color="grey" class="full-width" icon="qr_code_scanner"
|
label="Show QR Code"
|
||||||
@click="showPairingQR()"></q-btn>
|
flat
|
||||||
|
color="grey"
|
||||||
|
class="full-width"
|
||||||
|
icon="qr_code_scanner"
|
||||||
|
@click="showPairingQR()"
|
||||||
|
></q-btn>
|
||||||
</q-card-actions>
|
</q-card-actions>
|
||||||
|
|
||||||
<q-card-actions>
|
<q-card-actions>
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto full-width" @click="closePairingDialog">Close</q-btn>
|
<q-btn
|
||||||
|
v-close-popup
|
||||||
|
flat
|
||||||
|
color="grey"
|
||||||
|
class="q-ml-auto full-width"
|
||||||
|
@click="closePairingDialog"
|
||||||
|
>Close</q-btn
|
||||||
|
>
|
||||||
</q-card-actions>
|
</q-card-actions>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
@ -209,42 +300,78 @@
|
||||||
<h3 class="q-ma-none q-mb-xl text-center">Add connection</h3>
|
<h3 class="q-ma-none q-mb-xl text-center">Add connection</h3>
|
||||||
|
|
||||||
<q-form @submit="confirmConnectDialog" class="q-gutter-md">
|
<q-form @submit="confirmConnectDialog" class="q-gutter-md">
|
||||||
<q-input v-model="connectDialog.data.description" filled dense label="Description"></q-input>
|
<q-input
|
||||||
|
v-model="connectDialog.data.description"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
label="Description"
|
||||||
|
></q-input>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<q-input :disable="connectDialog.data.neverExpires" v-model="connectDialog.data.expires_at" class="col"
|
<q-input
|
||||||
v-model="connectDialog.data.expires_at" filled dense label="Expires at" type="datetime-local"></q-input>
|
:disable="connectDialog.data.neverExpires"
|
||||||
<q-checkbox v-model="connectDialog.data.neverExpires" class="col-auto"
|
v-model="connectDialog.data.expires_at"
|
||||||
v-model="connectDialog.data.neverExpires" label="Never"></q-checkbox>
|
class="col"
|
||||||
|
v-model="connectDialog.data.expires_at"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
label="Expires at"
|
||||||
|
type="datetime-local"
|
||||||
|
></q-input>
|
||||||
|
<q-checkbox
|
||||||
|
v-model="connectDialog.data.neverExpires"
|
||||||
|
class="col-auto"
|
||||||
|
v-model="connectDialog.data.neverExpires"
|
||||||
|
label="Never"
|
||||||
|
></q-checkbox>
|
||||||
</div>
|
</div>
|
||||||
<q-list dense bordered padding>
|
<q-list dense bordered padding>
|
||||||
<q-item>
|
<q-item>
|
||||||
<q-item-label header>Authorize the app to</q-item-label>
|
<q-item-label header>Authorize the app to</q-item-label>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-item v-for="permission in connectDialog.data.permissions">
|
<q-item v-for="permission in connectDialog.data.permissions">
|
||||||
<q-checkbox v-model="permission.value" :label="permission.name"></q-checkbox>
|
<q-checkbox
|
||||||
|
v-model="permission.value"
|
||||||
|
:label="permission.name"
|
||||||
|
></q-checkbox>
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
<q-list dense bordered padding>
|
<q-list dense bordered padding>
|
||||||
<q-item>
|
<q-item>
|
||||||
<q-item-label header>Limit the spendable amount</q-item-label>
|
<q-item-label header>Limit the spendable amount</q-item-label>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-item v-if="!connectDialog.data.budgets || connectDialog.data.budgets.length === 0">
|
<q-item
|
||||||
<q-item-section class="full-width text-center">No limit</q-item-section>
|
v-if="!connectDialog.data.budgets || connectDialog.data.budgets.length === 0"
|
||||||
|
>
|
||||||
|
<q-item-section class="full-width text-center"
|
||||||
|
>No limit</q-item-section
|
||||||
|
>
|
||||||
</q-item>
|
</q-item>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
|
||||||
<q-item v-for="(budget,index) in connectDialog.data.budgets">
|
<q-item v-for="(budget,index) in connectDialog.data.budgets">
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-input filled dense type="number" v-model="budget.budget_sats" label="Limit (sats)"></q-input>
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
type="number"
|
||||||
|
v-model="budget.budget_sats"
|
||||||
|
label="Limit (sats)"
|
||||||
|
></q-input>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-select filled dense emit-value :options="[
|
<q-select
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
emit-value
|
||||||
|
:options="[
|
||||||
{label: 'Daily', value: 'Daily'},
|
{label: 'Daily', value: 'Daily'},
|
||||||
{label: 'Weekly', value: 'Weekly'},
|
{label: 'Weekly', value: 'Weekly'},
|
||||||
{label: 'Monthly', value: 'Monthly'},
|
{label: 'Monthly', value: 'Monthly'},
|
||||||
{label: 'Yearly', value: 'Yearly'},
|
{label: 'Yearly', value: 'Yearly'},
|
||||||
{label: 'Never', value: 'Never'}
|
{label: 'Never', value: 'Never'}
|
||||||
]" label="Expires" v-model="budget.expiry"></q-select>
|
]"
|
||||||
|
label="Expires"
|
||||||
|
v-model="budget.expiry"
|
||||||
|
></q-select>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section side>
|
<q-item-section side>
|
||||||
<q-btn flat icon="delete" @click="deleteBudget(index)"></q-btn>
|
<q-btn flat icon="delete" @click="deleteBudget(index)"></q-btn>
|
||||||
|
|
@ -252,16 +379,25 @@
|
||||||
</q-item>
|
</q-item>
|
||||||
</template>
|
</template>
|
||||||
<q-item>
|
<q-item>
|
||||||
<q-btn flat icon="add" class="full-width" label="Add limit" @click="addBudget"></q-btn>
|
<q-btn
|
||||||
|
flat
|
||||||
|
icon="add"
|
||||||
|
class="full-width"
|
||||||
|
label="Add limit"
|
||||||
|
@click="addBudget"
|
||||||
|
></q-btn>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|
||||||
</q-list>
|
</q-list>
|
||||||
|
|
||||||
<q-card-actions class="q-gutter-sm">
|
<q-card-actions class="q-gutter-sm">
|
||||||
<div class="col q-mr-md">
|
<div class="col q-mr-md">
|
||||||
<q-btn unelevated color="primary" type="submit" class="full-width">Connect</q-btn>
|
<q-btn unelevated color="primary" type="submit" class="full-width"
|
||||||
|
>Connect</q-btn
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||||
|
>Cancel</q-btn
|
||||||
|
>
|
||||||
</q-card-actions>
|
</q-card-actions>
|
||||||
</q-form>
|
</q-form>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
@ -269,16 +405,14 @@
|
||||||
</div>
|
</div>
|
||||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
{% endblock %} {% 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>
|
<script>
|
||||||
|
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
el: '#vue',
|
el: "#vue",
|
||||||
mixins: [windowMixin],
|
mixins: [windowMixin],
|
||||||
delimiters: ['${', '}'],
|
delimiters: ["${", "}"],
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
selectedWallet: null,
|
selectedWallet: null,
|
||||||
|
|
@ -286,15 +420,35 @@
|
||||||
nwcEntries: [],
|
nwcEntries: [],
|
||||||
nwcsTable: {
|
nwcsTable: {
|
||||||
columns: [
|
columns: [
|
||||||
{ name: 'description', align: 'left', label: 'Description', field: 'description' },
|
{
|
||||||
{ name: 'status', align: 'left', label: 'Status', field: 'status' },
|
name: "description",
|
||||||
{ name: 'last_used', align: 'left', label: 'Last used', field: 'last_used' },
|
align: "left",
|
||||||
{ name: 'created_at', align: 'left', label: 'Created', field: 'created_at' },
|
label: "Description",
|
||||||
{ name: 'expires_at', align: 'left', label: 'Expires', field: 'expires_at' }
|
field: "description",
|
||||||
|
},
|
||||||
|
{ name: "status", align: "left", label: "Status", field: "status" },
|
||||||
|
{
|
||||||
|
name: "last_used",
|
||||||
|
align: "left",
|
||||||
|
label: "Last used",
|
||||||
|
field: "last_used",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "created_at",
|
||||||
|
align: "left",
|
||||||
|
label: "Created",
|
||||||
|
field: "created_at",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "expires_at",
|
||||||
|
align: "left",
|
||||||
|
label: "Expires",
|
||||||
|
field: "expires_at",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
pagination: {
|
pagination: {
|
||||||
rowsPerPage: 10
|
rowsPerPage: 10,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
connectDialog: {
|
connectDialog: {
|
||||||
show: false,
|
show: false,
|
||||||
|
|
@ -303,28 +457,30 @@
|
||||||
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) {
|
||||||
this.$q.notify({ color: 'red', message: 'Please select a wallet first' });
|
this.$q.notify({
|
||||||
|
color: "red",
|
||||||
|
message: "Please select a wallet first",
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
this.connectDialog.show = true;
|
this.connectDialog.show = true;
|
||||||
|
|
@ -339,24 +495,28 @@
|
||||||
},
|
},
|
||||||
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);
|
||||||
this.$q.notify({ color: 'primary', message: 'URL copied to clipboard!' });
|
this.$q.notify({
|
||||||
|
color: "primary",
|
||||||
|
message: "URL copied to clipboard!",
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.$q.notify({ color: 'red', message: 'Failed to copy URL.' });
|
this.$q.notify({ color: "red", 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() {
|
||||||
|
|
@ -364,33 +524,34 @@
|
||||||
},
|
},
|
||||||
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:
|
||||||
expiration: 'never'
|
new Date(new Date().setHours(0, 0, 0, 0)).getTime() / 1000,
|
||||||
})
|
expiration: "never",
|
||||||
|
});
|
||||||
},
|
},
|
||||||
closeConnectDialog() {
|
closeConnectDialog() {
|
||||||
this.connectDialog.show = false
|
this.connectDialog.show = false;
|
||||||
this.loadConnectDialogData();
|
this.loadConnectDialogData();
|
||||||
},
|
},
|
||||||
getWallet: function () {
|
getWallet: function () {
|
||||||
|
|
@ -405,7 +566,7 @@
|
||||||
},
|
},
|
||||||
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);
|
||||||
|
|
@ -413,62 +574,67 @@
|
||||||
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) {
|
||||||
this.$q.dialog({
|
this.$q
|
||||||
title: 'Confirm Deletion',
|
.dialog({
|
||||||
message: 'Are you sure you want to delete this connection?',
|
title: "Confirm Deletion",
|
||||||
|
message: "Are you sure you want to delete this connection?",
|
||||||
cancel: true,
|
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('DELETE', `/nwcprovider/api/v1/nwc/${pubkey}`, wallet.adminkey);
|
const response = await LNbits.api.request(
|
||||||
|
"DELETE",
|
||||||
|
`/nwcprovider/api/v1/nwc/${pubkey}`,
|
||||||
|
wallet.adminkey,
|
||||||
|
);
|
||||||
this.loadNwcs();
|
this.loadNwcs();
|
||||||
this.$q.notify({ type: 'positive', message: 'Deleted successfully' });
|
this.$q.notify({
|
||||||
|
type: "positive",
|
||||||
|
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
|
const response = await LNbits.api.request(
|
||||||
.request(
|
"GET",
|
||||||
'GET',
|
"/nwcprovider/api/v1/nwc?includeExpired=true&calculateSpendBudget=true",
|
||||||
'/nwcprovider/api/v1/nwc?includeExpired=true&calculateSpendBudget=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;
|
||||||
|
|
@ -480,15 +646,18 @@
|
||||||
for (const nwc of this.nwcs) {
|
for (const nwc of this.nwcs) {
|
||||||
const t = Quasar.utils.date.formatDate(
|
const t = Quasar.utils.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 = nwc.data.expires_at > 0 ? Quasar.utils.date.formatDate(
|
const e =
|
||||||
|
nwc.data.expires_at > 0
|
||||||
|
? Quasar.utils.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.utils.date.formatDate(
|
const l = Quasar.utils.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,
|
||||||
|
|
@ -498,47 +667,49 @@
|
||||||
pubkey: nwc.data.pubkey,
|
pubkey: nwc.data.pubkey,
|
||||||
permissions: nwc.data.permissions,
|
permissions: nwc.data.permissions,
|
||||||
budgets: [],
|
budgets: [],
|
||||||
status: "Active"
|
status: "Active",
|
||||||
}
|
};
|
||||||
if(nwc.data.expires_at > 0 && nwc.data.expires_at < new Date().getTime() / 1000){
|
if (
|
||||||
|
nwc.data.expires_at > 0 &&
|
||||||
|
nwc.data.expires_at < new Date().getTime() / 1000
|
||||||
|
) {
|
||||||
nwcTableEntry.status = "Expired";
|
nwcTableEntry.status = "Expired";
|
||||||
}
|
}
|
||||||
for (const budget of nwc.budgets) {
|
for (const budget of nwc.budgets) {
|
||||||
const createdAt = Quasar.utils.date.formatDate(
|
const createdAt = Quasar.utils.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);
|
||||||
|
|
@ -550,70 +721,79 @@
|
||||||
// timestamp
|
// timestamp
|
||||||
let expires_at = 0;
|
let expires_at = 0;
|
||||||
if (!this.connectDialog.data.neverExpires) {
|
if (!this.connectDialog.data.neverExpires) {
|
||||||
expires_at = new Date(this.connectDialog.data.expires_at).getTime() / 1000;
|
expires_at =
|
||||||
|
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: new Date(new Date().setHours(0, 0, 0, 0)).getTime() / 1000,
|
created_at:
|
||||||
|
new Date(new Date().setHours(0, 0, 0, 0)).getTime() / 1000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const wallet = this.getWallet();
|
const wallet = this.getWallet();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await LNbits.api
|
const response = await LNbits.api.request(
|
||||||
.request('PUT', '/nwcprovider/api/v1/nwc/' + keyPair.pubKey, wallet.adminkey, data);
|
"PUT",
|
||||||
this.closeConnectDialog()
|
"/nwcprovider/api/v1/nwc/" + keyPair.pubKey,
|
||||||
if (!response.data || !response.data.data || !response.data.data.pubkey) {
|
wallet.adminkey,
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
this.closeConnectDialog();
|
||||||
|
if (
|
||||||
|
!response.data ||
|
||||||
|
!response.data.data ||
|
||||||
|
!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();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
@ -1,22 +1,21 @@
|
||||||
import pytest
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from loguru import logger
|
import base64
|
||||||
import httpx
|
|
||||||
import asyncio
|
|
||||||
import bolt11
|
|
||||||
import secp256k1
|
|
||||||
import time
|
|
||||||
from typing import List, Dict
|
|
||||||
import websockets
|
|
||||||
import random
|
|
||||||
import json
|
|
||||||
from typing import List, Dict, Optional
|
|
||||||
from Cryptodome.Util.Padding import pad, unpad
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import json
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
from typing import Dict, List, Optional, Union
|
||||||
|
|
||||||
|
import bolt11
|
||||||
|
import httpx
|
||||||
|
import pytest
|
||||||
|
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
|
||||||
import base64
|
from Cryptodome.Util.Padding import pad, unpad
|
||||||
from typing import Union, Dict
|
from loguru import logger
|
||||||
|
|
||||||
wallets = {
|
wallets = {
|
||||||
"wallet1": {
|
"wallet1": {
|
||||||
"name": "wallet1",
|
"name": "wallet1",
|
||||||
|
|
@ -41,44 +40,54 @@ wallets = {
|
||||||
"id": "2faa91184177414ab14712cadafbc78f",
|
"id": "2faa91184177414ab14712cadafbc78f",
|
||||||
"admin_key": "0ffd65580a664e0aae85687f99dac7ad",
|
"admin_key": "0ffd65580a664e0aae85687f99dac7ad",
|
||||||
"balance_msats": 1000000,
|
"balance_msats": 1000000,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def check_services():
|
async def check_services():
|
||||||
# wait for http server in localhost:7777
|
# wait for http server in localhost:7777
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
resp = await client.get('http://localhost:7777')
|
resp = await client.get("http://localhost:7777")
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
break
|
break
|
||||||
except:
|
except Exception:
|
||||||
logger.info("Waiting for nostr relay @ http://localhost:7777")
|
logger.info("Waiting for nostr relay @ http://localhost:7777")
|
||||||
logger.info("Please start the required services by running `bash start.sh` if you haven't already")
|
logger.info(
|
||||||
|
"""Please start the required services by running\
|
||||||
|
`bash start.sh` if you haven't already"""
|
||||||
|
)
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
# wait lnbits @ localhost:5000
|
# wait lnbits @ localhost:5000
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
resp = await client.get('http://localhost:5002')
|
resp = await client.get("http://localhost:5002")
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
break
|
break
|
||||||
except:
|
except Exception:
|
||||||
logger.info("Waiting for lnbits @ http://localhost:5002")
|
logger.info("Waiting for lnbits @ http://localhost:5002")
|
||||||
logger.info("Please start the required services by running `bash start.sh` if you haven't already")
|
logger.info(
|
||||||
|
"""Please start the required services by running\
|
||||||
|
`bash start.sh` if you haven't already"""
|
||||||
|
)
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
async def get_wallet_balance(w:str):
|
async def get_wallet_balance(w: str):
|
||||||
api_key = wallets[w]["admin_key"]
|
api_key = wallets[w]["admin_key"]
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
resp = await client.get(f'http://localhost:5002/api/v1/wallet?api-key={api_key}')
|
resp = await client.get(
|
||||||
|
f"http://localhost:5002/api/v1/wallet?api-key={api_key}"
|
||||||
|
)
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
v = resp.json()
|
v = resp.json()
|
||||||
balance = v["balance"]
|
balance = v["balance"]
|
||||||
return balance
|
return balance
|
||||||
|
|
||||||
|
|
||||||
async def refresh_wallet_balances():
|
async def refresh_wallet_balances():
|
||||||
for w in wallets:
|
for w in wallets:
|
||||||
wallets[w]["balance_msats"] = await get_wallet_balance(w)
|
wallets[w]["balance_msats"] = await get_wallet_balance(w)
|
||||||
|
|
@ -89,48 +98,60 @@ def gen_keypair():
|
||||||
private_key_hex = bytes.hex(secp256k1._gen_private_key())
|
private_key_hex = bytes.hex(secp256k1._gen_private_key())
|
||||||
private_key = secp256k1.PrivateKey(bytes.fromhex(private_key_hex))
|
private_key = secp256k1.PrivateKey(bytes.fromhex(private_key_hex))
|
||||||
public_key = private_key.pubkey
|
public_key = private_key.pubkey
|
||||||
|
if not public_key:
|
||||||
|
raise Exception("Error generating pubkey")
|
||||||
public_key_hex = public_key.serialize().hex()[2:]
|
public_key_hex = public_key.serialize().hex()[2:]
|
||||||
return {
|
return {"priv": private_key_hex, "pub": public_key_hex}
|
||||||
"priv": private_key_hex,
|
|
||||||
"pub": public_key_hex
|
|
||||||
}
|
|
||||||
|
|
||||||
async def create_nwc(w:str, desc:str, permissions:List[str], budgets:List[Dict[str, int]], expiration: 0):
|
|
||||||
|
async def create_nwc(
|
||||||
|
w: str,
|
||||||
|
desc: str,
|
||||||
|
permissions: List[str],
|
||||||
|
budgets: List[Dict[str, int]],
|
||||||
|
expiration: int = 0,
|
||||||
|
):
|
||||||
keypair = gen_keypair()
|
keypair = gen_keypair()
|
||||||
api_key = wallets[w]["admin_key"]
|
api_key = wallets[w]["admin_key"]
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
resp = await client.put(f'http://localhost:5002/nwcprovider/api/v1/nwc/{keypair["pub"]}?api-key={api_key}', json={
|
resp = await client.put(
|
||||||
|
f'http://localhost:5002/nwcprovider/api/v1/nwc/{keypair["pub"]}?api-key={api_key}',
|
||||||
|
json={
|
||||||
"permissions": permissions,
|
"permissions": permissions,
|
||||||
"description": desc,
|
"description": desc,
|
||||||
"expires_at": time.time()+expiration if expiration > 0 else 0,
|
"expires_at": time.time() + expiration if expiration > 0 else 0,
|
||||||
"budgets": budgets
|
"budgets": budgets,
|
||||||
})
|
},
|
||||||
|
)
|
||||||
assert resp.status_code == 201
|
assert resp.status_code == 201
|
||||||
nwc = resp.json()
|
nwc = resp.json()
|
||||||
|
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
resp = await client.get(f'http://localhost:5002/nwcprovider/api/v1/pairing/{keypair["priv"]}')
|
resp = await client.get(
|
||||||
|
f'http://localhost:5002/nwcprovider/api/v1/pairing/{keypair["priv"]}'
|
||||||
|
)
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
pairing = resp.json()
|
pairing = resp.json()
|
||||||
return {
|
return {
|
||||||
"pubkey": keypair["pub"],
|
"pubkey": keypair["pub"],
|
||||||
"privkey": keypair["priv"],
|
"privkey": keypair["priv"],
|
||||||
"pairing": pairing,
|
"pairing": pairing,
|
||||||
"nwc": nwc
|
"nwc": nwc,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_nwc(w: str, pubkey: str):
|
||||||
async def delete_nwc(w:str, pubkey:str):
|
|
||||||
|
|
||||||
api_key = wallets[w]["admin_key"]
|
api_key = wallets[w]["admin_key"]
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
resp = await client.delete(f'http://localhost:5002/nwcprovider/api/v1/nwc/{pubkey}?api-key={api_key}')
|
resp = await client.delete(
|
||||||
|
f"http://localhost:5002/nwcprovider/api/v1/nwc/{pubkey}?api-key={api_key}"
|
||||||
|
)
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
return resp.json()
|
return resp.json()
|
||||||
|
|
||||||
|
|
||||||
class NWCWallet :
|
class NWCWallet:
|
||||||
def __init__(self, pairing_url):
|
def __init__(self, pairing_url):
|
||||||
# Extract from Pairing url nostr+walletconnect://provider_pub?relay=relay&secret=secret
|
# Extract from Pairing url nostr+walletconnect://provider_pub?relay=relay&secret=secret
|
||||||
self.pairing_url = pairing_url
|
self.pairing_url = pairing_url
|
||||||
|
|
@ -142,16 +163,22 @@ class NWCWallet :
|
||||||
self.shutdown = False
|
self.shutdown = False
|
||||||
self.event_queue = []
|
self.event_queue = []
|
||||||
self.subscriptions_count = 0
|
self.subscriptions_count = 0
|
||||||
self.sub_id=""
|
self.sub_id = ""
|
||||||
self.private_key = secp256k1.PrivateKey(bytes.fromhex(self.secret))
|
self.private_key = secp256k1.PrivateKey(bytes.fromhex(self.secret))
|
||||||
self.private_key_hex = self.secret
|
self.private_key_hex = self.secret
|
||||||
self.public_key = self.private_key.pubkey
|
self.public_key = self.private_key.pubkey
|
||||||
|
if not self.public_key:
|
||||||
|
raise Exception("Error generating pubkey")
|
||||||
self.public_key_hex = self.public_key.serialize().hex()[2:]
|
self.public_key_hex = self.public_key.serialize().hex()[2:]
|
||||||
|
self.task = None
|
||||||
|
|
||||||
async def close(self):
|
async def close(self):
|
||||||
self.shutdown = True
|
self.shutdown = True
|
||||||
|
if not self.ws:
|
||||||
|
raise Exception("Websocket not connected")
|
||||||
await self.ws.close()
|
await self.ws.close()
|
||||||
|
if self.task:
|
||||||
|
self.task.cancel()
|
||||||
self.connected = False
|
self.connected = False
|
||||||
|
|
||||||
async def _wait_for_connection(self):
|
async def _wait_for_connection(self):
|
||||||
|
|
@ -159,22 +186,20 @@ class NWCWallet :
|
||||||
await asyncio.sleep(0.2)
|
await asyncio.sleep(0.2)
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
asyncio.create_task(self._run())
|
self.task = asyncio.create_task(self._run())
|
||||||
await self._wait_for_connection()
|
await self._wait_for_connection()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _is_shutting_down(self):
|
def _is_shutting_down(self):
|
||||||
return self.shutdown
|
return self.shutdown
|
||||||
|
|
||||||
def _get_new_subid(self) -> str:
|
def _get_new_subid(self) -> str:
|
||||||
subid = "lnbitsnwcstest"+str(self.subscriptions_count)
|
subid = "lnbitsnwcstest" + str(self.subscriptions_count)
|
||||||
self.subscriptions_count += 1
|
self.subscriptions_count += 1
|
||||||
maxLength = 64
|
max_length = 64
|
||||||
chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
n = maxLength - len(subid)
|
n = max_length - len(subid)
|
||||||
if n > 0:
|
if n > 0:
|
||||||
for i in range(n):
|
for _ in range(n):
|
||||||
subid += chars[random.randint(0, len(chars) - 1)]
|
subid += chars[random.randint(0, len(chars) - 1)]
|
||||||
return subid
|
return subid
|
||||||
|
|
||||||
|
|
@ -188,15 +213,19 @@ class NWCWallet :
|
||||||
res_filter = {
|
res_filter = {
|
||||||
"kinds": [23195],
|
"kinds": [23195],
|
||||||
"authors": [self.provider_pub_hex],
|
"authors": [self.provider_pub_hex],
|
||||||
"since": int(time.time())
|
"since": int(time.time()),
|
||||||
}
|
}
|
||||||
await self.ws.send(self._json_dumps(["REQ", self.sub_id, res_filter]))
|
await self.ws.send(
|
||||||
|
self._json_dumps(["REQ", self.sub_id, res_filter])
|
||||||
|
)
|
||||||
while not self._is_shutting_down() and not ws.closed:
|
while not self._is_shutting_down() and not ws.closed:
|
||||||
try:
|
try:
|
||||||
reply = await ws.recv()
|
reply = await ws.recv()
|
||||||
|
if isinstance(reply, bytes):
|
||||||
|
reply = reply.decode("utf-8")
|
||||||
try:
|
try:
|
||||||
await self._on_message(ws, reply)
|
await self._on_message(ws, reply)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug("Error receiving message: " + str(e))
|
logger.debug("Error receiving message: " + str(e))
|
||||||
|
|
@ -210,33 +239,29 @@ class NWCWallet :
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
|
||||||
def _encrypt_content(self, content: str, pubkey_hex: str, iv_seed: Optional[int] = None) -> str:
|
def _encrypt_content(
|
||||||
pubkey = secp256k1.PublicKey(
|
self, content: str, pubkey_hex: str, iv_seed: Optional[int] = None
|
||||||
bytes.fromhex("02" + pubkey_hex), True)
|
) -> str:
|
||||||
shared = pubkey.tweak_mul(bytes.fromhex(
|
pubkey = secp256k1.PublicKey(bytes.fromhex("02" + pubkey_hex), True)
|
||||||
self.private_key_hex)).serialize()[1:]
|
shared = pubkey.tweak_mul(bytes.fromhex(self.private_key_hex)).serialize()[1:]
|
||||||
if not iv_seed:
|
if not iv_seed:
|
||||||
iv = Random.new().read(AES.block_size)
|
iv = Random.new().read(AES.block_size)
|
||||||
else:
|
else:
|
||||||
iv = hashlib.sha256(iv_seed.to_bytes(32, byteorder='big')).digest()
|
iv = hashlib.sha256(iv_seed.to_bytes(32, byteorder="big")).digest()
|
||||||
iv = iv[:AES.block_size]
|
iv = iv[: AES.block_size]
|
||||||
aes = AES.new(shared, AES.MODE_CBC, iv)
|
aes = AES.new(shared, AES.MODE_CBC, iv)
|
||||||
content_bytes = content.encode("utf-8")
|
content_bytes = content.encode("utf-8")
|
||||||
content_bytes = pad(content_bytes, AES.block_size)
|
content_bytes = pad(content_bytes, AES.block_size)
|
||||||
encrypted_b64 = base64.b64encode(
|
encrypted_b64 = base64.b64encode(aes.encrypt(content_bytes)).decode("ascii")
|
||||||
aes.encrypt(content_bytes)).decode("ascii")
|
iv_b64 = base64.b64encode(iv).decode("ascii")
|
||||||
ivB64 = base64.b64encode(iv).decode("ascii")
|
encrypted_content = encrypted_b64 + "?iv=" + iv_b64
|
||||||
encrypted_content = encrypted_b64 + "?iv=" + ivB64
|
|
||||||
return encrypted_content
|
return encrypted_content
|
||||||
|
|
||||||
def _decrypt_content(self, content: str, pubkey_hex: str) -> str:
|
def _decrypt_content(self, content: str, pubkey_hex: str) -> str:
|
||||||
pubkey = secp256k1.PublicKey(
|
pubkey = secp256k1.PublicKey(bytes.fromhex("02" + pubkey_hex), True)
|
||||||
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:]
|
|
||||||
(encrypted_content_b64, iv_b64) = content.split("?iv=")
|
(encrypted_content_b64, iv_b64) = content.split("?iv=")
|
||||||
encrypted_content = base64.b64decode(
|
encrypted_content = base64.b64decode(encrypted_content_b64.encode("ascii"))
|
||||||
encrypted_content_b64.encode("ascii"))
|
|
||||||
iv = base64.b64decode(iv_b64.encode("ascii"))
|
iv = base64.b64decode(iv_b64.encode("ascii"))
|
||||||
aes = AES.new(shared, AES.MODE_CBC, iv)
|
aes = AES.new(shared, AES.MODE_CBC, iv)
|
||||||
decrypted_bytes = aes.decrypt(encrypted_content)
|
decrypted_bytes = aes.decrypt(encrypted_content)
|
||||||
|
|
@ -248,65 +273,70 @@ class NWCWallet :
|
||||||
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
|
||||||
sub_id = msg[1]
|
|
||||||
event = msg[2]
|
event = msg[2]
|
||||||
nwc_pubkey = event["pubkey"]
|
nwc_pubkey = event["pubkey"]
|
||||||
content = self._decrypt_content(event["content"], nwc_pubkey)
|
content = self._decrypt_content(event["content"], nwc_pubkey)
|
||||||
content = json.loads(content)
|
content = json.loads(content)
|
||||||
self.event_queue.append({
|
self.event_queue.append(
|
||||||
|
{
|
||||||
"created_at": event["created_at"],
|
"created_at": event["created_at"],
|
||||||
"content": content,
|
"content": content,
|
||||||
"result": content["result"] if "result" in content else None,
|
"result": content["result"] if "result" in content else None,
|
||||||
"error": content["error"] if "error" in content else None,
|
"error": content["error"] if "error" in content else None,
|
||||||
"method": content["result_type"],
|
"method": content["result_type"],
|
||||||
"tags": event["tags"]
|
"tags": event["tags"],
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
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,
|
||||||
self.public_key_hex,
|
self.public_key_hex,
|
||||||
event["created_at"],
|
event["created_at"],
|
||||||
event["kind"],
|
event["kind"],
|
||||||
event["tags"],
|
event["tags"],
|
||||||
event["content"]
|
event["content"],
|
||||||
])
|
]
|
||||||
|
)
|
||||||
|
|
||||||
event_id = hashlib.sha256(signature_data.encode()).hexdigest()
|
event_id = hashlib.sha256(signature_data.encode()).hexdigest()
|
||||||
event["id"] = event_id
|
event["id"] = event_id
|
||||||
event["pubkey"] = self.public_key_hex
|
event["pubkey"] = self.public_key_hex
|
||||||
signature = (self.private_key.schnorr_sign(
|
signature = (
|
||||||
bytes.fromhex(event_id), None, raw=True)).hex()
|
self.private_key.schnorr_sign(bytes.fromhex(event_id), None, raw=True)
|
||||||
|
).hex()
|
||||||
event["sig"] = signature
|
event["sig"] = signature
|
||||||
return event
|
return event
|
||||||
|
|
||||||
|
async def send_event(self, method, params):
|
||||||
async def sendEvent(self, method,params):
|
if not self.ws:
|
||||||
|
raise Exception("Websocket not connected")
|
||||||
await self._wait_for_connection()
|
await self._wait_for_connection()
|
||||||
event = {
|
event = {
|
||||||
"created_at": int(time.time()),
|
"created_at": int(time.time()),
|
||||||
"kind": 23194,
|
"kind": 23194,
|
||||||
"tags":[
|
"tags": [
|
||||||
["p", self.provider_pub_hex],
|
["p", self.provider_pub_hex],
|
||||||
],
|
],
|
||||||
"content": json.dumps({
|
"content": json.dumps({"method": method, "params": params}),
|
||||||
"method": method,
|
|
||||||
"params": params
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
logger.debug("Sending event: " + str(event))
|
logger.debug("Sending event: " + str(event))
|
||||||
event["content"] = self._encrypt_content(event["content"], self.provider_pub_hex)
|
event["content"] = self._encrypt_content(
|
||||||
|
event["content"], self.provider_pub_hex
|
||||||
|
)
|
||||||
self._sign_event(event)
|
self._sign_event(event)
|
||||||
logger.debug("Sending event (encrypted): " + str(event))
|
logger.debug("Sending event (encrypted): " + str(event))
|
||||||
await self.ws.send(self._json_dumps(["EVENT", event]))
|
await self.ws.send(self._json_dumps(["EVENT", event]))
|
||||||
|
|
||||||
async def waitFor(self, result_type, callback=None, on_error_callback=None, timeout=10):
|
async def wait_for(
|
||||||
|
self, result_type, callback=None, on_error_callback=None, timeout=10
|
||||||
|
):
|
||||||
now = time.time()
|
now = time.time()
|
||||||
while True:
|
while True:
|
||||||
for i in range(len(self.event_queue)):
|
for i in range(len(self.event_queue)):
|
||||||
|
|
@ -328,7 +358,6 @@ class NWCWallet :
|
||||||
raise Exception("Timeout")
|
raise Exception("Timeout")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_create():
|
async def test_create():
|
||||||
await check_services()
|
await check_services()
|
||||||
|
|
@ -350,13 +379,17 @@ async def test_make_invoice():
|
||||||
nwc = await create_nwc("wallet1", "test_make_invoice", ["invoice"], [], 0)
|
nwc = await create_nwc("wallet1", "test_make_invoice", ["invoice"], [], 0)
|
||||||
wallet1 = NWCWallet(nwc["pairing"])
|
wallet1 = NWCWallet(nwc["pairing"])
|
||||||
await wallet1.start()
|
await wallet1.start()
|
||||||
await wallet1.sendEvent("make_invoice", {"amount": 1, "description": "test 123", "expiry": 1000})
|
await wallet1.send_event(
|
||||||
result, tags, error = await wallet1.waitFor("make_invoice")
|
"make_invoice", {"amount": 1, "description": "test 123", "expiry": 1000}
|
||||||
|
)
|
||||||
|
result, tags, 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.sendEvent("make_invoice", {"amount": 123000, "description":"test 123", "expiry": 1000})
|
await wallet1.send_event(
|
||||||
result, tags, error = await wallet1.waitFor("make_invoice")
|
"make_invoice", {"amount": 123000, "description": "test 123", "expiry": 1000}
|
||||||
|
)
|
||||||
|
result, tags, 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"
|
||||||
|
|
@ -384,9 +417,10 @@ async def test_lookup_invoice():
|
||||||
wallet1 = NWCWallet(nwc["pairing"])
|
wallet1 = NWCWallet(nwc["pairing"])
|
||||||
await wallet1.start()
|
await wallet1.start()
|
||||||
|
|
||||||
|
await wallet1.send_event(
|
||||||
await wallet1.sendEvent("make_invoice", {"amount": 123000, "description": "test 123", "expiry": 1000})
|
"make_invoice", {"amount": 123000, "description": "test 123", "expiry": 1000}
|
||||||
result, tags, error = await wallet1.waitFor("make_invoice")
|
)
|
||||||
|
result, tags, 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"
|
||||||
|
|
@ -401,8 +435,8 @@ async def test_lookup_invoice():
|
||||||
wallet2 = NWCWallet(nwc2["pairing"])
|
wallet2 = NWCWallet(nwc2["pairing"])
|
||||||
await wallet2.start()
|
await wallet2.start()
|
||||||
|
|
||||||
await wallet2.sendEvent("lookup_invoice", {"invoice": result["invoice"]})
|
await wallet2.send_event("lookup_invoice", {"invoice": result["invoice"]})
|
||||||
result, tags, error = await wallet2.waitFor("lookup_invoice")
|
result, tags, 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"
|
||||||
|
|
@ -423,13 +457,11 @@ async def test_get_info():
|
||||||
await check_services()
|
await check_services()
|
||||||
nwc = await create_nwc("wallet1", "test_get_info", ["info"], [], 0)
|
nwc = await create_nwc("wallet1", "test_get_info", ["info"], [], 0)
|
||||||
|
|
||||||
|
|
||||||
wallet1 = NWCWallet(nwc["pairing"])
|
wallet1 = NWCWallet(nwc["pairing"])
|
||||||
await wallet1.start()
|
await wallet1.start()
|
||||||
|
|
||||||
|
await wallet1.send_event("get_info", {})
|
||||||
await wallet1.sendEvent("get_info", {})
|
result, tags, error = await wallet1.wait_for("get_info")
|
||||||
result, tags, error = await wallet1.waitFor("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"] == ""
|
||||||
|
|
@ -445,49 +477,44 @@ async def test_get_info():
|
||||||
async def test_permisions():
|
async def test_permisions():
|
||||||
await check_services()
|
await check_services()
|
||||||
nwc = await create_nwc("wallet1", "test_permisions1", ["info"], [], 0)
|
nwc = await create_nwc("wallet1", "test_permisions1", ["info"], [], 0)
|
||||||
nwc2 = await create_nwc("wallet1", "test_permisions2", [ "pay", "invoice"], [], 0)
|
nwc2 = await create_nwc("wallet1", "test_permisions2", ["pay", "invoice"], [], 0)
|
||||||
nwc3 = await create_nwc("wallet1", "test_permisions3", ["info" , "pay", "invoice"], [], 0)
|
nwc3 = await create_nwc(
|
||||||
|
"wallet1", "test_permisions3", ["info", "pay", "invoice"], [], 0
|
||||||
|
)
|
||||||
|
|
||||||
wallet1 = NWCWallet(nwc["pairing"])
|
wallet1 = NWCWallet(nwc["pairing"])
|
||||||
wallet2 = NWCWallet(nwc2["pairing"])
|
wallet2 = NWCWallet(nwc2["pairing"])
|
||||||
wallet3 = NWCWallet(nwc3["pairing"])
|
wallet3 = NWCWallet(nwc3["pairing"])
|
||||||
await wallet1.start()
|
await wallet1.start()
|
||||||
|
|
||||||
|
await wallet1.send_event("get_info", {})
|
||||||
await wallet1.sendEvent("get_info", {})
|
result, tags, error = await wallet1.wait_for("get_info")
|
||||||
result, tags, error = await wallet1.waitFor("get_info")
|
|
||||||
assert not error
|
assert not error
|
||||||
|
|
||||||
await wallet1.sendEvent("make_invoice", {
|
await wallet1.send_event(
|
||||||
"amount": 123000,
|
"make_invoice", {"amount": 123000, "description": "test 123", "expiry": 1000}
|
||||||
"description": "test 123",
|
)
|
||||||
"expiry": 1000
|
result, tags, error = await wallet1.wait_for("make_invoice")
|
||||||
})
|
|
||||||
result, tags, error = await wallet1.waitFor("make_invoice")
|
|
||||||
assert error
|
assert error
|
||||||
|
|
||||||
await wallet1.close()
|
await wallet1.close()
|
||||||
await wallet2.start()
|
await wallet2.start()
|
||||||
|
|
||||||
await wallet2.sendEvent("get_info", {})
|
await wallet2.send_event("get_info", {})
|
||||||
result, tags, error = await wallet2.waitFor("get_info")
|
result, tags, error = await wallet2.wait_for("get_info")
|
||||||
assert error
|
assert error
|
||||||
|
|
||||||
|
await wallet2.send_event(
|
||||||
await wallet2.sendEvent("make_invoice", {
|
"make_invoice", {"amount": 123000, "description": "test 123", "expiry": 1000}
|
||||||
"amount": 123000,
|
)
|
||||||
"description": "test 123",
|
result, tags, error = await wallet2.wait_for("make_invoice")
|
||||||
"expiry": 1000
|
|
||||||
})
|
|
||||||
result, tags, error = await wallet2.waitFor("make_invoice")
|
|
||||||
assert not error
|
assert not error
|
||||||
|
|
||||||
await wallet2.close()
|
await wallet2.close()
|
||||||
await wallet3.start()
|
await wallet3.start()
|
||||||
|
|
||||||
await wallet3.sendEvent("get_info", {})
|
await wallet3.send_event("get_info", {})
|
||||||
result, tags, error = await wallet3.waitFor("get_info")
|
result, tags, 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"]
|
||||||
|
|
@ -499,25 +526,25 @@ async def test_permisions():
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_pay_invoice_and_balance():
|
async def test_pay_invoice_and_balance():
|
||||||
await check_services()
|
await check_services()
|
||||||
nwc = await create_nwc("wallet1", "test_pay_invoice_and_balance", ["invoice", "balance"], [], 0)
|
nwc = await create_nwc(
|
||||||
nwc2 = await create_nwc("wallet2", "test_pay_invoice_and_balance", ["pay", "balance"], [], 0)
|
"wallet1", "test_pay_invoice_and_balance", ["invoice", "balance"], [], 0
|
||||||
|
)
|
||||||
|
nwc2 = await create_nwc(
|
||||||
|
"wallet2", "test_pay_invoice_and_balance", ["pay", "balance"], [], 0
|
||||||
|
)
|
||||||
|
|
||||||
wallet1 = NWCWallet(nwc["pairing"])
|
wallet1 = NWCWallet(nwc["pairing"])
|
||||||
await wallet1.start()
|
await wallet1.start()
|
||||||
|
|
||||||
|
|
||||||
await refresh_wallet_balances()
|
await refresh_wallet_balances()
|
||||||
wallet1_balance = wallets["wallet1"]["balance_msats"]
|
wallet1_balance = wallets["wallet1"]["balance_msats"]
|
||||||
wallet2_balance = wallets["wallet2"]["balance_msats"]
|
wallet2_balance = wallets["wallet2"]["balance_msats"]
|
||||||
|
|
||||||
|
await wallet1.send_event(
|
||||||
|
"make_invoice", {"amount": 123000, "description": "test 123"}
|
||||||
|
)
|
||||||
|
|
||||||
await wallet1.sendEvent("make_invoice", {
|
result, tags, error = await wallet1.wait_for("make_invoice")
|
||||||
"amount": 123000,
|
|
||||||
"description": "test 123"
|
|
||||||
})
|
|
||||||
|
|
||||||
result, tags, error = await wallet1.waitFor("make_invoice")
|
|
||||||
assert not error
|
assert not error
|
||||||
assert result["invoice"]
|
assert result["invoice"]
|
||||||
|
|
||||||
|
|
@ -525,10 +552,8 @@ async def test_pay_invoice_and_balance():
|
||||||
wallet2 = NWCWallet(nwc2["pairing"])
|
wallet2 = NWCWallet(nwc2["pairing"])
|
||||||
await wallet2.start()
|
await wallet2.start()
|
||||||
|
|
||||||
await wallet2.sendEvent("pay_invoice", {
|
await wallet2.send_event("pay_invoice", {"invoice": invoice})
|
||||||
"invoice": invoice
|
result, tags, error = await wallet2.wait_for("pay_invoice")
|
||||||
})
|
|
||||||
result, tags, error = await wallet2.waitFor("pay_invoice")
|
|
||||||
assert not error
|
assert not error
|
||||||
assert result["preimage"]
|
assert result["preimage"]
|
||||||
|
|
||||||
|
|
@ -539,13 +564,13 @@ async def test_pay_invoice_and_balance():
|
||||||
assert wallet1_balance_new == wallet1_balance + 123000
|
assert wallet1_balance_new == wallet1_balance + 123000
|
||||||
assert wallet2_balance_new == wallet2_balance - 123000
|
assert wallet2_balance_new == wallet2_balance - 123000
|
||||||
|
|
||||||
await wallet1.sendEvent("get_balance", {})
|
await wallet1.send_event("get_balance", {})
|
||||||
result, tags, error = await wallet1.waitFor("get_balance")
|
result, tags, 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.sendEvent("get_balance", {})
|
await wallet2.send_event("get_balance", {})
|
||||||
result, tags, error = await wallet2.waitFor("get_balance")
|
result, tags, 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
|
||||||
|
|
||||||
|
|
@ -555,9 +580,15 @@ async def test_pay_invoice_and_balance():
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_multi_pay_invoices():
|
async def test_multi_pay_invoices():
|
||||||
nwc1 = await create_nwc("wallet1", "test_multi_pay_invoices", ["invoice", "pay", "balance"], [], 0)
|
nwc1 = await create_nwc(
|
||||||
nwc2 = await create_nwc("wallet2", "test_multi_pay_invoices", ["invoice", "pay", "balance"], [], 0)
|
"wallet1", "test_multi_pay_invoices", ["invoice", "pay", "balance"], [], 0
|
||||||
nwc3 = await create_nwc("wallet3", "test_multi_pay_invoices", ["invoice", "pay", "balance"], [], 0)
|
)
|
||||||
|
nwc2 = await create_nwc(
|
||||||
|
"wallet2", "test_multi_pay_invoices", ["invoice", "pay", "balance"], [], 0
|
||||||
|
)
|
||||||
|
nwc3 = await create_nwc(
|
||||||
|
"wallet3", "test_multi_pay_invoices", ["invoice", "pay", "balance"], [], 0
|
||||||
|
)
|
||||||
|
|
||||||
wallet1 = NWCWallet(nwc1["pairing"])
|
wallet1 = NWCWallet(nwc1["pairing"])
|
||||||
wallet2 = NWCWallet(nwc2["pairing"])
|
wallet2 = NWCWallet(nwc2["pairing"])
|
||||||
|
|
@ -572,53 +603,52 @@ async def test_multi_pay_invoices():
|
||||||
wallet2_balance = wallets["wallet2"]["balance_msats"]
|
wallet2_balance = wallets["wallet2"]["balance_msats"]
|
||||||
wallet3_balance = wallets["wallet3"]["balance_msats"]
|
wallet3_balance = wallets["wallet3"]["balance_msats"]
|
||||||
|
|
||||||
await wallet1.sendEvent("make_invoice", {
|
await wallet1.send_event(
|
||||||
"amount": 123000,
|
"make_invoice", {"amount": 123000, "description": "test 123"}
|
||||||
"description": "test 123"
|
)
|
||||||
})
|
|
||||||
|
|
||||||
result, tags, error = await wallet1.waitFor("make_invoice")
|
result, tags, error = await wallet1.wait_for("make_invoice")
|
||||||
assert not error
|
assert not error
|
||||||
assert result["invoice"]
|
assert result["invoice"]
|
||||||
invoice1 = result["invoice"]
|
invoice1 = result["invoice"]
|
||||||
|
|
||||||
await wallet1.sendEvent("make_invoice", {
|
await wallet1.send_event(
|
||||||
"amount": 123000,
|
"make_invoice", {"amount": 123000, "description": "test 123"}
|
||||||
"description": "test 123"
|
)
|
||||||
})
|
result, tags, error = await wallet1.wait_for("make_invoice")
|
||||||
result, tags, error = await wallet1.waitFor("make_invoice")
|
|
||||||
assert not error
|
assert not error
|
||||||
assert result["invoice"]
|
assert result["invoice"]
|
||||||
invoice2 = result["invoice"]
|
invoice2 = result["invoice"]
|
||||||
|
|
||||||
await wallet2.sendEvent("make_invoice", {
|
await wallet2.send_event(
|
||||||
"amount": 123000,
|
"make_invoice", {"amount": 123000, "description": "test 123"}
|
||||||
"description": "test 123"
|
)
|
||||||
})
|
result, tags, error = await wallet2.wait_for("make_invoice")
|
||||||
result, tags, error = await wallet2.waitFor("make_invoice")
|
|
||||||
assert not error
|
assert not error
|
||||||
assert result["invoice"]
|
assert result["invoice"]
|
||||||
invoice3 = result["invoice"]
|
invoice3 = result["invoice"]
|
||||||
|
|
||||||
await wallet3.sendEvent("multi_pay_invoice", {
|
await wallet3.send_event(
|
||||||
"invoices":[
|
"multi_pay_invoice",
|
||||||
{"id":"invoice1", "invoice": invoice1, "amount": 123000},
|
{
|
||||||
{"id":"invoice2", "invoice": invoice2, "amount": 123000},
|
"invoices": [
|
||||||
{ "invoice": invoice3}
|
{"id": "invoice1", "invoice": invoice1, "amount": 123000},
|
||||||
|
{"id": "invoice2", "invoice": invoice2, "amount": 123000},
|
||||||
|
{"invoice": invoice3},
|
||||||
]
|
]
|
||||||
})
|
},
|
||||||
result, tags, error = await wallet3.waitFor("multi_pay_invoice")
|
)
|
||||||
|
result, tags, error = await wallet3.wait_for("multi_pay_invoice")
|
||||||
assert not error
|
assert not error
|
||||||
d_tag = [t[1] for t in tags if t[0] == "d"][0]
|
d_tag = next((t[1] for t in tags if t[0] == "d"), None)
|
||||||
if d_tag=="invoice1":
|
if d_tag == "invoice1":
|
||||||
assert result["preimage"]
|
assert result["preimage"]
|
||||||
elif d_tag=="invoice2":
|
elif d_tag == "invoice2":
|
||||||
assert result["preimage"]
|
assert result["preimage"]
|
||||||
elif d_tag==invoice3:
|
elif d_tag == invoice3:
|
||||||
assert result["preimage"]
|
assert result["preimage"]
|
||||||
else:
|
else:
|
||||||
assert False
|
raise AssertionError("Unexpected d tag")
|
||||||
|
|
||||||
|
|
||||||
await refresh_wallet_balances()
|
await refresh_wallet_balances()
|
||||||
wallet1_balance_new = wallets["wallet1"]["balance_msats"]
|
wallet1_balance_new = wallets["wallet1"]["balance_msats"]
|
||||||
|
|
@ -629,20 +659,18 @@ async def test_multi_pay_invoices():
|
||||||
assert wallet2_balance_new == wallet2_balance + 123000
|
assert wallet2_balance_new == wallet2_balance + 123000
|
||||||
assert wallet3_balance_new == wallet3_balance - 123000 - 123000 - 123000
|
assert wallet3_balance_new == wallet3_balance - 123000 - 123000 - 123000
|
||||||
|
|
||||||
|
await wallet1.send_event("get_balance", {})
|
||||||
|
result, tags, error = await wallet1.wait_for("get_balance")
|
||||||
await wallet1.sendEvent("get_balance", {})
|
|
||||||
result, tags, error = await wallet1.waitFor("get_balance")
|
|
||||||
assert not error
|
assert not error
|
||||||
assert result["balance"] == wallet1_balance_new
|
assert result["balance"] == wallet1_balance_new
|
||||||
|
|
||||||
await wallet2.sendEvent("get_balance", {})
|
await wallet2.send_event("get_balance", {})
|
||||||
result, tags, error = await wallet2.waitFor("get_balance")
|
result, tags, 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
|
||||||
|
|
||||||
await wallet3.sendEvent("get_balance", {})
|
await wallet3.send_event("get_balance", {})
|
||||||
result, tags, error = await wallet3.waitFor("get_balance")
|
result, tags, error = await wallet3.wait_for("get_balance")
|
||||||
assert not error
|
assert not error
|
||||||
assert result["balance"] == wallet3_balance_new
|
assert result["balance"] == wallet3_balance_new
|
||||||
|
|
||||||
|
|
@ -651,14 +679,14 @@ async def test_multi_pay_invoices():
|
||||||
await wallet3.close()
|
await wallet3.close()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_insufficient_balance():
|
async def test_insufficient_balance():
|
||||||
nwc1 = await create_nwc("wallet1", "test_insufficient_balance", ["invoice", "pay", "balance"], [], 0)
|
nwc1 = await create_nwc(
|
||||||
nwc2 = await create_nwc("wallet2", "test_insufficient_balance", ["invoice", "pay", "balance"], [], 0)
|
"wallet1", "test_insufficient_balance", ["invoice", "pay", "balance"], [], 0
|
||||||
|
)
|
||||||
|
nwc2 = await create_nwc(
|
||||||
|
"wallet2", "test_insufficient_balance", ["invoice", "pay", "balance"], [], 0
|
||||||
|
)
|
||||||
await refresh_wallet_balances()
|
await refresh_wallet_balances()
|
||||||
wallet1_balance = wallets["wallet1"]["balance_msats"]
|
wallet1_balance = wallets["wallet1"]["balance_msats"]
|
||||||
amount_to_spend = wallet1_balance + 1000
|
amount_to_spend = wallet1_balance + 1000
|
||||||
|
|
@ -667,19 +695,16 @@ async def test_insufficient_balance():
|
||||||
await wallet1.start()
|
await wallet1.start()
|
||||||
await wallet2.start()
|
await wallet2.start()
|
||||||
|
|
||||||
await wallet2.sendEvent("make_invoice", {
|
await wallet2.send_event(
|
||||||
"amount": amount_to_spend,
|
"make_invoice", {"amount": amount_to_spend, "description": "test 123"}
|
||||||
"description": "test 123"
|
)
|
||||||
})
|
result, tags, error = await wallet2.wait_for("make_invoice")
|
||||||
result, tags, error = await wallet2.waitFor("make_invoice")
|
|
||||||
assert not error
|
assert not error
|
||||||
assert result["invoice"]
|
assert result["invoice"]
|
||||||
invoice = result["invoice"]
|
invoice = result["invoice"]
|
||||||
|
|
||||||
await wallet1.sendEvent("pay_invoice", {
|
await wallet1.send_event("pay_invoice", {"invoice": invoice})
|
||||||
"invoice": invoice
|
result, tags, error = await wallet1.wait_for("pay_invoice")
|
||||||
})
|
|
||||||
result, tags, error = await wallet1.waitFor("pay_invoice")
|
|
||||||
logger.info(error)
|
logger.info(error)
|
||||||
logger.info(result)
|
logger.info(result)
|
||||||
logger.info(amount_to_spend)
|
logger.info(amount_to_spend)
|
||||||
|
|
@ -687,88 +712,89 @@ async def test_insufficient_balance():
|
||||||
assert error
|
assert error
|
||||||
# The proper error code should be INSUFFICIENT_BALANCE
|
# The proper error code should be INSUFFICIENT_BALANCE
|
||||||
# but we use the more generic PAYMENT_FAILED in our implementation for simplicity
|
# but we use the more generic PAYMENT_FAILED in our implementation for simplicity
|
||||||
#assert error["code"] == "INSUFFICIENT_BALANCE"
|
# assert error["code"] == "INSUFFICIENT_BALANCE"
|
||||||
assert error["code"] == "PAYMENT_FAILED"
|
assert error["code"] == "PAYMENT_FAILED"
|
||||||
|
|
||||||
await wallet1.close()
|
await wallet1.close()
|
||||||
await wallet2.close()
|
await wallet2.close()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_expiry():
|
async def test_expiry():
|
||||||
nwc = await create_nwc("wallet3", "test_expiry", ["invoice", "pay", "balance"], [], 1)
|
nwc = await create_nwc(
|
||||||
|
"wallet3", "test_expiry", ["invoice", "pay", "balance"], [], 1
|
||||||
|
)
|
||||||
await asyncio.sleep(2)
|
await asyncio.sleep(2)
|
||||||
wallet3 = NWCWallet(nwc["pairing"])
|
wallet3 = NWCWallet(nwc["pairing"])
|
||||||
await wallet3.start()
|
await wallet3.start()
|
||||||
await wallet3.sendEvent("make_invoice", {
|
await wallet3.send_event(
|
||||||
"amount": 123000,
|
"make_invoice", {"amount": 123000, "description": "test 123"}
|
||||||
"description": "test 123"
|
)
|
||||||
})
|
result, tags, error = await wallet3.wait_for("make_invoice")
|
||||||
result, tags, error = await wallet3.waitFor("make_invoice")
|
|
||||||
assert error
|
assert error
|
||||||
assert error["code"] == "UNAUTHORIZED" , "Expected UNAUTHORIZED error, because the NWC expired"
|
assert (
|
||||||
|
error["code"] == "UNAUTHORIZED"
|
||||||
|
), "Expected UNAUTHORIZED error, because the NWC expired"
|
||||||
await wallet3.close()
|
await wallet3.close()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_budget():
|
async def test_budget():
|
||||||
nwc1 = await create_nwc("wallet1", "test_expiry", ["invoice", "pay", "balance"], [], 0)
|
nwc1 = await create_nwc(
|
||||||
nwc3 = await create_nwc("wallet3", "test_expiry", ["invoice", "pay", "balance"], [
|
"wallet1", "test_expiry", ["invoice", "pay", "balance"], [], 0
|
||||||
|
)
|
||||||
|
nwc3 = await create_nwc(
|
||||||
|
"wallet3",
|
||||||
|
"test_expiry",
|
||||||
|
["invoice", "pay", "balance"],
|
||||||
|
[
|
||||||
{
|
{
|
||||||
"budget_msats": 100000,
|
"budget_msats": 100000,
|
||||||
"refresh_window": 3600,
|
"refresh_window": 3600,
|
||||||
"created_at": time.time()
|
"created_at": int(time.time()),
|
||||||
}
|
}
|
||||||
], 0)
|
],
|
||||||
|
0,
|
||||||
|
)
|
||||||
wallet1 = NWCWallet(nwc1["pairing"])
|
wallet1 = NWCWallet(nwc1["pairing"])
|
||||||
wallet3 = NWCWallet(nwc3["pairing"])
|
wallet3 = NWCWallet(nwc3["pairing"])
|
||||||
await wallet3.start()
|
await wallet3.start()
|
||||||
await wallet1.start()
|
await wallet1.start()
|
||||||
await wallet1.sendEvent("make_invoice", {
|
await wallet1.send_event(
|
||||||
"amount": 101000,
|
"make_invoice", {"amount": 101000, "description": "Invalid"}
|
||||||
"description": "Invalid"
|
)
|
||||||
})
|
result, tags, error = await wallet1.wait_for("make_invoice")
|
||||||
result, tags, error = await wallet1.waitFor("make_invoice")
|
|
||||||
assert not error
|
assert not error
|
||||||
|
|
||||||
await wallet3.sendEvent("pay_invoice", {
|
await wallet3.send_event("pay_invoice", {"invoice": result["invoice"]})
|
||||||
"invoice": result["invoice"]
|
result, tags, error = await wallet3.wait_for("pay_invoice")
|
||||||
})
|
|
||||||
result, tags, error = await wallet3.waitFor("pay_invoice")
|
|
||||||
assert error
|
assert error
|
||||||
assert error["code"] == "QUOTA_EXCEEDED" , "Expected QUOTA_EXCEEDED error, because the budget was exceeded"
|
assert (
|
||||||
|
error["code"] == "QUOTA_EXCEEDED"
|
||||||
|
), "Expected QUOTA_EXCEEDED error, because the budget was exceeded"
|
||||||
|
|
||||||
await wallet1.sendEvent("make_invoice", {
|
await wallet1.send_event("make_invoice", {"amount": 99000, "description": "Valid"})
|
||||||
"amount": 99000,
|
result, tags, error = await wallet1.wait_for("make_invoice")
|
||||||
"description": "Valid"
|
|
||||||
})
|
|
||||||
result, tags, error = await wallet1.waitFor("make_invoice")
|
|
||||||
assert not error
|
assert not error
|
||||||
|
|
||||||
await wallet3.sendEvent("pay_invoice", {
|
await wallet3.send_event("pay_invoice", {"invoice": result["invoice"]})
|
||||||
"invoice": result["invoice"]
|
result, tags, error = await wallet3.wait_for("pay_invoice")
|
||||||
})
|
|
||||||
result, tags, error = await wallet3.waitFor("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"]
|
||||||
|
|
||||||
await wallet1.sendEvent("make_invoice", {
|
await wallet1.send_event(
|
||||||
"amount": 100000-99000+1000,
|
"make_invoice", {"amount": 100000 - 99000 + 1000, "description": "Invalid"}
|
||||||
"description": "Invalid"
|
)
|
||||||
})
|
|
||||||
|
|
||||||
result, tags, error = await wallet1.waitFor("make_invoice")
|
result, tags, error = await wallet1.wait_for("make_invoice")
|
||||||
assert not error
|
assert not error
|
||||||
|
|
||||||
await wallet3.sendEvent("pay_invoice", {
|
await wallet3.send_event("pay_invoice", {"invoice": result["invoice"]})
|
||||||
"invoice": result["invoice"]
|
result, tags, error = await wallet3.wait_for("pay_invoice")
|
||||||
})
|
|
||||||
result, tags, error = await wallet3.waitFor("pay_invoice")
|
|
||||||
assert error
|
assert error
|
||||||
assert error["code"] == "QUOTA_EXCEEDED" , "Expected QUOTA_EXCEEDED error, because the budget was exceeded"
|
assert (
|
||||||
|
error["code"] == "QUOTA_EXCEEDED"
|
||||||
|
), "Expected QUOTA_EXCEEDED error, because the budget was exceeded"
|
||||||
|
|
||||||
await wallet3.close()
|
await wallet3.close()
|
||||||
await wallet1.close()
|
await wallet1.close()
|
||||||
|
|
@ -776,63 +802,51 @@ async def test_budget():
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_budget_refresh():
|
async def test_budget_refresh():
|
||||||
nwc1 = await create_nwc("wallet1", "test_expiry", ["invoice", "pay", "balance"], [], 0)
|
nwc1 = await create_nwc(
|
||||||
nwc3 = await create_nwc("wallet3", "test_expiry", ["invoice", "pay", "balance"], [ {
|
"wallet1", "test_expiry", ["invoice", "pay", "balance"], [], 0
|
||||||
"budget_msats": 100000,
|
)
|
||||||
"refresh_window": 5,
|
nwc3 = await create_nwc(
|
||||||
"created_at": time.time()
|
"wallet3",
|
||||||
}], 0)
|
"test_expiry",
|
||||||
|
["invoice", "pay", "balance"],
|
||||||
|
[{"budget_msats": 100000, "refresh_window": 5, "created_at": int(time.time())}],
|
||||||
|
0,
|
||||||
|
)
|
||||||
wallet1 = NWCWallet(nwc1["pairing"])
|
wallet1 = NWCWallet(nwc1["pairing"])
|
||||||
wallet3 = NWCWallet(nwc3["pairing"])
|
wallet3 = NWCWallet(nwc3["pairing"])
|
||||||
await wallet3.start()
|
await wallet3.start()
|
||||||
await wallet1.start()
|
await wallet1.start()
|
||||||
await wallet1.sendEvent("make_invoice", {
|
await wallet1.send_event(
|
||||||
"amount": 100000,
|
"make_invoice", {"amount": 100000, "description": "Invalid"}
|
||||||
"description": "Invalid"
|
)
|
||||||
})
|
result, tags, error = await wallet1.wait_for("make_invoice")
|
||||||
result, tags, error = await wallet1.waitFor("make_invoice")
|
|
||||||
assert not error
|
assert not error
|
||||||
|
|
||||||
await wallet1.sendEvent("make_invoice", {
|
await wallet1.send_event(
|
||||||
"amount": 100000,
|
"make_invoice", {"amount": 100000, "description": "Invalid"}
|
||||||
"description": "Invalid"
|
)
|
||||||
})
|
result2, tags, error = await wallet1.wait_for("make_invoice")
|
||||||
result2, tags, error = await wallet1.waitFor("make_invoice")
|
|
||||||
assert not error
|
assert not error
|
||||||
|
|
||||||
await wallet3.sendEvent("pay_invoice", {
|
await wallet3.send_event("pay_invoice", {"invoice": result["invoice"]})
|
||||||
"invoice": result["invoice"]
|
result, tags, error = await wallet3.wait_for("pay_invoice")
|
||||||
})
|
|
||||||
result, tags, error = await wallet3.waitFor("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.sendEvent("pay_invoice", {
|
result, tags, error = await wallet3.wait_for("pay_invoice")
|
||||||
"invoice": result2["invoice"]
|
|
||||||
})
|
|
||||||
result, tags, error = await wallet3.waitFor("pay_invoice")
|
|
||||||
assert error
|
assert error
|
||||||
assert error["code"] == "QUOTA_EXCEEDED", "Expected QUOTA_EXCEEDED error, because the budget was exceeded"
|
assert (
|
||||||
|
error["code"] == "QUOTA_EXCEEDED"
|
||||||
|
), "Expected QUOTA_EXCEEDED error, because the budget was exceeded"
|
||||||
|
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
await wallet1.sendEvent("make_invoice", {
|
await wallet1.send_event("make_invoice", {"amount": 100000, "description": "Valid"})
|
||||||
"amount": 100000,
|
result, tags, error = await wallet1.wait_for("make_invoice")
|
||||||
"description": "Valid"
|
|
||||||
})
|
|
||||||
result, tags, error = await wallet1.waitFor("make_invoice")
|
|
||||||
assert not error
|
assert not error
|
||||||
|
|
||||||
|
await wallet3.send_event("pay_invoice", {"invoice": result["invoice"]})
|
||||||
await wallet3.sendEvent("pay_invoice", {
|
result, tags, error = await wallet3.wait_for("pay_invoice")
|
||||||
"invoice": result["invoice"]
|
|
||||||
})
|
|
||||||
result, tags, error = await wallet3.waitFor("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()
|
||||||
await wallet1.close()
|
await wallet1.close()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,41 @@
|
||||||
import json
|
import json
|
||||||
from loguru import logger
|
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
sys.path.insert(0, os.path.abspath(
|
import sys
|
||||||
os.path.join(os.path.dirname(__file__), '..', '..')))
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
|
||||||
####
|
####
|
||||||
|
|
||||||
import string
|
|
||||||
import random
|
import random
|
||||||
from NWCServiceProvider import NWCServiceProvider
|
import string
|
||||||
from unittest.mock import AsyncMock, patch
|
|
||||||
import asyncio
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from nwcp import NWCServiceProvider
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def nwc_service_provider():
|
def nwc_service_provider():
|
||||||
return NWCServiceProvider("d7b5232fba0e02e32cfe26f20cdf2c803b27ecd81052c2dd5d17e5e1a333fe58", "")
|
return NWCServiceProvider(
|
||||||
|
"d7b5232fba0e02e32cfe26f20cdf2c803b27ecd81052c2dd5d17e5e1a333fe58", ""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def nwc_service_provider2():
|
def nwc_service_provider2():
|
||||||
return NWCServiceProvider("ce40821040275f72f3074a89770db3e2744b189f204807c867840eb58565de51", "")
|
return NWCServiceProvider(
|
||||||
|
"ce40821040275f72f3074a89770db3e2744b189f204807c867840eb58565de51", ""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_supported_methods(nwc_service_provider):
|
async def test_supported_methods(nwc_service_provider):
|
||||||
def make_invoice(provider, pubkey, content):
|
def make_invoice(provider, pubkey, content):
|
||||||
return "invoice"
|
return "invoice"
|
||||||
nwc_service_provider.addRequestListener("make_invoice", make_invoice)
|
|
||||||
s = nwc_service_provider.getSupportedMethods()
|
nwc_service_provider.add_request_listener("make_invoice", make_invoice)
|
||||||
|
s = nwc_service_provider.get_supported_methods()
|
||||||
assert s == ["make_invoice"]
|
assert s == ["make_invoice"]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -38,14 +44,18 @@ async def test_encrytdecrypt(nwc_service_provider, nwc_service_provider2):
|
||||||
content = "Hello World"
|
content = "Hello World"
|
||||||
expected_enc = "qVurNVISSl/9CfREIhk5Lg==?iv=QpCo5dI9gUcoLsSMLA7o7Q=="
|
expected_enc = "qVurNVISSl/9CfREIhk5Lg==?iv=QpCo5dI9gUcoLsSMLA7o7Q=="
|
||||||
enc_a = nwc_service_provider._encrypt_content(
|
enc_a = nwc_service_provider._encrypt_content(
|
||||||
content, nwc_service_provider2.public_key_hex, 21)
|
content, nwc_service_provider2.public_key_hex, 21
|
||||||
|
)
|
||||||
enc_b = nwc_service_provider2._encrypt_content(
|
enc_b = nwc_service_provider2._encrypt_content(
|
||||||
content, nwc_service_provider.public_key_hex, 21)
|
content, nwc_service_provider.public_key_hex, 21
|
||||||
|
)
|
||||||
|
|
||||||
dec_a = nwc_service_provider2._decrypt_content(
|
dec_a = nwc_service_provider2._decrypt_content(
|
||||||
enc_a, nwc_service_provider.public_key_hex)
|
enc_a, nwc_service_provider.public_key_hex
|
||||||
|
)
|
||||||
dec_b = nwc_service_provider._decrypt_content(
|
dec_b = nwc_service_provider._decrypt_content(
|
||||||
enc_b, nwc_service_provider2.public_key_hex)
|
enc_b, nwc_service_provider2.public_key_hex
|
||||||
|
)
|
||||||
|
|
||||||
assert dec_a == content
|
assert dec_a == content
|
||||||
assert dec_b == content
|
assert dec_b == content
|
||||||
|
|
@ -62,15 +72,16 @@ async def test_signverify(nwc_service_provider, nwc_service_provider2):
|
||||||
|
|
||||||
tags = []
|
tags = []
|
||||||
for _ in range(random.choice([1, 2, 3, 4])):
|
for _ in range(random.choice([1, 2, 3, 4])):
|
||||||
tags.append([random.choice(string.ascii_letters)+"_" +
|
tags.append(
|
||||||
random.choice(string.ascii_letters), random.choice(string.ascii_letters)])
|
[
|
||||||
|
random.choice(string.ascii_letters)
|
||||||
|
+ "_"
|
||||||
|
+ random.choice(string.ascii_letters),
|
||||||
|
random.choice(string.ascii_letters),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
event = {
|
event = {"kind": 1, "content": content, "tags": tags, "created_at": 1234567890}
|
||||||
"kind": 1,
|
|
||||||
"content": content,
|
|
||||||
"tags": tags,
|
|
||||||
"created_at": 1234567890
|
|
||||||
}
|
|
||||||
|
|
||||||
signed = nwc_service_provider._sign_event(event)
|
signed = nwc_service_provider._sign_event(event)
|
||||||
assert nwc_service_provider2._verify_event(signed)
|
assert nwc_service_provider2._verify_event(signed)
|
||||||
|
|
@ -78,22 +89,17 @@ async def test_signverify(nwc_service_provider, nwc_service_provider2):
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_handle(nwc_service_provider, nwc_service_provider2):
|
async def test_handle(nwc_service_provider, nwc_service_provider2):
|
||||||
content = nwc_service_provider._json_dumps({
|
content = nwc_service_provider._json_dumps(
|
||||||
"method": "pay_invoice",
|
{"method": "pay_invoice", "params": {"invoice": "abc"}}
|
||||||
"params": {
|
)
|
||||||
"invoice": "abc"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
content = nwc_service_provider._encrypt_content(
|
content = nwc_service_provider._encrypt_content(
|
||||||
content, nwc_service_provider2.public_key_hex, 21)
|
content, nwc_service_provider2.public_key_hex, 21
|
||||||
|
)
|
||||||
event = {
|
event = {
|
||||||
"kind": 23194,
|
"kind": 23194,
|
||||||
"content": content,
|
"content": content,
|
||||||
"tags": [
|
"tags": [["p", nwc_service_provider2.public_key_hex]],
|
||||||
["p", nwc_service_provider2.public_key_hex]
|
"created_at": 1234567890,
|
||||||
],
|
|
||||||
"created_at": 1234567890
|
|
||||||
|
|
||||||
}
|
}
|
||||||
signed = nwc_service_provider._sign_event(event)
|
signed = nwc_service_provider._sign_event(event)
|
||||||
|
|
||||||
|
|
@ -101,30 +107,20 @@ async def test_handle(nwc_service_provider, nwc_service_provider2):
|
||||||
assert pubkey == nwc_service_provider.public_key_hex
|
assert pubkey == nwc_service_provider.public_key_hex
|
||||||
assert content["method"] == "pay_invoice"
|
assert content["method"] == "pay_invoice"
|
||||||
assert content["params"]["invoice"] == "abc"
|
assert content["params"]["invoice"] == "abc"
|
||||||
return [
|
return [({"preimage": "00000"}, None, [["r1", "v1"]])]
|
||||||
(
|
|
||||||
{
|
|
||||||
"preimage": "00000"
|
|
||||||
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
[
|
|
||||||
["r1", "v1"]
|
|
||||||
]
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
async def _send_pass(obj):
|
async def _send_pass(obj):
|
||||||
pass
|
pass
|
||||||
nwc_service_provider2._send=_send_pass
|
|
||||||
nwc_service_provider2.addRequestListener(
|
nwc_service_provider2._send = _send_pass
|
||||||
"pay_invoice", _handle_pay_invoice)
|
nwc_service_provider2.add_request_listener("pay_invoice", _handle_pay_invoice)
|
||||||
sent_events = await nwc_service_provider2._handle_request(signed)
|
sent_events = await nwc_service_provider2._handle_request(signed)
|
||||||
assert len(sent_events) == 1
|
assert len(sent_events) == 1
|
||||||
for revent in sent_events:
|
for revent in sent_events:
|
||||||
assert nwc_service_provider2._verify_event(revent)
|
assert nwc_service_provider2._verify_event(revent)
|
||||||
content = nwc_service_provider2._decrypt_content(
|
content = nwc_service_provider2._decrypt_content(
|
||||||
revent["content"], nwc_service_provider.public_key_hex)
|
revent["content"], nwc_service_provider.public_key_hex
|
||||||
|
)
|
||||||
logger.debug(event)
|
logger.debug(event)
|
||||||
logger.debug(revent)
|
logger.debug(revent)
|
||||||
content = json.loads(content)
|
content = json.loads(content)
|
||||||
22
views.py
22
views.py
|
|
@ -1,20 +1,26 @@
|
||||||
|
from fastapi import APIRouter, Depends, Request
|
||||||
from fastapi import Depends, Request
|
|
||||||
from starlette.responses import HTMLResponse
|
from starlette.responses import HTMLResponse
|
||||||
|
|
||||||
from lnbits.core.models import User
|
from lnbits.core.models import User
|
||||||
from lnbits.decorators import check_user_exists
|
from lnbits.decorators import check_admin, check_user_exists
|
||||||
from lnbits.decorators import check_admin
|
from lnbits.helpers import template_renderer
|
||||||
|
|
||||||
from . import nwcprovider_ext,nwcprovider_renderer
|
|
||||||
|
|
||||||
|
|
||||||
@nwcprovider_ext.get("/", response_class=HTMLResponse)
|
def nwcprovider_renderer():
|
||||||
|
return template_renderer(["nwcprovider/templates"])
|
||||||
|
|
||||||
|
|
||||||
|
nwcprovider_router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@nwcprovider_router.get("/", response_class=HTMLResponse)
|
||||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||||
return nwcprovider_renderer().TemplateResponse(
|
return nwcprovider_renderer().TemplateResponse(
|
||||||
"nwcprovider/index.html", {"request": request, "user": user.dict()}
|
"nwcprovider/index.html", {"request": request, "user": user.dict()}
|
||||||
)
|
)
|
||||||
|
|
||||||
@nwcprovider_ext.get("/admin", response_class=HTMLResponse)
|
|
||||||
|
@nwcprovider_router.get("/admin", response_class=HTMLResponse)
|
||||||
async def admin(request: Request, user: User = Depends(check_admin)):
|
async def admin(request: Request, user: User = Depends(check_admin)):
|
||||||
return nwcprovider_renderer().TemplateResponse(
|
return nwcprovider_renderer().TemplateResponse(
|
||||||
"nwcprovider/admin.html", {"request": request, "user": user.dict()}
|
"nwcprovider/admin.html", {"request": request, "user": user.dict()}
|
||||||
|
|
|
||||||
156
views_api.py
156
views_api.py
|
|
@ -1,26 +1,30 @@
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
import json
|
from typing import Dict, List, Optional
|
||||||
from typing import List, Optional, Dict
|
|
||||||
from .models import NWCRegistrationRequest, NWCGetResponse
|
|
||||||
from fastapi import Depends, Request
|
|
||||||
from lnbits.decorators import (
|
|
||||||
WalletTypeInfo,
|
|
||||||
require_admin_key
|
|
||||||
)
|
|
||||||
from fastapi.responses import JSONResponse
|
|
||||||
|
|
||||||
from . import nwcprovider_ext
|
|
||||||
|
|
||||||
from .crud import get_nwc, get_wallet_nwcs, get_all_config_nwc, create_nwc, delete_nwc, get_budgets_nwc, get_config_nwc,set_config_nwc
|
|
||||||
|
|
||||||
|
|
||||||
from lnbits.decorators import check_admin
|
|
||||||
from fastapi import Depends
|
|
||||||
import secp256k1
|
import secp256k1
|
||||||
|
from fastapi import APIRouter, Depends, Request
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from lnbits.core.models import WalletTypeInfo
|
||||||
|
from lnbits.decorators import check_admin, require_admin_key
|
||||||
|
|
||||||
|
from .crud import (
|
||||||
|
create_nwc,
|
||||||
|
delete_nwc,
|
||||||
|
get_all_config_nwc,
|
||||||
|
get_budgets_nwc,
|
||||||
|
get_config_nwc,
|
||||||
|
get_nwc,
|
||||||
|
get_wallet_nwcs,
|
||||||
|
set_config_nwc,
|
||||||
|
)
|
||||||
|
from .models import NWCGetResponse, NWCRegistrationRequest
|
||||||
from .permission import nwc_permissions
|
from .permission import nwc_permissions
|
||||||
|
|
||||||
|
nwcprovider_api_router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
# Get supported permissions
|
# Get supported permissions
|
||||||
@nwcprovider_ext.get("/api/v1/permissions", status_code=HTTPStatus.OK)
|
@nwcprovider_api_router.get("/api/v1/permissions", status_code=HTTPStatus.OK)
|
||||||
async def api_get_permissions(
|
async def api_get_permissions(
|
||||||
req: Request,
|
req: Request,
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
|
|
@ -29,60 +33,63 @@ async def api_get_permissions(
|
||||||
|
|
||||||
|
|
||||||
## Get nwc keys associated with the wallet
|
## Get nwc keys associated with the wallet
|
||||||
@nwcprovider_ext.get("/api/v1/nwc", status_code=HTTPStatus.OK, response_model=List[NWCGetResponse])
|
@nwcprovider_api_router.get(
|
||||||
|
"/api/v1/nwc", status_code=HTTPStatus.OK, response_model=List[NWCGetResponse]
|
||||||
|
)
|
||||||
async def api_get_nwcs(
|
async def api_get_nwcs(
|
||||||
req: Request,
|
req: Request,
|
||||||
includeExpired: Optional[bool] = False,
|
include_expired: bool = False,
|
||||||
calculateSpendBudget: Optional[bool] = False,
|
calculate_spent_budget: bool = False,
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
):
|
):
|
||||||
|
|
||||||
wallet_id = wallet.wallet.id
|
wallet_id = wallet.wallet.id
|
||||||
nwcs = await get_wallet_nwcs(wallet_id, includeExpired)
|
nwcs = await get_wallet_nwcs(wallet_id, include_expired)
|
||||||
out = []
|
out = []
|
||||||
for nwc in nwcs:
|
for nwc in nwcs:
|
||||||
budgets = await get_budgets_nwc(nwc.pubkey, calculateSpendBudget)
|
budgets = await get_budgets_nwc(nwc.pubkey, calculate_spent_budget)
|
||||||
res = NWCGetResponse(
|
res = NWCGetResponse(data=nwc, budgets=budgets)
|
||||||
data=nwc,
|
|
||||||
budgets=budgets
|
|
||||||
)
|
|
||||||
out.append(res)
|
out.append(res)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Get a nwc key
|
# Get a nwc key
|
||||||
@nwcprovider_ext.get("/api/v1/nwc/{pubkey}", status_code=HTTPStatus.OK, response_model=NWCGetResponse)
|
@nwcprovider_api_router.get(
|
||||||
|
"/api/v1/nwc/{pubkey}", status_code=HTTPStatus.OK, response_model=NWCGetResponse
|
||||||
|
)
|
||||||
async def api_get_nwc(
|
async def api_get_nwc(
|
||||||
req: Request,
|
req: Request,
|
||||||
pubkey: str,
|
pubkey: str,
|
||||||
includeExpired: Optional[bool] = False,
|
include_expired: Optional[bool] = False,
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key)
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
) -> NWCGetResponse:
|
) -> NWCGetResponse:
|
||||||
wallet_id = wallet.wallet.id
|
wallet_id = wallet.wallet.id
|
||||||
nwc = await get_nwc(pubkey, wallet_id, includeExpired)
|
nwc = await get_nwc(pubkey, wallet_id, include_expired)
|
||||||
res = NWCGetResponse(
|
if not nwc:
|
||||||
data=nwc,
|
raise Exception("Pubkey has no associated wallet")
|
||||||
budgets=await get_budgets_nwc(pubkey)
|
res = NWCGetResponse(data=nwc, budgets=await get_budgets_nwc(pubkey))
|
||||||
)
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
# Get pairing url for given secret
|
# Get pairing url for given secret
|
||||||
@nwcprovider_ext.get("/api/v1/pairing/{secret}", status_code=HTTPStatus.OK, response_model=str)
|
@nwcprovider_api_router.get(
|
||||||
async def api_get_pairing_url(
|
"/api/v1/pairing/{secret}", status_code=HTTPStatus.OK, response_model=str
|
||||||
req: Request,
|
)
|
||||||
secret: str
|
async def api_get_pairing_url(req: Request, secret: str) -> str:
|
||||||
) -> str:
|
pprivkey: Optional[str] = await get_config_nwc("provider_key")
|
||||||
pprivkey = await get_config_nwc("provider_key")
|
if not pprivkey:
|
||||||
|
raise Exception("Extension is not configured")
|
||||||
relay = await get_config_nwc("relay")
|
relay = await get_config_nwc("relay")
|
||||||
relay_alias = await get_config_nwc("relay_alias")
|
if not relay:
|
||||||
|
raise Exception("Extension is not configured")
|
||||||
|
relay_alias: Optional[str] = await get_config_nwc("relay_alias")
|
||||||
if relay_alias:
|
if relay_alias:
|
||||||
relay = relay_alias
|
relay = relay_alias
|
||||||
else:
|
else:
|
||||||
if relay == "nostrclient":
|
if relay == "nostrclient":
|
||||||
scheme = req.url.scheme # http or https
|
scheme = req.url.scheme # http or https
|
||||||
netloc = req.url.netloc # hostname and port
|
netloc = req.url.netloc # hostname and port
|
||||||
if scheme=="http":
|
if scheme == "http":
|
||||||
scheme = "ws"
|
scheme = "ws"
|
||||||
else:
|
else:
|
||||||
scheme = "wss"
|
scheme = "wss"
|
||||||
|
|
@ -90,48 +97,57 @@ async def api_get_pairing_url(
|
||||||
relay = f"{scheme}://{netloc}"
|
relay = f"{scheme}://{netloc}"
|
||||||
psk = secp256k1.PrivateKey(bytes.fromhex(pprivkey))
|
psk = secp256k1.PrivateKey(bytes.fromhex(pprivkey))
|
||||||
ppk = psk.pubkey
|
ppk = psk.pubkey
|
||||||
|
if not ppk:
|
||||||
|
raise Exception("Error generating pubkey")
|
||||||
ppubkey = ppk.serialize().hex()[2:]
|
ppubkey = ppk.serialize().hex()[2:]
|
||||||
url = "nostr+walletconnect://"
|
url = "nostr+walletconnect://"
|
||||||
url += ppubkey
|
url += ppubkey
|
||||||
url += "?relay="+relay
|
url += "?relay=" + relay
|
||||||
url += "&secret="+secret
|
url += "&secret=" + secret
|
||||||
#lud16=?
|
# lud16=?
|
||||||
return url
|
return url
|
||||||
|
|
||||||
|
|
||||||
## Register a new nwc key
|
## Register a new nwc key
|
||||||
@nwcprovider_ext.put("/api/v1/nwc/{pubkey}", status_code=HTTPStatus.CREATED, response_model=NWCGetResponse)
|
@nwcprovider_api_router.put(
|
||||||
|
"/api/v1/nwc/{pubkey}",
|
||||||
|
status_code=HTTPStatus.CREATED,
|
||||||
|
response_model=NWCGetResponse,
|
||||||
|
)
|
||||||
async def api_register_nwc(
|
async def api_register_nwc(
|
||||||
req: Request,
|
req: Request,
|
||||||
pubkey: str,
|
pubkey: str,
|
||||||
registration_data: NWCRegistrationRequest, # Use the Pydantic model here
|
registration_data: NWCRegistrationRequest, # Use the Pydantic model here
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key)
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
):
|
):
|
||||||
wallet_id = wallet.wallet.id
|
wallet_id = wallet.wallet.id
|
||||||
nwc = await create_nwc(pubkey, wallet_id, registration_data.description, registration_data.expires_at, registration_data.permissions, registration_data.budgets)
|
nwc = await create_nwc(
|
||||||
budgets = await get_budgets_nwc(pubkey)
|
pubkey,
|
||||||
res = NWCGetResponse(
|
wallet_id,
|
||||||
data=nwc,
|
registration_data.description,
|
||||||
budgets=budgets
|
registration_data.expires_at,
|
||||||
|
registration_data.permissions,
|
||||||
|
registration_data.budgets,
|
||||||
)
|
)
|
||||||
|
budgets = await get_budgets_nwc(pubkey)
|
||||||
|
res = NWCGetResponse(data=nwc, budgets=budgets)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
# Delete a nwc key
|
# Delete a nwc key
|
||||||
@nwcprovider_ext.delete("/api/v1/nwc/{pubkey}", status_code=HTTPStatus.OK)
|
@nwcprovider_api_router.delete("/api/v1/nwc/{pubkey}", status_code=HTTPStatus.OK)
|
||||||
async def api_delete_nwc(
|
async def api_delete_nwc(
|
||||||
req: Request,
|
req: Request, pubkey: str, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||||
pubkey: str,
|
|
||||||
wallet: WalletTypeInfo=Depends(require_admin_key)
|
|
||||||
):
|
):
|
||||||
wallet_id = wallet.wallet.id
|
wallet_id = wallet.wallet.id
|
||||||
await delete_nwc(pubkey, wallet_id)
|
await delete_nwc(pubkey, wallet_id)
|
||||||
return JSONResponse(content={"message": f"NWC key {pubkey} deleted successfully."})
|
return JSONResponse(content={"message": f"NWC key {pubkey} deleted successfully."})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Get config
|
# Get config
|
||||||
@nwcprovider_ext.get("/api/v1/config", status_code=HTTPStatus.OK, dependencies=[Depends(check_admin)])
|
@nwcprovider_api_router.get(
|
||||||
|
"/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(
|
||||||
req: Request,
|
req: Request,
|
||||||
):
|
):
|
||||||
|
|
@ -140,23 +156,23 @@ async def api_get_all_config_nwc(
|
||||||
|
|
||||||
|
|
||||||
# Get config
|
# Get config
|
||||||
@nwcprovider_ext.get("/api/v1/config/{key}", status_code=HTTPStatus.OK, dependencies=[Depends(check_admin)])
|
@nwcprovider_api_router.get(
|
||||||
async def api_get_config_nwc(
|
"/api/v1/config/{key}",
|
||||||
req: Request,
|
status_code=HTTPStatus.OK,
|
||||||
key:str
|
dependencies=[Depends(check_admin)],
|
||||||
):
|
)
|
||||||
|
async def api_get_config_nwc(req: Request, key: str):
|
||||||
config = await get_config_nwc(key)
|
config = await get_config_nwc(key)
|
||||||
out = {}
|
out = {}
|
||||||
out[key] = config
|
out[key] = config
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Set config
|
# Set config
|
||||||
@nwcprovider_ext.post("/api/v1/config", status_code=HTTPStatus.OK, dependencies=[Depends(check_admin)])
|
@nwcprovider_api_router.post(
|
||||||
async def api_set_config_nwc(
|
"/api/v1/config", status_code=HTTPStatus.OK, dependencies=[Depends(check_admin)]
|
||||||
req: Request
|
)
|
||||||
):
|
async def api_set_config_nwc(req: Request):
|
||||||
data = await req.json()
|
data = await req.json()
|
||||||
for key, value in data.items():
|
for key, value in data.items():
|
||||||
await set_config_nwc(key, value)
|
await set_config_nwc(key, value)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue