mirror of
https://github.com/lnbits/lnbits.git
synced 2025-11-24 21:10:44 +01:00
Merge branch 'main' into SCRUB
This commit is contained in:
3
.github/workflows/mypy.yml
vendored
3
.github/workflows/mypy.yml
vendored
@@ -5,10 +5,9 @@ on: [push, pull_request]
|
|||||||
jobs:
|
jobs:
|
||||||
check:
|
check:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ 'false' == 'true' }} # skip mypy for now
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
- uses: jpetrucciani/mypy-check@master
|
- uses: jpetrucciani/mypy-check@master
|
||||||
with:
|
with:
|
||||||
mypy_flags: '--install-types --non-interactive'
|
mypy_flags: '--install-types --non-interactive'
|
||||||
path: lnbits
|
path: 'lnbits'
|
||||||
|
|||||||
8
.github/workflows/regtest.yml
vendored
8
.github/workflows/regtest.yml
vendored
@@ -16,7 +16,6 @@ jobs:
|
|||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
- name: Setup Regtest
|
- name: Setup Regtest
|
||||||
run: |
|
run: |
|
||||||
docker build -t lnbits-legend .
|
|
||||||
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
|
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
|
||||||
cd docker
|
cd docker
|
||||||
chmod +x ./tests
|
chmod +x ./tests
|
||||||
@@ -39,8 +38,8 @@ jobs:
|
|||||||
LNBITS_DATA_FOLDER: ./data
|
LNBITS_DATA_FOLDER: ./data
|
||||||
LNBITS_BACKEND_WALLET_CLASS: LndRestWallet
|
LNBITS_BACKEND_WALLET_CLASS: LndRestWallet
|
||||||
LND_REST_ENDPOINT: https://localhost:8081/
|
LND_REST_ENDPOINT: https://localhost:8081/
|
||||||
LND_REST_CERT: docker/data/lnd-1/tls.cert
|
LND_REST_CERT: ./docker/data/lnd-1/tls.cert
|
||||||
LND_REST_MACAROON: docker/data/lnd-1/data/chain/bitcoin/regtest/admin.macaroon
|
LND_REST_MACAROON: ./docker/data/lnd-1/data/chain/bitcoin/regtest/admin.macaroon
|
||||||
run: |
|
run: |
|
||||||
sudo chmod -R a+rwx . && rm -rf ./data && mkdir -p ./data
|
sudo chmod -R a+rwx . && rm -rf ./data && mkdir -p ./data
|
||||||
make test-real-wallet
|
make test-real-wallet
|
||||||
@@ -57,7 +56,6 @@ jobs:
|
|||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
- name: Setup Regtest
|
- name: Setup Regtest
|
||||||
run: |
|
run: |
|
||||||
docker build -t lnbits-legend .
|
|
||||||
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
|
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
|
||||||
cd docker
|
cd docker
|
||||||
chmod +x ./tests
|
chmod +x ./tests
|
||||||
@@ -79,7 +77,7 @@ jobs:
|
|||||||
PORT: 5123
|
PORT: 5123
|
||||||
LNBITS_DATA_FOLDER: ./data
|
LNBITS_DATA_FOLDER: ./data
|
||||||
LNBITS_BACKEND_WALLET_CLASS: CLightningWallet
|
LNBITS_BACKEND_WALLET_CLASS: CLightningWallet
|
||||||
CLIGHTNING_RPC: docker/data/clightning-1/regtest/lightning-rpc
|
CLIGHTNING_RPC: ./docker/data/clightning-1/regtest/lightning-rpc
|
||||||
run: |
|
run: |
|
||||||
sudo chmod -R a+rwx . && rm -rf ./data && mkdir -p ./data
|
sudo chmod -R a+rwx . && rm -rf ./data && mkdir -p ./data
|
||||||
make test-real-wallet
|
make test-real-wallet
|
||||||
|
|||||||
59
.github/workflows/tests.yml
vendored
59
.github/workflows/tests.yml
vendored
@@ -68,11 +68,11 @@ jobs:
|
|||||||
uses: codecov/codecov-action@v3
|
uses: codecov/codecov-action@v3
|
||||||
with:
|
with:
|
||||||
file: ./coverage.xml
|
file: ./coverage.xml
|
||||||
pipenv-sqlite:
|
poetry-sqlite:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: [3.7, 3.8, 3.9]
|
python-version: [3.9]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
@@ -80,9 +80,56 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
env:
|
||||||
|
VIRTUAL_ENV: ./venv
|
||||||
|
PATH: ${{ env.VIRTUAL_ENV }}/bin:${{ env.PATH }}
|
||||||
run: |
|
run: |
|
||||||
pip install pipenv
|
python -m venv ${{ env.VIRTUAL_ENV }}
|
||||||
pipenv install --dev
|
./venv/bin/python -m pip install --upgrade pip
|
||||||
pipenv install importlib-metadata
|
./venv/bin/pip install -r requirements.txt
|
||||||
|
./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: make test-pipenv
|
run: make test
|
||||||
|
poetry-postgres:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:latest
|
||||||
|
env:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
POSTGRES_DB: postgres
|
||||||
|
ports:
|
||||||
|
# maps tcp port 5432 on service container to the host
|
||||||
|
- 5432:5432
|
||||||
|
options: >-
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: [3.9]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- name: Install dependencies
|
||||||
|
env:
|
||||||
|
VIRTUAL_ENV: ./venv
|
||||||
|
PATH: ${{ env.VIRTUAL_ENV }}/bin:${{ env.PATH }}
|
||||||
|
run: |
|
||||||
|
python -m venv ${{ env.VIRTUAL_ENV }}
|
||||||
|
./venv/bin/python -m pip install --upgrade pip
|
||||||
|
./venv/bin/pip install -r requirements.txt
|
||||||
|
./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock
|
||||||
|
- name: Run tests
|
||||||
|
env:
|
||||||
|
LNBITS_DATABASE_URL: postgres://postgres:postgres@0.0.0.0:5432/postgres
|
||||||
|
run: make test
|
||||||
|
- name: Upload coverage to Codecov
|
||||||
|
uses: codecov/codecov-action@v3
|
||||||
|
with:
|
||||||
|
file: ./coverage.xml
|
||||||
44
Pipfile
44
Pipfile
@@ -1,44 +0,0 @@
|
|||||||
[[source]]
|
|
||||||
name = "pypi"
|
|
||||||
url = "https://pypi.org/simple"
|
|
||||||
verify_ssl = true
|
|
||||||
|
|
||||||
[requires]
|
|
||||||
python_version = "3.8"
|
|
||||||
|
|
||||||
[packages]
|
|
||||||
bitstring = "*"
|
|
||||||
cerberus = "*"
|
|
||||||
ecdsa = "*"
|
|
||||||
environs = "*"
|
|
||||||
lnurl = "==0.3.6"
|
|
||||||
loguru = "*"
|
|
||||||
pyscss = "*"
|
|
||||||
shortuuid = "*"
|
|
||||||
typing-extensions = "*"
|
|
||||||
httpx = "*"
|
|
||||||
sqlalchemy-aio = "*"
|
|
||||||
embit = "*"
|
|
||||||
pyqrcode = "*"
|
|
||||||
pypng = "*"
|
|
||||||
sqlalchemy = "==1.3.23"
|
|
||||||
psycopg2-binary = "*"
|
|
||||||
aiofiles = "*"
|
|
||||||
asyncio = "*"
|
|
||||||
fastapi = "*"
|
|
||||||
uvicorn = {extras = ["standard"], version = "*"}
|
|
||||||
sse-starlette = "*"
|
|
||||||
jinja2 = "==3.0.1"
|
|
||||||
pyngrok = "*"
|
|
||||||
secp256k1 = "==0.14.0"
|
|
||||||
cffi = "==1.15.0"
|
|
||||||
pycryptodomex = "*"
|
|
||||||
|
|
||||||
[dev-packages]
|
|
||||||
black = "==20.8b1"
|
|
||||||
pytest = "*"
|
|
||||||
pytest-cov = "*"
|
|
||||||
mypy = "*"
|
|
||||||
pytest-asyncio = "*"
|
|
||||||
requests = "*"
|
|
||||||
mock = "*"
|
|
||||||
1157
Pipfile.lock
generated
1157
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ LNbits
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
# LNbits v0.3 BETA, free and open-source lightning-network wallet/accounts system
|
# LNbits v0.9 BETA, free and open-source lightning-network wallet/accounts system
|
||||||
|
|
||||||
(Join us on [https://t.me/lnbits](https://t.me/lnbits))
|
(Join us on [https://t.me/lnbits](https://t.me/lnbits))
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ nav_order: 1
|
|||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
This guide has been moved to the [installation guide](../guide/installation.md).
|
This guide has been moved to the [installation guide](../guide/installation.md).
|
||||||
To install the developer packages, use `pipenv install --dev`.
|
To install the developer packages for running tests etc before pr'ing, use `./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock black mypy isort`.
|
||||||
|
|
||||||
## Notes:
|
## Notes:
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ nav_order: 2
|
|||||||
|
|
||||||
# Basic installation
|
# Basic installation
|
||||||
|
|
||||||
You can choose between four package managers, `poetry`, `pipenv`, `venv` and `nix`.
|
You can choose between four package managers, `poetry`, `nix` and `venv`.
|
||||||
|
|
||||||
By default, LNbits will use SQLite as its database. You can also use PostgreSQL which is recommended for applications with a high load (see guide below).
|
By default, LNbits will use SQLite as its database. You can also use PostgreSQL which is recommended for applications with a high load (see guide below).
|
||||||
|
|
||||||
@@ -33,35 +33,25 @@ poetry run lnbits
|
|||||||
# To change port/host pass 'poetry run lnbits --port 9000 --host 0.0.0.0'
|
# To change port/host pass 'poetry run lnbits --port 9000 --host 0.0.0.0'
|
||||||
```
|
```
|
||||||
|
|
||||||
## Option 2: pipenv
|
## Option 2: Nix
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/lnbits/lnbits-legend.git
|
git clone https://github.com/lnbits/lnbits-legend.git
|
||||||
cd lnbits-legend/
|
cd lnbits-legend/
|
||||||
|
# Modern debian distros usually include Nix, however you can install with:
|
||||||
|
# 'sh <(curl -L https://nixos.org/nix/install) --daemon', or use setup here https://nixos.org/download.html#nix-verify-installation
|
||||||
|
|
||||||
sudo apt update && sudo apt install -y pipenv
|
nix build .#lnbits
|
||||||
pipenv install --dev
|
mkdir data
|
||||||
# pipenv --python 3.9 install --dev (if you wish to use a version of Python higher than 3.7)
|
|
||||||
pipenv shell
|
|
||||||
# pipenv --python 3.9 shell (if you wish to use a version of Python higher than 3.7)
|
|
||||||
|
|
||||||
# If any of the modules fails to install, try checking and upgrading your setupTool module
|
|
||||||
# pip install -U setuptools wheel
|
|
||||||
|
|
||||||
# install libffi/libpq in case "pipenv install" fails
|
|
||||||
# sudo apt-get install -y libffi-dev libpq-dev
|
|
||||||
|
|
||||||
mkdir data && cp .env.example .env
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Running the server
|
|
||||||
|
|
||||||
```sh
|
|
||||||
pipenv run python -m uvicorn lnbits.__main__:app --port 5000 --host 0.0.0.0
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Add the flag `--reload` for development (includes hot-reload).
|
#### Running the server
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# .env variables are currently passed when running
|
||||||
|
LNBITS_DATA_FOLDER=data LNBITS_BACKEND_WALLET_CLASS=LNbitsWallet LNBITS_ENDPOINT=https://legend.lnbits.com LNBITS_KEY=7b1a78d6c78f48b09a202f2dcb2d22eb ./result/bin/lnbits --port 9000
|
||||||
|
```
|
||||||
|
|
||||||
## Option 3: venv
|
## Option 3: venv
|
||||||
|
|
||||||
@@ -84,26 +74,6 @@ mkdir data && cp .env.example .env
|
|||||||
|
|
||||||
If you want to host LNbits on the internet, run with the option `--host 0.0.0.0`.
|
If you want to host LNbits on the internet, run with the option `--host 0.0.0.0`.
|
||||||
|
|
||||||
## Option 4: Nix
|
|
||||||
|
|
||||||
```sh
|
|
||||||
git clone https://github.com/lnbits/lnbits-legend.git
|
|
||||||
cd lnbits-legend/
|
|
||||||
# Install nix, modern debian distros usually already include
|
|
||||||
sh <(curl -L https://nixos.org/nix/install) --daemon
|
|
||||||
|
|
||||||
nix build .#lnbits
|
|
||||||
mkdir data
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Running the server
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# .env variables are currently passed when running
|
|
||||||
LNBITS_DATA_FOLDER=data LNBITS_BACKEND_WALLET_CLASS=LNbitsWallet LNBITS_ENDPOINT=https://legend.lnbits.com LNBITS_KEY=7b1a78d6c78f48b09a202f2dcb2d22eb ./result/bin/lnbits --port 9000
|
|
||||||
```
|
|
||||||
|
|
||||||
### Troubleshooting
|
### Troubleshooting
|
||||||
|
|
||||||
Problems installing? These commands have helped us install LNbits.
|
Problems installing? These commands have helped us install LNbits.
|
||||||
@@ -112,10 +82,10 @@ Problems installing? These commands have helped us install LNbits.
|
|||||||
sudo apt install pkg-config libffi-dev libpq-dev
|
sudo apt install pkg-config libffi-dev libpq-dev
|
||||||
|
|
||||||
# if the secp256k1 build fails:
|
# if the secp256k1 build fails:
|
||||||
# if you used pipenv (option 1)
|
# if you used venv
|
||||||
pipenv install setuptools wheel
|
|
||||||
# if you used venv (option 2)
|
|
||||||
./venv/bin/pip install setuptools wheel
|
./venv/bin/pip install setuptools wheel
|
||||||
|
# if you used poetry
|
||||||
|
poetry add setuptools wheel
|
||||||
# build essentials for debian/ubuntu
|
# build essentials for debian/ubuntu
|
||||||
sudo apt install python3-dev gcc build-essential
|
sudo apt install python3-dev gcc build-essential
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ from loguru import logger
|
|||||||
import lnbits.settings
|
import lnbits.settings
|
||||||
from lnbits.core.tasks import register_task_listeners
|
from lnbits.core.tasks import register_task_listeners
|
||||||
|
|
||||||
from .commands import db_migrate, handle_assets
|
|
||||||
from .core import core_app
|
from .core import core_app
|
||||||
from .core.views.generic import core_html_routes
|
from .core.views.generic import core_html_routes
|
||||||
from .helpers import (
|
from .helpers import (
|
||||||
@@ -93,7 +92,6 @@ def create_app(config_object="lnbits.settings") -> FastAPI:
|
|||||||
check_funding_source(app)
|
check_funding_source(app)
|
||||||
register_assets(app)
|
register_assets(app)
|
||||||
register_routes(app)
|
register_routes(app)
|
||||||
# register_commands(app)
|
|
||||||
register_async_tasks(app)
|
register_async_tasks(app)
|
||||||
register_exception_handlers(app)
|
register_exception_handlers(app)
|
||||||
|
|
||||||
@@ -146,12 +144,6 @@ def register_routes(app: FastAPI) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def register_commands(app: FastAPI):
|
|
||||||
"""Register Click commands."""
|
|
||||||
app.cli.add_command(db_migrate)
|
|
||||||
app.cli.add_command(handle_assets)
|
|
||||||
|
|
||||||
|
|
||||||
def register_assets(app: FastAPI):
|
def register_assets(app: FastAPI):
|
||||||
"""Serve each vendored asset separately or a bundle."""
|
"""Serve each vendored asset separately or a bundle."""
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ def decode(pr: str) -> Invoice:
|
|||||||
invoice = Invoice()
|
invoice = Invoice()
|
||||||
|
|
||||||
# decode the amount from the hrp
|
# decode the amount from the hrp
|
||||||
m = re.search("[^\d]+", hrp[2:])
|
m = re.search(r"[^\d]+", hrp[2:])
|
||||||
if m:
|
if m:
|
||||||
amountstr = hrp[2 + m.end() :]
|
amountstr = hrp[2 + m.end() :]
|
||||||
if amountstr != "":
|
if amountstr != "":
|
||||||
@@ -296,7 +296,7 @@ def _unshorten_amount(amount: str) -> int:
|
|||||||
# BOLT #11:
|
# BOLT #11:
|
||||||
# A reader SHOULD fail if `amount` contains a non-digit, or is followed by
|
# A reader SHOULD fail if `amount` contains a non-digit, or is followed by
|
||||||
# anything except a `multiplier` in the table above.
|
# anything except a `multiplier` in the table above.
|
||||||
if not re.fullmatch("\d+[pnum]?", str(amount)):
|
if not re.fullmatch(r"\d+[pnum]?", str(amount)):
|
||||||
raise ValueError("Invalid amount '{}'".format(amount))
|
raise ValueError("Invalid amount '{}'".format(amount))
|
||||||
|
|
||||||
if unit in units:
|
if unit in units:
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ async def create_wallet(
|
|||||||
async def update_wallet(
|
async def update_wallet(
|
||||||
wallet_id: str, new_name: str, conn: Optional[Connection] = None
|
wallet_id: str, new_name: str, conn: Optional[Connection] = None
|
||||||
) -> Optional[Wallet]:
|
) -> Optional[Wallet]:
|
||||||
await (conn or db).execute(
|
return await (conn or db).execute(
|
||||||
"""
|
"""
|
||||||
UPDATE wallets SET
|
UPDATE wallets SET
|
||||||
name = ?
|
name = ?
|
||||||
|
|||||||
@@ -106,6 +106,8 @@ class Payment(BaseModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def tag(self) -> Optional[str]:
|
def tag(self) -> Optional[str]:
|
||||||
|
if self.extra is None:
|
||||||
|
return ""
|
||||||
return self.extra.get("tag")
|
return self.extra.get("tag")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -109,18 +109,15 @@ async def pay_invoice(
|
|||||||
raise ValueError("Amount in invoice is too high.")
|
raise ValueError("Amount in invoice is too high.")
|
||||||
|
|
||||||
# put all parameters that don't change here
|
# put all parameters that don't change here
|
||||||
PaymentKwargs = TypedDict(
|
class PaymentKwargs(TypedDict):
|
||||||
"PaymentKwargs",
|
wallet_id: str
|
||||||
{
|
payment_request: str
|
||||||
"wallet_id": str,
|
payment_hash: str
|
||||||
"payment_request": str,
|
amount: int
|
||||||
"payment_hash": str,
|
memo: str
|
||||||
"amount": int,
|
extra: Optional[Dict]
|
||||||
"memo": str,
|
|
||||||
"extra": Optional[Dict],
|
payment_kwargs: PaymentKwargs = PaymentKwargs(
|
||||||
},
|
|
||||||
)
|
|
||||||
payment_kwargs: PaymentKwargs = dict(
|
|
||||||
wallet_id=wallet_id,
|
wallet_id=wallet_id,
|
||||||
payment_request=payment_request,
|
payment_request=payment_request,
|
||||||
payment_hash=invoice.payment_hash,
|
payment_hash=invoice.payment_hash,
|
||||||
@@ -272,6 +269,7 @@ async def perform_lnurlauth(
|
|||||||
cb = urlparse(callback)
|
cb = urlparse(callback)
|
||||||
|
|
||||||
k1 = unhexlify(parse_qs(cb.query)["k1"][0])
|
k1 = unhexlify(parse_qs(cb.query)["k1"][0])
|
||||||
|
|
||||||
key = wallet.wallet.lnurlauth_key(cb.netloc)
|
key = wallet.wallet.lnurlauth_key(cb.netloc)
|
||||||
|
|
||||||
def int_to_bytes_suitable_der(x: int) -> bytes:
|
def int_to_bytes_suitable_der(x: int) -> bytes:
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ async def dispatch_webhook(payment: Payment):
|
|||||||
data = payment.dict()
|
data = payment.dict()
|
||||||
try:
|
try:
|
||||||
logger.debug("sending webhook", payment.webhook)
|
logger.debug("sending webhook", payment.webhook)
|
||||||
r = await client.post(payment.webhook, json=data, timeout=40)
|
r = await client.post(payment.webhook, json=data, timeout=40) # type: ignore
|
||||||
await mark_webhook_sent(payment, r.status_code)
|
await mark_webhook_sent(payment, r.status_code)
|
||||||
except (httpx.ConnectError, httpx.RequestError):
|
except (httpx.ConnectError, httpx.RequestError):
|
||||||
await mark_webhook_sent(payment, -1)
|
await mark_webhook_sent(payment, -1)
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ import hashlib
|
|||||||
import json
|
import json
|
||||||
from binascii import unhexlify
|
from binascii import unhexlify
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import Dict, List, Optional, Union
|
from io import BytesIO
|
||||||
|
from typing import Dict, List, Optional, Tuple, Union
|
||||||
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
|
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
import pyqrcode
|
||||||
from fastapi import Depends, Header, Query, Request
|
from fastapi import Depends, Header, Query, Request
|
||||||
from fastapi.exceptions import HTTPException
|
from fastapi.exceptions import HTTPException
|
||||||
from fastapi.params import Body
|
from fastapi.params import Body
|
||||||
@@ -14,6 +16,7 @@ from loguru import logger
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from pydantic.fields import Field
|
from pydantic.fields import Field
|
||||||
from sse_starlette.sse import EventSourceResponse
|
from sse_starlette.sse import EventSourceResponse
|
||||||
|
from starlette.responses import HTMLResponse, StreamingResponse
|
||||||
|
|
||||||
from lnbits import bolt11, lnurl
|
from lnbits import bolt11, lnurl
|
||||||
from lnbits.core.models import Payment, Wallet
|
from lnbits.core.models import Payment, Wallet
|
||||||
@@ -185,7 +188,7 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
|
|||||||
assert (
|
assert (
|
||||||
data.lnurl_balance_check is not None
|
data.lnurl_balance_check is not None
|
||||||
), "lnurl_balance_check is required"
|
), "lnurl_balance_check is required"
|
||||||
save_balance_check(wallet.id, data.lnurl_balance_check)
|
await save_balance_check(wallet.id, data.lnurl_balance_check)
|
||||||
|
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
try:
|
try:
|
||||||
@@ -248,7 +251,7 @@ async def api_payments_pay_invoice(bolt11: str, wallet: Wallet):
|
|||||||
)
|
)
|
||||||
async def api_payments_create(
|
async def api_payments_create(
|
||||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||||
invoiceData: CreateInvoiceData = Body(...),
|
invoiceData: CreateInvoiceData = Body(...), # type: ignore
|
||||||
):
|
):
|
||||||
if invoiceData.out is True and wallet.wallet_type == 0:
|
if invoiceData.out is True and wallet.wallet_type == 0:
|
||||||
if not invoiceData.bolt11:
|
if not invoiceData.bolt11:
|
||||||
@@ -291,7 +294,7 @@ async def api_payments_pay_lnurl(
|
|||||||
timeout=40,
|
timeout=40,
|
||||||
)
|
)
|
||||||
if r.is_error:
|
if r.is_error:
|
||||||
raise httpx.ConnectError
|
raise httpx.ConnectError("LNURL callback connection error")
|
||||||
except (httpx.ConnectError, httpx.RequestError):
|
except (httpx.ConnectError, httpx.RequestError):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
@@ -354,7 +357,7 @@ async def subscribe(request: Request, wallet: Wallet):
|
|||||||
logger.debug("adding sse listener", payment_queue)
|
logger.debug("adding sse listener", payment_queue)
|
||||||
api_invoice_listeners.append(payment_queue)
|
api_invoice_listeners.append(payment_queue)
|
||||||
|
|
||||||
send_queue: asyncio.Queue[tuple[str, Payment]] = asyncio.Queue(0)
|
send_queue: asyncio.Queue[Tuple[str, Payment]] = asyncio.Queue(0)
|
||||||
|
|
||||||
async def payment_received() -> None:
|
async def payment_received() -> None:
|
||||||
while True:
|
while True:
|
||||||
@@ -393,16 +396,13 @@ async def api_payments_sse(
|
|||||||
async def api_payment(payment_hash, X_Api_Key: Optional[str] = Header(None)):
|
async def api_payment(payment_hash, X_Api_Key: Optional[str] = Header(None)):
|
||||||
# We use X_Api_Key here because we want this call to work with and without keys
|
# We use X_Api_Key here because we want this call to work with and without keys
|
||||||
# If a valid key is given, we also return the field "details", otherwise not
|
# If a valid key is given, we also return the field "details", otherwise not
|
||||||
wallet = None
|
wallet = await get_wallet_for_key(X_Api_Key) if type(X_Api_Key) == str else None
|
||||||
try:
|
|
||||||
if X_Api_Key.extra:
|
# we have to specify the wallet id here, because postgres and sqlite return internal payments in different order
|
||||||
logger.warning("No key")
|
# and get_standalone_payment otherwise just fetches the first one, causing unpredictable results
|
||||||
except:
|
|
||||||
wallet = await get_wallet_for_key(X_Api_Key)
|
|
||||||
payment = await get_standalone_payment(
|
payment = await get_standalone_payment(
|
||||||
payment_hash, wallet_id=wallet.id if wallet else None
|
payment_hash, wallet_id=wallet.id if wallet else None
|
||||||
) # we have to specify the wallet id here, because postgres and sqlite return internal payments in different order
|
)
|
||||||
# and get_standalone_payment otherwise just fetches the first one, causing unpredictable results
|
|
||||||
if payment is None:
|
if payment is None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Payment does not exist."
|
status_code=HTTPStatus.NOT_FOUND, detail="Payment does not exist."
|
||||||
@@ -488,7 +488,8 @@ async def api_lnurlscan(code: str, wallet: WalletTypeInfo = Depends(get_key_type
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tag = data["tag"]
|
tag: str = data.get("tag")
|
||||||
|
params.update(**data)
|
||||||
if tag == "channelRequest":
|
if tag == "channelRequest":
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
@@ -498,10 +499,7 @@ async def api_lnurlscan(code: str, wallet: WalletTypeInfo = Depends(get_key_type
|
|||||||
"message": "unsupported",
|
"message": "unsupported",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
elif tag == "withdrawRequest":
|
||||||
params.update(**data)
|
|
||||||
|
|
||||||
if tag == "withdrawRequest":
|
|
||||||
params.update(kind="withdraw")
|
params.update(kind="withdraw")
|
||||||
params.update(fixed=data["minWithdrawable"] == data["maxWithdrawable"])
|
params.update(fixed=data["minWithdrawable"] == data["maxWithdrawable"])
|
||||||
|
|
||||||
@@ -519,8 +517,7 @@ async def api_lnurlscan(code: str, wallet: WalletTypeInfo = Depends(get_key_type
|
|||||||
query=urlencode(qs, doseq=True)
|
query=urlencode(qs, doseq=True)
|
||||||
)
|
)
|
||||||
params.update(callback=urlunparse(parsed_callback))
|
params.update(callback=urlunparse(parsed_callback))
|
||||||
|
elif tag == "payRequest":
|
||||||
if tag == "payRequest":
|
|
||||||
params.update(kind="pay")
|
params.update(kind="pay")
|
||||||
params.update(fixed=data["minSendable"] == data["maxSendable"])
|
params.update(fixed=data["minSendable"] == data["maxSendable"])
|
||||||
|
|
||||||
@@ -538,8 +535,8 @@ async def api_lnurlscan(code: str, wallet: WalletTypeInfo = Depends(get_key_type
|
|||||||
params.update(image=data_uri)
|
params.update(image=data_uri)
|
||||||
if k == "text/email" or k == "text/identifier":
|
if k == "text/email" or k == "text/identifier":
|
||||||
params.update(targetUser=v)
|
params.update(targetUser=v)
|
||||||
|
|
||||||
params.update(commentAllowed=data.get("commentAllowed", 0))
|
params.update(commentAllowed=data.get("commentAllowed", 0))
|
||||||
|
|
||||||
except KeyError as exc:
|
except KeyError as exc:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
|
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
|
||||||
@@ -612,8 +609,8 @@ class ConversionData(BaseModel):
|
|||||||
async def api_fiat_as_sats(data: ConversionData):
|
async def api_fiat_as_sats(data: ConversionData):
|
||||||
output = {}
|
output = {}
|
||||||
if data.from_ == "sat":
|
if data.from_ == "sat":
|
||||||
output["sats"] = int(data.amount)
|
|
||||||
output["BTC"] = data.amount / 100000000
|
output["BTC"] = data.amount / 100000000
|
||||||
|
output["sats"] = int(data.amount)
|
||||||
for currency in data.to.split(","):
|
for currency in data.to.split(","):
|
||||||
output[currency.strip().upper()] = await satoshis_amount_as_fiat(
|
output[currency.strip().upper()] = await satoshis_amount_as_fiat(
|
||||||
data.amount, currency.strip()
|
data.amount, currency.strip()
|
||||||
@@ -624,3 +621,24 @@ async def api_fiat_as_sats(data: ConversionData):
|
|||||||
output["sats"] = await fiat_amount_as_satoshis(data.amount, data.from_)
|
output["sats"] = await fiat_amount_as_satoshis(data.amount, data.from_)
|
||||||
output["BTC"] = output["sats"] / 100000000
|
output["BTC"] = output["sats"] / 100000000
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
@core_app.get("/api/v1/qrcode/{data}", response_class=StreamingResponse)
|
||||||
|
async def img(request: Request, data):
|
||||||
|
qr = pyqrcode.create(data)
|
||||||
|
stream = BytesIO()
|
||||||
|
qr.svg(stream, scale=3)
|
||||||
|
stream.seek(0)
|
||||||
|
|
||||||
|
async def _generator(stream: BytesIO):
|
||||||
|
yield stream.getvalue()
|
||||||
|
|
||||||
|
return StreamingResponse(
|
||||||
|
_generator(stream),
|
||||||
|
headers={
|
||||||
|
"Content-Type": "image/svg+xml",
|
||||||
|
"Cache-Control": "no-cache, no-store, must-revalidate",
|
||||||
|
"Pragma": "no-cache",
|
||||||
|
"Expires": "0",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|||||||
@@ -55,9 +55,9 @@ async def home(request: Request, lightning: str = None):
|
|||||||
)
|
)
|
||||||
async def extensions(
|
async def extensions(
|
||||||
request: Request,
|
request: Request,
|
||||||
user: User = Depends(check_user_exists),
|
user: User = Depends(check_user_exists), # type: ignore
|
||||||
enable: str = Query(None),
|
enable: str = Query(None), # type: ignore
|
||||||
disable: str = Query(None),
|
disable: str = Query(None), # type: ignore
|
||||||
):
|
):
|
||||||
extension_to_enable = enable
|
extension_to_enable = enable
|
||||||
extension_to_disable = disable
|
extension_to_disable = disable
|
||||||
@@ -88,7 +88,7 @@ async def extensions(
|
|||||||
|
|
||||||
# Update user as his extensions have been updated
|
# Update user as his extensions have been updated
|
||||||
if extension_to_enable or extension_to_disable:
|
if extension_to_enable or extension_to_disable:
|
||||||
user = await get_user(user.id)
|
user = await get_user(user.id) # type: ignore
|
||||||
|
|
||||||
return template_renderer().TemplateResponse(
|
return template_renderer().TemplateResponse(
|
||||||
"core/extensions.html", {"request": request, "user": user.dict()}
|
"core/extensions.html", {"request": request, "user": user.dict()}
|
||||||
@@ -109,10 +109,10 @@ nothing: create everything<br>
|
|||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
async def wallet(
|
async def wallet(
|
||||||
request: Request = Query(None),
|
request: Request = Query(None), # type: ignore
|
||||||
nme: Optional[str] = Query(None),
|
nme: Optional[str] = Query(None), # type: ignore
|
||||||
usr: Optional[UUID4] = Query(None),
|
usr: Optional[UUID4] = Query(None), # type: ignore
|
||||||
wal: Optional[UUID4] = Query(None),
|
wal: Optional[UUID4] = Query(None), # type: ignore
|
||||||
):
|
):
|
||||||
user_id = usr.hex if usr else None
|
user_id = usr.hex if usr else None
|
||||||
wallet_id = wal.hex if wal else None
|
wallet_id = wal.hex if wal else None
|
||||||
@@ -121,7 +121,7 @@ async def wallet(
|
|||||||
|
|
||||||
if not user_id:
|
if not user_id:
|
||||||
user = await get_user((await create_account()).id)
|
user = await get_user((await create_account()).id)
|
||||||
logger.info(f"Create user {user.id}")
|
logger.info(f"Create user {user.id}") # type: ignore
|
||||||
else:
|
else:
|
||||||
user = await get_user(user_id)
|
user = await get_user(user_id)
|
||||||
if not user:
|
if not user:
|
||||||
@@ -135,22 +135,22 @@ async def wallet(
|
|||||||
if LNBITS_ADMIN_USERS and user_id in LNBITS_ADMIN_USERS:
|
if LNBITS_ADMIN_USERS and user_id in LNBITS_ADMIN_USERS:
|
||||||
user.admin = True
|
user.admin = True
|
||||||
if not wallet_id:
|
if not wallet_id:
|
||||||
if user.wallets and not wallet_name:
|
if user.wallets and not wallet_name: # type: ignore
|
||||||
wallet = user.wallets[0]
|
wallet = user.wallets[0] # type: ignore
|
||||||
else:
|
else:
|
||||||
wallet = await create_wallet(user_id=user.id, wallet_name=wallet_name)
|
wallet = await create_wallet(user_id=user.id, wallet_name=wallet_name) # type: ignore
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Created new wallet {wallet_name if wallet_name else '(no name)'} for user {user.id}"
|
f"Created new wallet {wallet_name if wallet_name else '(no name)'} for user {user.id}" # type: ignore
|
||||||
)
|
)
|
||||||
|
|
||||||
return RedirectResponse(
|
return RedirectResponse(
|
||||||
f"/wallet?usr={user.id}&wal={wallet.id}",
|
f"/wallet?usr={user.id}&wal={wallet.id}", # type: ignore
|
||||||
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
|
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug(f"Access wallet {wallet_name}{'of user '+ user.id if user else ''}")
|
logger.debug(f"Access wallet {wallet_name}{'of user '+ user.id if user else ''}")
|
||||||
wallet = user.get_wallet(wallet_id)
|
userwallet = user.get_wallet(wallet_id) # type: ignore
|
||||||
if not wallet:
|
if not userwallet:
|
||||||
return template_renderer().TemplateResponse(
|
return template_renderer().TemplateResponse(
|
||||||
"error.html", {"request": request, "err": "Wallet not found"}
|
"error.html", {"request": request, "err": "Wallet not found"}
|
||||||
)
|
)
|
||||||
@@ -159,10 +159,10 @@ async def wallet(
|
|||||||
"core/wallet.html",
|
"core/wallet.html",
|
||||||
{
|
{
|
||||||
"request": request,
|
"request": request,
|
||||||
"user": user.dict(),
|
"user": user.dict(), # type: ignore
|
||||||
"wallet": wallet.dict(),
|
"wallet": userwallet.dict(),
|
||||||
"service_fee": service_fee,
|
"service_fee": service_fee,
|
||||||
"web_manifest": f"/manifest/{user.id}.webmanifest",
|
"web_manifest": f"/manifest/{user.id}.webmanifest", # type: ignore
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -216,20 +216,20 @@ async def lnurl_full_withdraw_callback(request: Request):
|
|||||||
|
|
||||||
|
|
||||||
@core_html_routes.get("/deletewallet", response_class=RedirectResponse)
|
@core_html_routes.get("/deletewallet", response_class=RedirectResponse)
|
||||||
async def deletewallet(request: Request, wal: str = Query(...), usr: str = Query(...)):
|
async def deletewallet(request: Request, wal: str = Query(...), usr: str = Query(...)): # type: ignore
|
||||||
user = await get_user(usr)
|
user = await get_user(usr)
|
||||||
user_wallet_ids = [u.id for u in user.wallets]
|
user_wallet_ids = [u.id for u in user.wallets] # type: ignore
|
||||||
|
|
||||||
if wal not in user_wallet_ids:
|
if wal not in user_wallet_ids:
|
||||||
raise HTTPException(HTTPStatus.FORBIDDEN, "Not your wallet.")
|
raise HTTPException(HTTPStatus.FORBIDDEN, "Not your wallet.")
|
||||||
else:
|
else:
|
||||||
await delete_wallet(user_id=user.id, wallet_id=wal)
|
await delete_wallet(user_id=user.id, wallet_id=wal) # type: ignore
|
||||||
user_wallet_ids.remove(wal)
|
user_wallet_ids.remove(wal)
|
||||||
logger.debug("Deleted wallet {wal} of user {user.id}")
|
logger.debug("Deleted wallet {wal} of user {user.id}")
|
||||||
|
|
||||||
if user_wallet_ids:
|
if user_wallet_ids:
|
||||||
return RedirectResponse(
|
return RedirectResponse(
|
||||||
url_for("/wallet", usr=user.id, wal=user_wallet_ids[0]),
|
url_for("/wallet", usr=user.id, wal=user_wallet_ids[0]), # type: ignore
|
||||||
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
|
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -242,7 +242,7 @@ async def deletewallet(request: Request, wal: str = Query(...), usr: str = Query
|
|||||||
async def lnurl_balance_notify(request: Request, service: str):
|
async def lnurl_balance_notify(request: Request, service: str):
|
||||||
bc = await get_balance_check(request.query_params.get("wal"), service)
|
bc = await get_balance_check(request.query_params.get("wal"), service)
|
||||||
if bc:
|
if bc:
|
||||||
redeem_lnurl_withdraw(bc.wallet, bc.url)
|
await redeem_lnurl_withdraw(bc.wallet, bc.url)
|
||||||
|
|
||||||
|
|
||||||
@core_html_routes.get(
|
@core_html_routes.get(
|
||||||
@@ -252,7 +252,7 @@ async def lnurlwallet(request: Request):
|
|||||||
async with db.connect() as conn:
|
async with db.connect() as conn:
|
||||||
account = await create_account(conn=conn)
|
account = await create_account(conn=conn)
|
||||||
user = await get_user(account.id, conn=conn)
|
user = await get_user(account.id, conn=conn)
|
||||||
wallet = await create_wallet(user_id=user.id, conn=conn)
|
wallet = await create_wallet(user_id=user.id, conn=conn) # type: ignore
|
||||||
|
|
||||||
asyncio.create_task(
|
asyncio.create_task(
|
||||||
redeem_lnurl_withdraw(
|
redeem_lnurl_withdraw(
|
||||||
@@ -265,7 +265,7 @@ async def lnurlwallet(request: Request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return RedirectResponse(
|
return RedirectResponse(
|
||||||
f"/wallet?usr={user.id}&wal={wallet.id}",
|
f"/wallet?usr={user.id}&wal={wallet.id}", # type: ignore
|
||||||
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
|
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
from cerberus import Validator # type: ignore
|
from cerberus import Validator # type: ignore
|
||||||
from fastapi import status
|
from fastapi import status
|
||||||
@@ -29,20 +30,21 @@ class KeyChecker(SecurityBase):
|
|||||||
self._key_type = "invoice"
|
self._key_type = "invoice"
|
||||||
self._api_key = api_key
|
self._api_key = api_key
|
||||||
if api_key:
|
if api_key:
|
||||||
self.model: APIKey = APIKey(
|
key = APIKey(
|
||||||
**{"in": APIKeyIn.query},
|
**{"in": APIKeyIn.query},
|
||||||
name="X-API-KEY",
|
name="X-API-KEY",
|
||||||
description="Wallet API Key - QUERY",
|
description="Wallet API Key - QUERY",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.model: APIKey = APIKey(
|
key = APIKey(
|
||||||
**{"in": APIKeyIn.header},
|
**{"in": APIKeyIn.header},
|
||||||
name="X-API-KEY",
|
name="X-API-KEY",
|
||||||
description="Wallet API Key - HEADER",
|
description="Wallet API Key - HEADER",
|
||||||
)
|
)
|
||||||
self.wallet = None
|
self.wallet = None # type: ignore
|
||||||
|
self.model: APIKey = key
|
||||||
|
|
||||||
async def __call__(self, request: Request) -> Wallet:
|
async def __call__(self, request: Request):
|
||||||
try:
|
try:
|
||||||
key_value = (
|
key_value = (
|
||||||
self._api_key
|
self._api_key
|
||||||
@@ -52,7 +54,7 @@ class KeyChecker(SecurityBase):
|
|||||||
# FIXME: Find another way to validate the key. A fetch from DB should be avoided here.
|
# FIXME: Find another way to validate the key. A fetch from DB should be avoided here.
|
||||||
# Also, we should not return the wallet here - thats silly.
|
# Also, we should not return the wallet here - thats silly.
|
||||||
# Possibly store it in a Redis DB
|
# Possibly store it in a Redis DB
|
||||||
self.wallet = await get_wallet_for_key(key_value, self._key_type)
|
self.wallet = await get_wallet_for_key(key_value, self._key_type) # type: ignore
|
||||||
if not self.wallet:
|
if not self.wallet:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.UNAUTHORIZED,
|
status_code=HTTPStatus.UNAUTHORIZED,
|
||||||
@@ -120,8 +122,8 @@ api_key_query = APIKeyQuery(
|
|||||||
|
|
||||||
async def get_key_type(
|
async def get_key_type(
|
||||||
r: Request,
|
r: Request,
|
||||||
api_key_header: str = Security(api_key_header),
|
api_key_header: str = Security(api_key_header), # type: ignore
|
||||||
api_key_query: str = Security(api_key_query),
|
api_key_query: str = Security(api_key_query), # type: ignore
|
||||||
) -> WalletTypeInfo:
|
) -> WalletTypeInfo:
|
||||||
# 0: admin
|
# 0: admin
|
||||||
# 1: invoice
|
# 1: invoice
|
||||||
@@ -134,9 +136,9 @@ async def get_key_type(
|
|||||||
token = api_key_header if api_key_header else api_key_query
|
token = api_key_header if api_key_header else api_key_query
|
||||||
|
|
||||||
try:
|
try:
|
||||||
checker = WalletAdminKeyChecker(api_key=token)
|
admin_checker = WalletAdminKeyChecker(api_key=token)
|
||||||
await checker.__call__(r)
|
await admin_checker.__call__(r)
|
||||||
wallet = WalletTypeInfo(0, checker.wallet)
|
wallet = WalletTypeInfo(0, admin_checker.wallet) # type: ignore
|
||||||
if (LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS) and (
|
if (LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS) and (
|
||||||
LNBITS_ADMIN_EXTENSIONS and pathname in LNBITS_ADMIN_EXTENSIONS
|
LNBITS_ADMIN_EXTENSIONS and pathname in LNBITS_ADMIN_EXTENSIONS
|
||||||
):
|
):
|
||||||
@@ -153,9 +155,9 @@ async def get_key_type(
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
try:
|
try:
|
||||||
checker = WalletInvoiceKeyChecker(api_key=token)
|
invoice_checker = WalletInvoiceKeyChecker(api_key=token)
|
||||||
await checker.__call__(r)
|
await invoice_checker.__call__(r)
|
||||||
wallet = WalletTypeInfo(1, checker.wallet)
|
wallet = WalletTypeInfo(1, invoice_checker.wallet) # type: ignore
|
||||||
if (LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS) and (
|
if (LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS) and (
|
||||||
LNBITS_ADMIN_EXTENSIONS and pathname in LNBITS_ADMIN_EXTENSIONS
|
LNBITS_ADMIN_EXTENSIONS and pathname in LNBITS_ADMIN_EXTENSIONS
|
||||||
):
|
):
|
||||||
@@ -167,15 +169,16 @@ async def get_key_type(
|
|||||||
if e.status_code == HTTPStatus.BAD_REQUEST:
|
if e.status_code == HTTPStatus.BAD_REQUEST:
|
||||||
raise
|
raise
|
||||||
if e.status_code == HTTPStatus.UNAUTHORIZED:
|
if e.status_code == HTTPStatus.UNAUTHORIZED:
|
||||||
return WalletTypeInfo(2, None)
|
return WalletTypeInfo(2, None) # type: ignore
|
||||||
except:
|
except:
|
||||||
raise
|
raise
|
||||||
|
return wallet
|
||||||
|
|
||||||
|
|
||||||
async def require_admin_key(
|
async def require_admin_key(
|
||||||
r: Request,
|
r: Request,
|
||||||
api_key_header: str = Security(api_key_header),
|
api_key_header: str = Security(api_key_header), # type: ignore
|
||||||
api_key_query: str = Security(api_key_query),
|
api_key_query: str = Security(api_key_query), # type: ignore
|
||||||
):
|
):
|
||||||
token = api_key_header if api_key_header else api_key_query
|
token = api_key_header if api_key_header else api_key_query
|
||||||
|
|
||||||
@@ -193,8 +196,8 @@ async def require_admin_key(
|
|||||||
|
|
||||||
async def require_invoice_key(
|
async def require_invoice_key(
|
||||||
r: Request,
|
r: Request,
|
||||||
api_key_header: str = Security(api_key_header),
|
api_key_header: str = Security(api_key_header), # type: ignore
|
||||||
api_key_query: str = Security(api_key_query),
|
api_key_query: str = Security(api_key_query), # type: ignore
|
||||||
):
|
):
|
||||||
token = api_key_header if api_key_header else api_key_query
|
token = api_key_header if api_key_header else api_key_query
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class ExtensionManager:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def extensions(self) -> List[Extension]:
|
def extensions(self) -> List[Extension]:
|
||||||
output = []
|
output: List[Extension] = []
|
||||||
|
|
||||||
if "all" in self._disabled:
|
if "all" in self._disabled:
|
||||||
return output
|
return output
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class Jinja2Templates(templating.Jinja2Templates):
|
|||||||
self.env = self.get_environment(loader)
|
self.env = self.get_environment(loader)
|
||||||
|
|
||||||
def get_environment(self, loader: "jinja2.BaseLoader") -> "jinja2.Environment":
|
def get_environment(self, loader: "jinja2.BaseLoader") -> "jinja2.Environment":
|
||||||
@jinja2.contextfunction
|
@jinja2.pass_context
|
||||||
def url_for(context: dict, name: str, **path_params: typing.Any) -> str:
|
def url_for(context: dict, name: str, **path_params: typing.Any) -> str:
|
||||||
request: Request = context["request"]
|
request: Request = context["request"]
|
||||||
return request.app.url_path_for(name, **path_params)
|
return request.app.url_path_for(name, **path_params)
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ async def webhook_handler():
|
|||||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
internal_invoice_queue = asyncio.Queue(0)
|
internal_invoice_queue: asyncio.Queue = asyncio.Queue(0)
|
||||||
|
|
||||||
|
|
||||||
async def internal_invoice_listener():
|
async def internal_invoice_listener():
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from typing import Callable, NamedTuple
|
from typing import Callable, List, NamedTuple
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -227,10 +227,10 @@ async def btc_price(currency: str) -> float:
|
|||||||
"TO": currency.upper(),
|
"TO": currency.upper(),
|
||||||
"to": currency.lower(),
|
"to": currency.lower(),
|
||||||
}
|
}
|
||||||
rates = []
|
rates: List[float] = []
|
||||||
tasks = []
|
tasks: List[asyncio.Task] = []
|
||||||
|
|
||||||
send_channel = asyncio.Queue()
|
send_channel: asyncio.Queue = asyncio.Queue()
|
||||||
|
|
||||||
async def controller():
|
async def controller():
|
||||||
failures = 0
|
failures = 0
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ from typing import AsyncGenerator, Dict, Optional
|
|||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from websockets import connect
|
|
||||||
|
# TODO: https://github.com/lnbits/lnbits-legend/issues/764
|
||||||
|
# mypy https://github.com/aaugustin/websockets/issues/940
|
||||||
|
from websockets import connect # type: ignore
|
||||||
from websockets.exceptions import (
|
from websockets.exceptions import (
|
||||||
ConnectionClosed,
|
ConnectionClosed,
|
||||||
ConnectionClosedError,
|
ConnectionClosedError,
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class FakeWallet(Wallet):
|
|||||||
logger.info(
|
logger.info(
|
||||||
"FakeWallet funding source is for using LNbits as a centralised, stand-alone payment system with brrrrrr."
|
"FakeWallet funding source is for using LNbits as a centralised, stand-alone payment system with brrrrrr."
|
||||||
)
|
)
|
||||||
return StatusResponse(None, float("inf"))
|
return StatusResponse(None, 1000000000)
|
||||||
|
|
||||||
async def create_invoice(
|
async def create_invoice(
|
||||||
self,
|
self,
|
||||||
@@ -82,7 +82,7 @@ class FakeWallet(Wallet):
|
|||||||
invoice = decode(bolt11)
|
invoice = decode(bolt11)
|
||||||
if (
|
if (
|
||||||
hasattr(invoice, "checking_id")
|
hasattr(invoice, "checking_id")
|
||||||
and invoice.checking_id[6:] == data["privkey"][:6]
|
and invoice.checking_id[6:] == data["privkey"][:6] # type: ignore
|
||||||
):
|
):
|
||||||
return PaymentResponse(True, invoice.payment_hash, 0)
|
return PaymentResponse(True, invoice.payment_hash, 0)
|
||||||
else:
|
else:
|
||||||
@@ -97,7 +97,7 @@ class FakeWallet(Wallet):
|
|||||||
return PaymentStatus(None)
|
return PaymentStatus(None)
|
||||||
|
|
||||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
self.queue = asyncio.Queue(0)
|
self.queue: asyncio.Queue = asyncio.Queue(0)
|
||||||
while True:
|
while True:
|
||||||
value = await self.queue.get()
|
value = await self.queue.get()
|
||||||
yield value
|
yield value
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ class LNPayWallet(Wallet):
|
|||||||
return PaymentStatus(statuses[r.json()["settled"]])
|
return PaymentStatus(statuses[r.json()["settled"]])
|
||||||
|
|
||||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
self.queue = asyncio.Queue(0)
|
self.queue: asyncio.Queue = asyncio.Queue(0)
|
||||||
while True:
|
while True:
|
||||||
value = await self.queue.get()
|
value = await self.queue.get()
|
||||||
yield value
|
yield value
|
||||||
|
|||||||
@@ -73,10 +73,10 @@ class AESCipher(object):
|
|||||||
final_key += key
|
final_key += key
|
||||||
return final_key[:output]
|
return final_key[:output]
|
||||||
|
|
||||||
def decrypt(self, encrypted: str) -> str:
|
def decrypt(self, encrypted: str) -> str: # type: ignore
|
||||||
"""Decrypts a string using AES-256-CBC."""
|
"""Decrypts a string using AES-256-CBC."""
|
||||||
passphrase = self.passphrase
|
passphrase = self.passphrase
|
||||||
encrypted = base64.b64decode(encrypted)
|
encrypted = base64.b64decode(encrypted) # type: ignore
|
||||||
assert encrypted[0:8] == b"Salted__"
|
assert encrypted[0:8] == b"Salted__"
|
||||||
salt = encrypted[8:16]
|
salt = encrypted[8:16]
|
||||||
key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16)
|
key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16)
|
||||||
@@ -84,7 +84,7 @@ class AESCipher(object):
|
|||||||
iv = key_iv[32:]
|
iv = key_iv[32:]
|
||||||
aes = AES.new(key, AES.MODE_CBC, iv)
|
aes = AES.new(key, AES.MODE_CBC, iv)
|
||||||
try:
|
try:
|
||||||
return self.unpad(aes.decrypt(encrypted[16:])).decode()
|
return self.unpad(aes.decrypt(encrypted[16:])).decode() # type: ignore
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
raise ValueError("Wrong passphrase")
|
raise ValueError("Wrong passphrase")
|
||||||
|
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ class OpenNodeWallet(Wallet):
|
|||||||
return PaymentStatus(statuses[r.json()["data"]["status"]])
|
return PaymentStatus(statuses[r.json()["data"]["status"]])
|
||||||
|
|
||||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
self.queue = asyncio.Queue(0)
|
self.queue: asyncio.Queue = asyncio.Queue(0)
|
||||||
while True:
|
while True:
|
||||||
value = await self.queue.get()
|
value = await self.queue.get()
|
||||||
yield value
|
yield value
|
||||||
|
|||||||
7
mypy.ini
7
mypy.ini
@@ -1,7 +1,8 @@
|
|||||||
[mypy]
|
[mypy]
|
||||||
ignore_missing_imports = True
|
ignore_missing_imports = True
|
||||||
exclude = lnbits/wallets/lnd_grpc_files/
|
exclude = (?x)(
|
||||||
exclude = lnbits/extensions/
|
^lnbits/extensions.
|
||||||
|
| ^lnbits/wallets/lnd_grpc_files.
|
||||||
|
)
|
||||||
[mypy-lnbits.wallets.lnd_grpc_files.*]
|
[mypy-lnbits.wallets.lnd_grpc_files.*]
|
||||||
follow_imports = skip
|
follow_imports = skip
|
||||||
|
|||||||
1
result
Symbolic link
1
result
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
/nix/store/ds9c48q7hnkdmpzy3aq14kc1x9wrrszd-python3.9-lnbits-0.1.0
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
import pytest_asyncio
|
import pytest_asyncio
|
||||||
from lnbits.core.crud import get_wallet
|
from lnbits.core.crud import get_wallet
|
||||||
|
from lnbits.core.views.api import api_payment
|
||||||
|
|
||||||
from ...helpers import get_random_invoice_data
|
from ...helpers import get_random_invoice_data
|
||||||
|
|
||||||
@@ -155,3 +156,26 @@ async def test_decode_invoice(client, invoice):
|
|||||||
)
|
)
|
||||||
assert response.status_code < 300
|
assert response.status_code < 300
|
||||||
assert response.json()["payment_hash"] == invoice["payment_hash"]
|
assert response.json()["payment_hash"] == invoice["payment_hash"]
|
||||||
|
|
||||||
|
|
||||||
|
# check api_payment() internal function call (NOT API): payment status
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_api_payment_without_key(invoice):
|
||||||
|
# check the payment status
|
||||||
|
response = await api_payment(invoice["payment_hash"])
|
||||||
|
assert type(response) == dict
|
||||||
|
assert response["paid"] == True
|
||||||
|
# no key, that's why no "details"
|
||||||
|
assert "details" not in response
|
||||||
|
|
||||||
|
|
||||||
|
# check api_payment() internal function call (NOT API): payment status
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_api_payment_with_key(invoice, inkey_headers_from):
|
||||||
|
# check the payment status
|
||||||
|
response = await api_payment(
|
||||||
|
invoice["payment_hash"], inkey_headers_from["X-Api-Key"]
|
||||||
|
)
|
||||||
|
assert type(response) == dict
|
||||||
|
assert response["paid"] == True
|
||||||
|
assert "details" in response
|
||||||
|
|||||||
Reference in New Issue
Block a user