Compare commits
24
Commits
v1.3.0-rc4
...
v1.3.0-rc8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92d3269a85 | ||
|
|
183b84c167 | ||
|
|
60f50a71a2 | ||
|
|
c3252ce4dc | ||
|
|
c312d70e63 | ||
|
|
36fc911b88 | ||
|
|
672a5b3a4d | ||
|
|
c0b33560bb | ||
|
|
15b6b1d512 | ||
|
|
67c92a79cf | ||
|
|
0d4751d6e0 | ||
|
|
213bde61a8 | ||
|
|
59553a6e1e | ||
|
|
918add5e6e | ||
|
|
2375f7bcf8 | ||
|
|
5021570f68 | ||
|
|
0d91d5b5be | ||
|
|
49f02a5b77 | ||
|
|
95b9dabb60 | ||
|
|
bc0087ec36 | ||
|
|
8272147012 | ||
|
|
244de2ae86 | ||
|
|
ef371e303c | ||
|
|
5ba06d42d0 |
@@ -21,29 +21,16 @@ runs:
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
# cache poetry install via pip
|
||||
cache: "pip"
|
||||
|
||||
- name: Set up Poetry
|
||||
uses: abatilo/actions-poetry@v2
|
||||
|
||||
- name: Setup a local virtual environment (if no poetry.toml file)
|
||||
shell: bash
|
||||
run: |
|
||||
poetry config virtualenvs.create true --local
|
||||
poetry config virtualenvs.in-project true --local
|
||||
|
||||
- uses: actions/cache@v4
|
||||
name: Define a cache for the virtual environment based on the dependencies lock file
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
with:
|
||||
path: ./.venv
|
||||
key: venv-${{ hashFiles('poetry.lock') }}
|
||||
enable-cache: true
|
||||
python-version: ${{ inputs.python-version }}
|
||||
|
||||
- name: Install the project dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
poetry env use python${{ inputs.python-version }}
|
||||
poetry install --all-extras
|
||||
run: uv sync --locked --all-extras --dev
|
||||
|
||||
- name: Use Node.js ${{ inputs.node-version }}
|
||||
if: ${{ (inputs.npm == 'true') }}
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
db-url: ["", "postgres://lnbits:lnbits@0.0.0.0:5432/lnbits"]
|
||||
uses: ./.github/workflows/tests.yml
|
||||
with:
|
||||
custom-pytest: "poetry run pytest tests/api"
|
||||
custom-pytest: "uv run pytest tests/api"
|
||||
python-version: ${{ matrix.python-version }}
|
||||
db-url: ${{ matrix.db-url }}
|
||||
secrets:
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
db-url: ["", "postgres://lnbits:lnbits@0.0.0.0:5432/lnbits"]
|
||||
uses: ./.github/workflows/tests.yml
|
||||
with:
|
||||
custom-pytest: "poetry run pytest tests/wallets"
|
||||
custom-pytest: "uv run pytest tests/wallets"
|
||||
python-version: ${{ matrix.python-version }}
|
||||
db-url: ${{ matrix.db-url }}
|
||||
secrets:
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
db-url: ["", "postgres://lnbits:lnbits@0.0.0.0:5432/lnbits"]
|
||||
uses: ./.github/workflows/tests.yml
|
||||
with:
|
||||
custom-pytest: "poetry run pytest tests/unit"
|
||||
custom-pytest: "uv run pytest tests/unit"
|
||||
python-version: ${{ matrix.python-version }}
|
||||
db-url: ${{ matrix.db-url }}
|
||||
secrets:
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
python-version: ["3.10"]
|
||||
backend-wallet-class: ["LndRestWallet", "LndWallet", "CoreLightningWallet", "CoreLightningRestWallet", "LNbitsWallet", "EclairWallet"]
|
||||
with:
|
||||
custom-pytest: "poetry run pytest tests/regtest"
|
||||
custom-pytest: "uv run pytest tests/regtest"
|
||||
python-version: ${{ matrix.python-version }}
|
||||
backend-wallet-class: ${{ matrix.backend-wallet-class }}
|
||||
secrets:
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
LNBITS_EXTENSIONS_DEFAULT_INSTALL: "watchonly, satspay, tipjar, tpos, lnurlp, withdraw"
|
||||
LNBITS_BACKEND_WALLET_CLASS: FakeWallet
|
||||
run: |
|
||||
poetry run lnbits &
|
||||
uv run lnbits &
|
||||
sleep 10
|
||||
|
||||
- name: setup java version
|
||||
|
||||
@@ -53,3 +53,6 @@ jobs:
|
||||
with:
|
||||
make: checkbundle
|
||||
npm: true
|
||||
|
||||
poetry:
|
||||
uses: ./.github/workflows/poetry.yml
|
||||
|
||||
@@ -14,18 +14,17 @@ on:
|
||||
- 'flake.nix'
|
||||
- 'flake.lock'
|
||||
- 'pyproject.toml'
|
||||
- 'poetry.lock'
|
||||
- 'uv.lock'
|
||||
- '.github/workflows/nix.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'flake.nix'
|
||||
- 'flake.lock'
|
||||
- 'pyproject.toml'
|
||||
- 'poetry.lock'
|
||||
- 'uv.lock'
|
||||
|
||||
jobs:
|
||||
nix:
|
||||
if: false # temporarly disable nix support until the `poetry2nix` issue is resolved
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
name: poetry
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
python-version:
|
||||
description: "python version"
|
||||
type: string
|
||||
default: "3.10"
|
||||
|
||||
jobs:
|
||||
poetry:
|
||||
name: run poetry install to check lock file
|
||||
runs-on: "ubuntu-24.04"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: set up python ${{ inputs.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
- name: install poetry
|
||||
run: |
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
poetry install
|
||||
@@ -32,6 +32,10 @@ jobs:
|
||||
run: |
|
||||
docker build -t lnbits/lnbits .
|
||||
|
||||
- uses: ./.github/actions/prepare
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
|
||||
- name: Setup Regtest
|
||||
run: |
|
||||
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
|
||||
@@ -40,10 +44,6 @@ jobs:
|
||||
./tests
|
||||
sudo chmod -R a+rwx .
|
||||
|
||||
- uses: ./.github/actions/prepare
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
|
||||
- name: Run pytest
|
||||
uses: pavelzw/pytest-action@v2
|
||||
env:
|
||||
|
||||
@@ -61,3 +61,4 @@ dist
|
||||
|
||||
# jetbrains
|
||||
.idea
|
||||
.venv
|
||||
|
||||
@@ -2,7 +2,7 @@ exclude: '^lnbits/static/bundle.*|^docs/.*|^lnbits/static/vendor/.*|^lnbits/exte
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.3.0
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
@@ -14,16 +14,16 @@ repos:
|
||||
- id: mixed-line-ending
|
||||
- id: check-case-conflict
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 24.2.0
|
||||
rev: 25.1.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.3.2
|
||||
rev: v0.12.10
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [ --fix, --exit-non-zero-on-fix ]
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: "v4.0.0-alpha.8"
|
||||
- repo: https://github.com/rbubley/mirrors-prettier
|
||||
rev: v3.6.2
|
||||
hooks:
|
||||
- id: prettier
|
||||
types_or: [css, javascript, html, json]
|
||||
|
||||
+7
-20
@@ -2,26 +2,20 @@ FROM python:3.12-slim-bookworm AS builder
|
||||
|
||||
RUN apt-get clean
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y curl pkg-config build-essential libnss-myhostname
|
||||
RUN apt-get install -y curl pkg-config build-essential libnss-myhostname automake
|
||||
|
||||
RUN curl -sSL https://install.python-poetry.org | python3 -
|
||||
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
ENV PATH="/root/.local/bin:$PATH"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Only copy the files required to install the dependencies
|
||||
COPY pyproject.toml poetry.lock ./
|
||||
COPY pyproject.toml uv.lock ./
|
||||
RUN touch README.md
|
||||
|
||||
RUN mkdir data
|
||||
|
||||
ENV POETRY_NO_INTERACTION=1 \
|
||||
POETRY_VIRTUALENVS_IN_PROJECT=1 \
|
||||
POETRY_VIRTUALENVS_CREATE=1 \
|
||||
POETRY_CACHE_DIR=/tmp/poetry_cache
|
||||
|
||||
ARG POETRY_INSTALL_ARGS="--only main"
|
||||
RUN poetry install --no-root ${POETRY_INSTALL_ARGS}
|
||||
RUN uv sync --all-extras
|
||||
|
||||
FROM python:3.12-slim-bookworm
|
||||
|
||||
@@ -34,26 +28,19 @@ RUN apt-get update && apt-get -y upgrade && \
|
||||
apt-get -y install postgresql-client-14 postgresql-client-common && \
|
||||
apt-get clean all && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN curl -sSL https://install.python-poetry.org | python3 -
|
||||
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
ENV PATH="/root/.local/bin:$PATH"
|
||||
|
||||
ENV POETRY_NO_INTERACTION=1 \
|
||||
POETRY_VIRTUALENVS_IN_PROJECT=1 \
|
||||
POETRY_VIRTUALENVS_CREATE=1 \
|
||||
VIRTUAL_ENV=/app/.venv \
|
||||
PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
COPY --from=builder /app/.venv .venv
|
||||
|
||||
ARG POETRY_INSTALL_ARGS="--only main"
|
||||
RUN poetry install ${POETRY_INSTALL_ARGS}
|
||||
RUN uv sync --all-extras
|
||||
|
||||
ENV LNBITS_PORT="5000"
|
||||
ENV LNBITS_HOST="0.0.0.0"
|
||||
|
||||
EXPOSE 5000
|
||||
|
||||
CMD ["sh", "-c", "poetry run lnbits --port $LNBITS_PORT --host $LNBITS_HOST --forwarded-allow-ips='*'"]
|
||||
CMD ["sh", "-c", "uv run lnbits --port $LNBITS_PORT --host $LNBITS_HOST --forwarded-allow-ips='*'"]
|
||||
|
||||
+4
-2
@@ -7,9 +7,11 @@ RUN ls -l /usr/local/bin/boltzd
|
||||
RUN apt-get update && apt-get -y upgrade && \
|
||||
apt-get install -y netcat-openbsd
|
||||
|
||||
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
ENV PATH="/root/.local/bin:$PATH"
|
||||
|
||||
# Reinstall dependencies just in case (needed for CMD usage)
|
||||
ARG POETRY_INSTALL_ARGS="--only main"
|
||||
RUN poetry install ${POETRY_INSTALL_ARGS}
|
||||
RUN uv sync --all-extras
|
||||
|
||||
# LNbits + boltzd configuration
|
||||
ENV LNBITS_PORT="5000"
|
||||
|
||||
@@ -9,34 +9,34 @@ check: mypy pyright checkblack checkruff checkprettier checkbundle
|
||||
test: test-unit test-wallets test-api test-regtest
|
||||
|
||||
prettier:
|
||||
poetry run ./node_modules/.bin/prettier --write .
|
||||
uv run ./node_modules/.bin/prettier --write .
|
||||
|
||||
pyright:
|
||||
poetry run ./node_modules/.bin/pyright
|
||||
uv run ./node_modules/.bin/pyright
|
||||
|
||||
mypy:
|
||||
poetry run mypy
|
||||
uv run mypy
|
||||
|
||||
black:
|
||||
poetry run black .
|
||||
uv run black .
|
||||
|
||||
ruff:
|
||||
poetry run ruff check . --fix
|
||||
uv run ruff check . --fix
|
||||
|
||||
checkruff:
|
||||
poetry run ruff check .
|
||||
uv run ruff check .
|
||||
|
||||
checkprettier:
|
||||
poetry run ./node_modules/.bin/prettier --check .
|
||||
uv run ./node_modules/.bin/prettier --check .
|
||||
|
||||
checkblack:
|
||||
poetry run black --check .
|
||||
uv run black --check .
|
||||
|
||||
checkeditorconfig:
|
||||
editorconfig-checker
|
||||
|
||||
dev:
|
||||
poetry run lnbits --reload
|
||||
uv run lnbits --reload
|
||||
|
||||
docker:
|
||||
docker build -t lnbits/lnbits .
|
||||
@@ -46,27 +46,27 @@ test-wallets:
|
||||
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
DEBUG=true \
|
||||
poetry run pytest tests/wallets
|
||||
uv run pytest tests/wallets
|
||||
|
||||
test-unit:
|
||||
LNBITS_DATA_FOLDER="./tests/data" \
|
||||
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
DEBUG=true \
|
||||
poetry run pytest tests/unit
|
||||
uv run pytest tests/unit
|
||||
|
||||
test-api:
|
||||
LNBITS_DATA_FOLDER="./tests/data" \
|
||||
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
DEBUG=true \
|
||||
poetry run pytest tests/api
|
||||
uv run pytest tests/api
|
||||
|
||||
test-regtest:
|
||||
LNBITS_DATA_FOLDER="./tests/data" \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
DEBUG=true \
|
||||
poetry run pytest tests/regtest
|
||||
uv run pytest tests/regtest
|
||||
|
||||
test-migration:
|
||||
LNBITS_ADMIN_UI=True \
|
||||
@@ -74,18 +74,18 @@ test-migration:
|
||||
HOST=0.0.0.0 \
|
||||
PORT=5002 \
|
||||
LNBITS_DATA_FOLDER="./tests/data" \
|
||||
timeout 5s poetry run lnbits --host 0.0.0.0 --port 5002 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi
|
||||
timeout 5s uv run lnbits --host 0.0.0.0 --port 5002 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi
|
||||
HOST=0.0.0.0 \
|
||||
PORT=5002 \
|
||||
LNBITS_DATABASE_URL="postgres://lnbits:lnbits@localhost:5432/migration" \
|
||||
LNBITS_ADMIN_UI=False \
|
||||
timeout 5s poetry run lnbits --host 0.0.0.0 --port 5002 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi
|
||||
timeout 5s uv run lnbits --host 0.0.0.0 --port 5002 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi
|
||||
LNBITS_DATA_FOLDER="./tests/data" \
|
||||
LNBITS_DATABASE_URL="postgres://lnbits:lnbits@localhost:5432/migration" \
|
||||
poetry run python tools/conv.py
|
||||
uv run python tools/conv.py
|
||||
|
||||
migration:
|
||||
poetry run python tools/conv.py
|
||||
uv run python tools/conv.py
|
||||
|
||||
openapi:
|
||||
LNBITS_ADMIN_UI=False \
|
||||
@@ -94,9 +94,9 @@ openapi:
|
||||
PYTHONUNBUFFERED=1 \
|
||||
HOST=0.0.0.0 \
|
||||
PORT=5003 \
|
||||
poetry run lnbits &
|
||||
uv run lnbits &
|
||||
sleep 15
|
||||
curl -s http://0.0.0.0:5003/openapi.json | poetry run openapi-spec-validator --errors=all -
|
||||
curl -s http://0.0.0.0:5003/openapi.json | uv run openapi-spec-validator --errors=all -
|
||||
# kill -9 %1
|
||||
|
||||
bak:
|
||||
@@ -109,7 +109,7 @@ sass:
|
||||
bundle:
|
||||
npm install
|
||||
npm run bundle
|
||||
poetry run ./node_modules/.bin/prettier -w ./lnbits/static/vendor.json
|
||||
uv run ./node_modules/.bin/prettier -w ./lnbits/static/vendor.json
|
||||
|
||||
checkbundle:
|
||||
cp lnbits/static/bundle.min.js lnbits/static/bundle.min.js.old
|
||||
@@ -126,8 +126,8 @@ checkbundle:
|
||||
|
||||
install-pre-commit-hook:
|
||||
@echo "Installing pre-commit hook to git"
|
||||
@echo "Uninstall the hook with poetry run pre-commit uninstall"
|
||||
poetry run pre-commit install
|
||||
@echo "Uninstall the hook with uv run pre-commit uninstall"
|
||||
uv run pre-commit install
|
||||
|
||||
pre-commit:
|
||||
poetry run pre-commit run --all-files
|
||||
uv run pre-commit run --all-files
|
||||
|
||||
+1
-1
@@ -23,4 +23,4 @@ if ! nc -z localhost 9002; then
|
||||
fi
|
||||
|
||||
echo "Starting LNbits on $LNBITS_HOST:$LNBITS_PORT..."
|
||||
exec poetry run lnbits --port "$LNBITS_PORT" --host "$LNBITS_HOST" --forwarded-allow-ips='*'
|
||||
exec uv run lnbits --port "$LNBITS_PORT" --host "$LNBITS_HOST" --forwarded-allow-ips='*'
|
||||
|
||||
@@ -11,13 +11,13 @@ Thanks for contributing :)
|
||||
|
||||
# Run
|
||||
|
||||
Follow the [Basic installation: Option 1 (recommended): poetry](https://docs.lnbits.org/guide/installation.html#option-1-recommended-poetry)
|
||||
guide to install poetry and other dependencies.
|
||||
Follow the [Option 2 (recommended): UV](https://docs.lnbits.org/guide/installation.html)
|
||||
guide to install uv and other dependencies.
|
||||
|
||||
Then you can start LNbits uvicorn server with:
|
||||
|
||||
```bash
|
||||
poetry run lnbits
|
||||
uv run lnbits
|
||||
```
|
||||
|
||||
Or you can use the following to start uvicorn with hot reloading enabled:
|
||||
@@ -25,7 +25,7 @@ Or you can use the following to start uvicorn with hot reloading enabled:
|
||||
```bash
|
||||
make dev
|
||||
# or
|
||||
poetry run lnbits --reload
|
||||
uv run lnbits --reload
|
||||
```
|
||||
|
||||
You might need the following extra dependencies on clean installation of Debian:
|
||||
@@ -50,7 +50,7 @@ make install-pre-commit-hook
|
||||
This project has unit tests that help prevent regressions. Before you can run the tests, you must install a few dependencies:
|
||||
|
||||
```bash
|
||||
poetry install
|
||||
uv sync --all-extras --dev
|
||||
npm i
|
||||
```
|
||||
|
||||
@@ -69,7 +69,7 @@ make format
|
||||
Run mypy checks:
|
||||
|
||||
```bash
|
||||
poetry run mypy
|
||||
make mypy
|
||||
```
|
||||
|
||||
Run everything:
|
||||
|
||||
@@ -42,14 +42,10 @@ mv templates/example templates/mysuperplugin # Rename templates folder.
|
||||
|
||||
DO NOT ADD NEW DEPENDENCIES. Try to use the dependencies that are available in `pyproject.toml`. Getting the LNbits project to accept a new dependency is time consuming and uncertain, and may result in your extension NOT being made available to others.
|
||||
|
||||
If for some reason your extensions must have a new python package to work, and its nees are not met in `pyproject.toml`, you can add a new package using `poerty`:
|
||||
|
||||
```sh
|
||||
$ poetry add <package>
|
||||
```
|
||||
If for some reason your extensions must have a new python package to work, and its nees are not met in `pyproject.toml`, you can add a new package using `poerty` or `uv`:
|
||||
|
||||
**But we need an extra step to make sure LNbits doesn't break in production.**
|
||||
Dependencies need to be added to `pyproject.toml`, then tested by running on `poetry` compatibility can be tested with `nix build .#checks.x86_64-linux.vmTest`.
|
||||
Dependencies need to be added to `pyproject.toml`, then tested by running on `uv` and `poetry` compatibility can be tested with `nix build .#checks.x86_64-linux.vmTest`.
|
||||
|
||||
## SQLite to PostgreSQL migration
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ $ sudo nano .env
|
||||
Now start LNbits once in the terminal window
|
||||
|
||||
```
|
||||
$ poetry run lnbits
|
||||
$ uv run lnbits
|
||||
```
|
||||
|
||||
You can now `cat` the Super User ID:
|
||||
|
||||
+40
-22
@@ -23,15 +23,15 @@ LNBITS_ADMIN_UI=true HOST=0.0.0.0 PORT=5000 ./LNbits-latest.AppImage # most syst
|
||||
|
||||
LNbits will create a folder for db and extension files in the folder the AppImage runs from.
|
||||
|
||||
## Option 2: Poetry (recommended for developers)
|
||||
## Option 2: UV (recommended for developers)
|
||||
|
||||
It is recommended to use the latest version of Poetry. Make sure you have Python version `3.12` installed.
|
||||
It is recommended to use the latest version of UV. Make sure you have Python version `3.12` installed.
|
||||
|
||||
### Install Python 3.12
|
||||
|
||||
## Option 2 (recommended): Poetry
|
||||
## Option 2 (recommended): UV
|
||||
|
||||
It is recommended to use the latest version of Poetry. Make sure you have Python version 3.9 or higher installed.
|
||||
It is recommended to use the latest version of UV. Make sure you have Python version 3.10 or higher installed.
|
||||
|
||||
### Verify Python version
|
||||
|
||||
@@ -39,11 +39,19 @@ It is recommended to use the latest version of Poetry. Make sure you have Python
|
||||
python3 --version
|
||||
```
|
||||
|
||||
### Install Poetry
|
||||
### Install UV
|
||||
|
||||
```sh
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
```
|
||||
|
||||
### (old) Install Poetry
|
||||
|
||||
```sh
|
||||
# If path 'export PATH="$HOME/.local/bin:$PATH"' fails, use the path echoed by the install
|
||||
curl -sSL https://install.python-poetry.org | python3 - && export PATH="$HOME/.local/bin:$PATH"
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
```
|
||||
|
||||
### install LNbits
|
||||
@@ -51,9 +59,13 @@ curl -sSL https://install.python-poetry.org | python3 - && export PATH="$HOME/.l
|
||||
```sh
|
||||
git clone https://github.com/lnbits/lnbits.git
|
||||
cd lnbits
|
||||
poetry env use 3.12
|
||||
git checkout main
|
||||
poetry install --only main
|
||||
uv sync --all-extras
|
||||
|
||||
# or poetry
|
||||
# poetry env use 3.12
|
||||
# poetry install --only main
|
||||
|
||||
cp .env.example .env
|
||||
# Optional: to set funding source amongst other options via the env `nano .env`
|
||||
```
|
||||
@@ -61,8 +73,11 @@ cp .env.example .env
|
||||
#### Running the server
|
||||
|
||||
```sh
|
||||
poetry run lnbits
|
||||
# To change port/host pass 'poetry run lnbits --port 9000 --host 0.0.0.0'
|
||||
uv run lnbits
|
||||
# To change port/host pass 'uv run lnbits --port 9000 --host 0.0.0.0'
|
||||
|
||||
# or poetry
|
||||
# poetry run lnbits
|
||||
# adding --debug in the start-up command above to help your troubleshooting and generate a more verbose output
|
||||
# Note that you have to add the line DEBUG=true in your .env file, too.
|
||||
```
|
||||
@@ -71,7 +86,7 @@ poetry run lnbits
|
||||
|
||||
```sh
|
||||
# A very useful terminal client for getting the supersuer ID, updating extensions, etc
|
||||
poetry run lnbits-cli --help
|
||||
uv run lnbits-cli --help
|
||||
```
|
||||
|
||||
#### Updating the server
|
||||
@@ -85,15 +100,18 @@ cd lnbits
|
||||
git pull --rebase
|
||||
|
||||
# Check your poetry version with
|
||||
poetry env list
|
||||
# poetry env list
|
||||
# If version is less 3.12, update it by running
|
||||
poetry env use python3.12
|
||||
poetry env remove python3.9
|
||||
poetry env list
|
||||
# poetry env use python3.12
|
||||
# poetry env remove python3.9
|
||||
# poetry env list
|
||||
|
||||
# Run install and start LNbits with
|
||||
poetry install --only main
|
||||
poetry run lnbits
|
||||
# poetry install --only main
|
||||
# poetry run lnbits
|
||||
|
||||
uv sync --all-extras
|
||||
uv run lnbits
|
||||
|
||||
# use LNbits admin UI Extensions page function "Update All" do get extensions onto proper level
|
||||
```
|
||||
@@ -108,13 +126,13 @@ chmod +x lnbits.sh &&
|
||||
|
||||
Now visit `0.0.0.0:5000` to make a super-user account.
|
||||
|
||||
`./lnbits.sh` can be used to run, but for more control `cd lnbits` and use `poetry run lnbits` (see previous option).
|
||||
`./lnbits.sh` can be used to run, but for more control `cd lnbits` and use `uv run lnbits` (see previous option).
|
||||
|
||||
## Option 3: Nix
|
||||
|
||||
```sh
|
||||
# Install nix. If you have installed via another manager, remove and use this install (from https://nixos.org/download)
|
||||
sh <(curl -L https://nixos.org/nix/install) --daemon
|
||||
sh <(c&url -L https://nixos.org/nix/install) --daemon
|
||||
|
||||
# Enable nix-command and flakes experimental features for nix:
|
||||
echo 'experimental-features = nix-command flakes' >> /etc/nix/nix.conf
|
||||
@@ -332,7 +350,7 @@ LNBITS_DATABASE_URL="postgres://postgres:postgres@localhost/lnbits"
|
||||
|
||||
# START LNbits
|
||||
# STOP LNbits
|
||||
poetry run python tools/conv.py
|
||||
uv run python tools/conv.py
|
||||
# or
|
||||
make migration
|
||||
```
|
||||
@@ -357,8 +375,8 @@ Description=LNbits
|
||||
[Service]
|
||||
# replace with the absolute path of your lnbits installation
|
||||
WorkingDirectory=/home/lnbits/lnbits
|
||||
# same here. run `which poetry` if you can't find the poetry binary
|
||||
ExecStart=/home/lnbits/.local/bin/poetry run lnbits
|
||||
# same here. run `which uv` if you can't find the poetry binary
|
||||
ExecStart=/home/lnbits/.local/bin/uv run lnbits
|
||||
# replace with the user that you're running lnbits on
|
||||
User=lnbits
|
||||
Restart=always
|
||||
|
||||
@@ -72,7 +72,7 @@ You can also use an AES-encrypted macaroon (more info) instead by using
|
||||
|
||||
- `LND_GRPC_MACAROON_ENCRYPTED`: eNcRyPtEdMaCaRoOn
|
||||
|
||||
To encrypt your macaroon, run `poetry run lnbits-cli encrypt macaroon`.
|
||||
To encrypt your macaroon, run `uv run lnbits-cli encrypt macaroon`.
|
||||
|
||||
### LNbits
|
||||
|
||||
|
||||
Generated
+82
-67
@@ -1,15 +1,39 @@
|
||||
{
|
||||
"nodes": {
|
||||
"build-system-pkgs": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"pyproject-nix": [
|
||||
"pyproject-nix"
|
||||
],
|
||||
"uv2nix": "uv2nix"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1755484659,
|
||||
"narHash": "sha256-2FfbqsaHVQd12XFFUAinIMAuGO3853LONmva1gT3vKw=",
|
||||
"owner": "pyproject-nix",
|
||||
"repo": "build-system-pkgs",
|
||||
"rev": "9778e87c2361810ff15e287ca5895c9da4a0e900",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "pyproject-nix",
|
||||
"repo": "build-system-pkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1710146030,
|
||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -18,71 +42,49 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix-github-actions": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"poetry2nix",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1703863825,
|
||||
"narHash": "sha256-rXwqjtwiGKJheXB43ybM8NwWB8rO2dSRrEqes0S7F5Y=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nix-github-actions",
|
||||
"rev": "5163432afc817cf8bd1f031418d1869e4c9d5547",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "nix-github-actions",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1735563628,
|
||||
"narHash": "sha256-OnSAY7XDSx7CtDoqNh8jwVwh4xNL/2HaJxGjryLWzX8=",
|
||||
"owner": "nixos",
|
||||
"lastModified": 1751274312,
|
||||
"narHash": "sha256-/bVBlRpECLVzjV19t5KMdMFWSwKLtb5RyXdjz3LJT+g=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "b134951a4c9f3c995fd7be05f3243f8ecd65d798",
|
||||
"rev": "50ab793786d9de88ee30ec4e4c24fb4236fc2674",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-24.05",
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-24.11",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"poetry2nix": {
|
||||
"pyproject-nix": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nix-github-actions": "nix-github-actions",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"systems": "systems_2",
|
||||
"treefmt-nix": "treefmt-nix"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1724134185,
|
||||
"narHash": "sha256-nDqpGjz7cq3ThdC98BPe1ANCNlsJds/LLZ3/MdIXjA0=",
|
||||
"owner": "nix-community",
|
||||
"repo": "poetry2nix",
|
||||
"rev": "5ee730a8752264e463c0eaf06cc060fd07f6dae9",
|
||||
"lastModified": 1754923840,
|
||||
"narHash": "sha256-QSKpYg+Ts9HYF155ltlj40iBex39c05cpOF8gjoE2EM=",
|
||||
"owner": "pyproject-nix",
|
||||
"repo": "pyproject.nix",
|
||||
"rev": "023cd4be230eacae52635be09eef100c37ef78da",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "poetry2nix",
|
||||
"owner": "pyproject-nix",
|
||||
"repo": "pyproject.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"build-system-pkgs": "build-system-pkgs",
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"poetry2nix": "poetry2nix"
|
||||
"pyproject-nix": "pyproject-nix",
|
||||
"uv2nix": "uv2nix_2"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
@@ -100,38 +102,51 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "systems",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"treefmt-nix": {
|
||||
"uv2nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"poetry2nix",
|
||||
"build-system-pkgs",
|
||||
"nixpkgs"
|
||||
],
|
||||
"pyproject-nix": [
|
||||
"build-system-pkgs",
|
||||
"pyproject-nix"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1719749022,
|
||||
"narHash": "sha256-ddPKHcqaKCIFSFc/cvxS14goUhCOAwsM1PbMr0ZtHMg=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "8df5ff62195d4e67e2264df0b7f5e8c9995fd0bd",
|
||||
"lastModified": 1755210905,
|
||||
"narHash": "sha256-WnoFEk79ysjL85TNP7bvImzhxvQw9B6uNtnLd4oJntw=",
|
||||
"owner": "pyproject-nix",
|
||||
"repo": "uv2nix",
|
||||
"rev": "87bcba013ef304bbfd67c8e8a257aee634ed5a4c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"owner": "pyproject-nix",
|
||||
"repo": "uv2nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"uv2nix_2": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"pyproject-nix": [
|
||||
"pyproject-nix"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1755485731,
|
||||
"narHash": "sha256-k8kxwVs8Oze6q/jAaRa3RvZbb50I/K0b5uptlsh0HXI=",
|
||||
"owner": "pyproject-nix",
|
||||
"repo": "uv2nix",
|
||||
"rev": "bebbd80bf56110fcd20b425589814af28f1939eb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "pyproject-nix",
|
||||
"repo": "uv2nix",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,70 +1,149 @@
|
||||
{
|
||||
description = "LNbits, free and open-source Lightning wallet and accounts system";
|
||||
description = "LNbits, free and open-source Lightning wallet and accounts system (uv2nix)";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-24.05";
|
||||
poetry2nix = {
|
||||
url = "github:nix-community/poetry2nix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
|
||||
pyproject-nix.url = "github:pyproject-nix/pyproject.nix";
|
||||
uv2nix.url = "github:pyproject-nix/uv2nix";
|
||||
build-system-pkgs.url = "github:pyproject-nix/build-system-pkgs";
|
||||
|
||||
pyproject-nix.inputs.nixpkgs.follows = "nixpkgs";
|
||||
uv2nix.inputs.nixpkgs.follows = "nixpkgs";
|
||||
build-system-pkgs.inputs.nixpkgs.follows = "nixpkgs";
|
||||
uv2nix.inputs.pyproject-nix.follows = "pyproject-nix";
|
||||
build-system-pkgs.inputs.pyproject-nix.follows = "pyproject-nix";
|
||||
};
|
||||
outputs = { self, nixpkgs, poetry2nix }@inputs:
|
||||
let
|
||||
supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
|
||||
forSystems = systems: f:
|
||||
nixpkgs.lib.genAttrs systems
|
||||
(system: f system (import nixpkgs { inherit system; overlays = [ poetry2nix.overlays.default self.overlays.default ]; }));
|
||||
forAllSystems = forSystems supportedSystems;
|
||||
projectName = "lnbits";
|
||||
in
|
||||
{
|
||||
overlays = {
|
||||
default = final: prev: {
|
||||
${projectName} = self.packages.${prev.stdenv.hostPlatform.system}.${projectName};
|
||||
};
|
||||
};
|
||||
packages = forAllSystems (system: pkgs: {
|
||||
default = self.packages.${system}.${projectName};
|
||||
${projectName} = pkgs.poetry2nix.mkPoetryApplication {
|
||||
projectDir = ./.;
|
||||
meta.rev = self.dirtyRev or self.rev;
|
||||
meta.mainProgram = projectName;
|
||||
overrides = pkgs.poetry2nix.overrides.withDefaults (final: prev: {
|
||||
coincurve = prev.coincurve.override { preferWheel = true; };
|
||||
protobuf = prev.protobuf.override { preferWheel = true; };
|
||||
ruff = prev.ruff.override { preferWheel = true; };
|
||||
wallycore = prev.wallycore.override { preferWheel = true; };
|
||||
tlv8 = prev.tlv8.overrideAttrs (old: {
|
||||
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [
|
||||
prev.setuptools
|
||||
];
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils, uv2nix, pyproject-nix, build-system-pkgs, ... }:
|
||||
flake-utils.lib.eachSystem [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]
|
||||
(system:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
lib = pkgs.lib;
|
||||
|
||||
python = pkgs.python312;
|
||||
|
||||
# Read uv.lock / pyproject via uv2nix
|
||||
workspace = uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./.; };
|
||||
|
||||
# Prefer wheels when available
|
||||
uvLockedOverlay = workspace.mkPyprojectOverlay { sourcePreference = "wheel"; };
|
||||
|
||||
# Helper for extending lists safely (works if a is null)
|
||||
plus = a: b: lib.unique (((if a == null then [] else a)) ++ b);
|
||||
|
||||
# Extra build inputs for troublesome sdists
|
||||
myOverrides = (final: prev: {
|
||||
# embit needs setuptools at build time
|
||||
embit = prev.embit.overrideAttrs (old: {
|
||||
nativeBuildInputs = plus (old.nativeBuildInputs or []) [ prev.setuptools ];
|
||||
});
|
||||
pynostr = prev.pynostr.overrideAttrs (old: {
|
||||
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [
|
||||
prev.setuptools-scm
|
||||
|
||||
# http-ece (pywebpush dep) needs setuptools
|
||||
"http-ece" = prev."http-ece".overrideAttrs (old: {
|
||||
nativeBuildInputs = plus (old.nativeBuildInputs or []) [ prev.setuptools ];
|
||||
});
|
||||
|
||||
# pyqrcode needs setuptools
|
||||
pyqrcode = prev.pyqrcode.overrideAttrs (old: {
|
||||
nativeBuildInputs = plus (old.nativeBuildInputs or []) [ prev.setuptools ];
|
||||
});
|
||||
|
||||
# tlv8 needs setuptools
|
||||
tlv8 = prev.tlv8.overrideAttrs (old: {
|
||||
nativeBuildInputs = plus (old.nativeBuildInputs or []) [ prev.setuptools ];
|
||||
});
|
||||
|
||||
# secp256k1 Python binding:
|
||||
# - setuptools, pkg-config
|
||||
# - cffi + pycparser
|
||||
# - system libsecp256k1 for headers/libs
|
||||
secp256k1 = prev.secp256k1.overrideAttrs (old: {
|
||||
nativeBuildInputs = plus (old.nativeBuildInputs or []) [
|
||||
prev.setuptools
|
||||
pkgs.pkg-config
|
||||
prev.cffi
|
||||
prev.pycparser
|
||||
];
|
||||
buildInputs = plus (old.buildInputs or []) [ pkgs.secp256k1 ];
|
||||
propagatedBuildInputs = plus (old.propagatedBuildInputs or []) [ prev.cffi prev.pycparser ];
|
||||
env = (old.env or { }) // { PKG_CONFIG = "${pkgs.pkg-config}/bin/pkg-config"; };
|
||||
});
|
||||
|
||||
# pynostr uses setuptools-scm for versioning
|
||||
pynostr = prev.pynostr.overrideAttrs (old: {
|
||||
nativeBuildInputs = plus (old.nativeBuildInputs or []) [ prev.setuptools-scm ];
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
||||
nixosModules = {
|
||||
default = { pkgs, lib, config, ... }: {
|
||||
imports = [ "${./nix/modules/${projectName}-service.nix}" ];
|
||||
nixpkgs.overlays = [ self.overlays.default ];
|
||||
};
|
||||
};
|
||||
checks = forAllSystems (system: pkgs:
|
||||
let
|
||||
vmTests = import ./nix/tests {
|
||||
makeTest = (import (nixpkgs + "/nixos/lib/testing-python.nix") { inherit system; }).makeTest;
|
||||
inherit inputs pkgs;
|
||||
|
||||
# Compose Python package set honoring uv.lock
|
||||
pythonSet =
|
||||
(pkgs.callPackage pyproject-nix.build.packages { inherit python; })
|
||||
.overrideScope (lib.composeManyExtensions [
|
||||
build-system-pkgs.overlays.default
|
||||
uvLockedOverlay
|
||||
myOverrides
|
||||
]);
|
||||
|
||||
projectName = "lnbits";
|
||||
|
||||
# Build a venv from the locked spec (this installs the resolved wheels)
|
||||
runtimeVenv = pythonSet.mkVirtualEnv "${projectName}-env" workspace.deps.default;
|
||||
|
||||
# Wrapper so `nix run` behaves like `uv run` (use local source tree for templates/static/extensions)
|
||||
lnbitsApp = pkgs.writeShellApplication {
|
||||
name = "lnbits";
|
||||
text = ''
|
||||
export SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt
|
||||
export REQUESTS_CA_BUNDLE=$SSL_CERT_FILE
|
||||
export PYTHONPATH="$PWD:${PYTHONPATH:-}"
|
||||
exec ${runtimeVenv}/bin/lnbits "$@"
|
||||
'';
|
||||
};
|
||||
|
||||
lnbitsCliApp = pkgs.writeShellApplication {
|
||||
name = "lnbits-cli";
|
||||
text = ''
|
||||
export PYTHONPATH="$PWD:${PYTHONPATH:-}"
|
||||
exec ${runtimeVenv}/bin/lnbits-cli "$@"
|
||||
'';
|
||||
};
|
||||
in
|
||||
pkgs.lib.optionalAttrs pkgs.stdenv.isLinux vmTests # vmTests can only be ran on Linux, so append them only if on Linux.
|
||||
//
|
||||
{
|
||||
# Other checks here...
|
||||
}
|
||||
);
|
||||
};
|
||||
# nix build → produces the venv in ./result
|
||||
packages.default = runtimeVenv;
|
||||
packages.${projectName} = runtimeVenv;
|
||||
|
||||
# nix run . → launches via wrapper that imports from source tree
|
||||
apps.default = { type = "app"; program = "${lnbitsApp}/bin/lnbits"; };
|
||||
apps.${projectName} = self.apps.${system}.default;
|
||||
apps."${projectName}-cli" = { type = "app"; program = "${lnbitsCliApp}/bin/lnbits-cli"; };
|
||||
|
||||
# dev shell with locked deps + tools
|
||||
devShells.default = pkgs.mkShell {
|
||||
packages = [
|
||||
runtimeVenv
|
||||
pkgs.uv
|
||||
pkgs.ruff
|
||||
pkgs.black
|
||||
pkgs.mypy
|
||||
pkgs.pre-commit
|
||||
pkgs.openapi-generator-cli
|
||||
];
|
||||
};
|
||||
|
||||
overlays.default = final: prev: {
|
||||
${projectName} = self.packages.${final.stdenv.hostPlatform.system}.${projectName};
|
||||
replaceVars = prev.replaceVars or (path: vars: prev.substituteAll ({ src = path; } // vars));
|
||||
};
|
||||
|
||||
nixosModules.default = { pkgs, lib, config, ... }: {
|
||||
imports = [ "${./nix/modules/lnbits-service.nix}" ];
|
||||
nixpkgs.overlays = [ self.overlays.default ];
|
||||
};
|
||||
|
||||
checks = { };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,10 +13,9 @@ if [ ! -d lnbits/data ]; then
|
||||
# Install Python 3.10 and distutils non-interactively
|
||||
sudo apt install -y python3.10 python3.10-distutils
|
||||
|
||||
# Install Poetry
|
||||
curl -sSL https://install.python-poetry.org | python3.10 -
|
||||
# Install UV
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
|
||||
# Add Poetry to PATH for the current session
|
||||
export PATH="/home/$USER/.local/bin:$PATH"
|
||||
|
||||
if [ ! -d lnbits/wallets ]; then
|
||||
@@ -42,13 +41,13 @@ elif [ ! -d lnbits/wallets ]; then
|
||||
cd lnbits || { echo "Failed to cd into lnbits ... FAIL"; exit 1; }
|
||||
fi
|
||||
|
||||
# Install the dependencies using Poetry
|
||||
poetry env use python3.9
|
||||
poetry install --only main
|
||||
# Install the dependencies using UV
|
||||
uv sync --all-extras
|
||||
|
||||
|
||||
# Set environment variables for LNbits
|
||||
export LNBITS_ADMIN_UI=true
|
||||
export HOST=0.0.0.0
|
||||
|
||||
# Run LNbits
|
||||
poetry run lnbits
|
||||
uv run lnbits
|
||||
|
||||
+2
-2
@@ -5,9 +5,9 @@ import os
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
from typing import Callable, Optional
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
@@ -280,7 +280,7 @@ async def check_installed_extensions(app: FastAPI):
|
||||
|
||||
|
||||
async def build_all_installed_extensions_list( # noqa: C901
|
||||
include_deactivated: Optional[bool] = True,
|
||||
include_deactivated: bool | None = True,
|
||||
) -> list[InstallableExtension]:
|
||||
"""
|
||||
Returns a list of all the installed extensions plus the extensions that
|
||||
|
||||
+32
-33
@@ -5,7 +5,6 @@ import time
|
||||
from functools import wraps
|
||||
from getpass import getpass
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from uuid import uuid4
|
||||
|
||||
import click
|
||||
@@ -96,7 +95,7 @@ def decrypt():
|
||||
"""
|
||||
|
||||
|
||||
def get_super_user() -> Optional[str]:
|
||||
def get_super_user() -> str | None:
|
||||
"""Get the superuser"""
|
||||
superuser_file = Path(settings.lnbits_data_folder, ".super_user")
|
||||
if not superuser_file.exists() or not superuser_file.is_file():
|
||||
@@ -155,7 +154,7 @@ async def db_versions():
|
||||
@db.command("cleanup-wallets")
|
||||
@click.argument("days", type=int, required=False)
|
||||
@coro
|
||||
async def database_cleanup_wallets(days: Optional[int] = None):
|
||||
async def database_cleanup_wallets(days: int | None = None):
|
||||
"""Delete all wallets that never had any transaction"""
|
||||
async with core_db.connect() as conn:
|
||||
delta = days or settings.cleanup_wallets_days
|
||||
@@ -212,10 +211,10 @@ async def database_revert_payment(checking_id: str):
|
||||
@click.option("-v", "--verbose", is_flag=True, help="Detailed log.")
|
||||
@coro
|
||||
async def check_invalid_payments(
|
||||
days: Optional[int] = None,
|
||||
limit: Optional[int] = None,
|
||||
wallet: Optional[str] = None,
|
||||
verbose: Optional[bool] = False,
|
||||
days: int | None = None,
|
||||
limit: int | None = None,
|
||||
wallet: str | None = None,
|
||||
verbose: bool | None = False,
|
||||
):
|
||||
"""Check payments that are settled in the DB but pending on the Funding Source"""
|
||||
await check_admin_settings()
|
||||
@@ -303,7 +302,7 @@ async def create_user(username: str, password: str):
|
||||
@users.command("cleanup-accounts")
|
||||
@click.argument("days", type=int, required=False)
|
||||
@coro
|
||||
async def database_cleanup_accounts(days: Optional[int] = None):
|
||||
async def database_cleanup_accounts(days: int | None = None):
|
||||
"""Delete all accounts that have no wallets"""
|
||||
async with core_db.connect() as conn:
|
||||
delta = days or settings.cleanup_wallets_days
|
||||
@@ -353,12 +352,12 @@ async def extensions_list():
|
||||
)
|
||||
@coro
|
||||
async def extensions_update( # noqa: C901
|
||||
extension: Optional[str] = None,
|
||||
all_extensions: Optional[bool] = False,
|
||||
repo_index: Optional[str] = None,
|
||||
source_repo: Optional[str] = None,
|
||||
url: Optional[str] = None,
|
||||
admin_user: Optional[str] = None,
|
||||
extension: str | None = None,
|
||||
all_extensions: bool | None = False,
|
||||
repo_index: str | None = None,
|
||||
source_repo: str | None = None,
|
||||
url: str | None = None,
|
||||
admin_user: str | None = None,
|
||||
):
|
||||
"""
|
||||
Update extension to the latest version.
|
||||
@@ -443,10 +442,10 @@ async def extensions_update( # noqa: C901
|
||||
@coro
|
||||
async def extensions_install(
|
||||
extension: str,
|
||||
repo_index: Optional[str] = None,
|
||||
source_repo: Optional[str] = None,
|
||||
url: Optional[str] = None,
|
||||
admin_user: Optional[str] = None,
|
||||
repo_index: str | None = None,
|
||||
source_repo: str | None = None,
|
||||
url: str | None = None,
|
||||
admin_user: str | None = None,
|
||||
):
|
||||
"""Install a extension"""
|
||||
click.echo(f"Installing {extension}... {repo_index}")
|
||||
@@ -473,7 +472,7 @@ async def extensions_install(
|
||||
)
|
||||
@coro
|
||||
async def extensions_uninstall(
|
||||
extension: str, url: Optional[str] = None, admin_user: Optional[str] = None
|
||||
extension: str, url: str | None = None, admin_user: str | None = None
|
||||
):
|
||||
"""Uninstall a extension"""
|
||||
click.echo(f"Uninstalling '{extension}'...")
|
||||
@@ -562,10 +561,10 @@ if __name__ == "__main__":
|
||||
|
||||
async def install_extension(
|
||||
extension: str,
|
||||
repo_index: Optional[str] = None,
|
||||
source_repo: Optional[str] = None,
|
||||
url: Optional[str] = None,
|
||||
admin_user: Optional[str] = None,
|
||||
repo_index: str | None = None,
|
||||
source_repo: str | None = None,
|
||||
url: str | None = None,
|
||||
admin_user: str | None = None,
|
||||
) -> tuple[bool, str]:
|
||||
try:
|
||||
release = await _select_release(extension, repo_index, source_repo)
|
||||
@@ -591,10 +590,10 @@ async def install_extension(
|
||||
|
||||
async def update_extension(
|
||||
extension: str,
|
||||
repo_index: Optional[str] = None,
|
||||
source_repo: Optional[str] = None,
|
||||
url: Optional[str] = None,
|
||||
admin_user: Optional[str] = None,
|
||||
repo_index: str | None = None,
|
||||
source_repo: str | None = None,
|
||||
url: str | None = None,
|
||||
admin_user: str | None = None,
|
||||
) -> tuple[bool, str]:
|
||||
try:
|
||||
click.echo(f"Updating '{extension}' extension.")
|
||||
@@ -644,9 +643,9 @@ async def update_extension(
|
||||
|
||||
async def _select_release(
|
||||
extension: str,
|
||||
repo_index: Optional[str] = None,
|
||||
source_repo: Optional[str] = None,
|
||||
) -> Optional[ExtensionRelease]:
|
||||
repo_index: str | None = None,
|
||||
source_repo: str | None = None,
|
||||
) -> ExtensionRelease | None:
|
||||
all_releases = await InstallableExtension.get_extension_releases(extension)
|
||||
if len(all_releases) == 0:
|
||||
click.echo(f"No repository found for extension '{extension}'.")
|
||||
@@ -706,7 +705,7 @@ def _get_latest_release_per_repo(all_releases):
|
||||
|
||||
|
||||
async def _call_install_extension(
|
||||
data: CreateExtension, url: Optional[str], user_id: Optional[str] = None
|
||||
data: CreateExtension, url: str | None, user_id: str | None = None
|
||||
):
|
||||
if url:
|
||||
user_id = user_id or get_super_user()
|
||||
@@ -720,7 +719,7 @@ async def _call_install_extension(
|
||||
|
||||
|
||||
async def _call_uninstall_extension(
|
||||
extension: str, url: Optional[str], user_id: Optional[str] = None
|
||||
extension: str, url: str | None, user_id: str | None = None
|
||||
):
|
||||
if url:
|
||||
user_id = user_id or get_super_user()
|
||||
@@ -756,7 +755,7 @@ async def _can_run_operation(url) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def _is_lnbits_started(url: Optional[str]):
|
||||
async def _is_lnbits_started(url: str | None):
|
||||
try:
|
||||
url = url or f"http://{settings.host}:{settings.port}/api/v1/health"
|
||||
async with httpx.AsyncClient() as client:
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from typing import Optional
|
||||
|
||||
from lnbits.core.db import db
|
||||
from lnbits.core.models import AuditEntry, AuditFilters
|
||||
from lnbits.core.models.audit import AuditCountStat
|
||||
@@ -8,14 +6,14 @@ from lnbits.db import Connection, Filters, Page
|
||||
|
||||
async def create_audit_entry(
|
||||
entry: AuditEntry,
|
||||
conn: Optional[Connection] = None,
|
||||
conn: Connection | None = None,
|
||||
) -> None:
|
||||
await (conn or db).insert("audit", entry)
|
||||
|
||||
|
||||
async def get_audit_entries(
|
||||
filters: Optional[Filters[AuditFilters]] = None,
|
||||
conn: Optional[Connection] = None,
|
||||
filters: Filters[AuditFilters] | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> Page[AuditEntry]:
|
||||
return await (conn or db).fetch_page(
|
||||
"SELECT * from audit",
|
||||
@@ -27,7 +25,7 @@ async def get_audit_entries(
|
||||
|
||||
|
||||
async def delete_expired_audit_entries(
|
||||
conn: Optional[Connection] = None,
|
||||
conn: Connection | None = None,
|
||||
):
|
||||
await (conn or db).execute(
|
||||
# Timestamp placeholder is safe from SQL injection (not user input)
|
||||
@@ -40,8 +38,8 @@ async def delete_expired_audit_entries(
|
||||
|
||||
async def get_count_stats(
|
||||
field: str,
|
||||
filters: Optional[Filters[AuditFilters]] = None,
|
||||
conn: Optional[Connection] = None,
|
||||
filters: Filters[AuditFilters] | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> list[AuditCountStat]:
|
||||
if field not in ["request_method", "component", "response_code"]:
|
||||
return []
|
||||
@@ -67,8 +65,8 @@ async def get_count_stats(
|
||||
|
||||
|
||||
async def get_long_duration_stats(
|
||||
filters: Optional[Filters[AuditFilters]] = None,
|
||||
conn: Optional[Connection] = None,
|
||||
filters: Filters[AuditFilters] | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> list[AuditCountStat]:
|
||||
if not filters:
|
||||
filters = Filters()
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from typing import Optional
|
||||
|
||||
from lnbits.core.db import db
|
||||
from lnbits.db import Connection
|
||||
|
||||
@@ -7,8 +5,8 @@ from ..models import DbVersion
|
||||
|
||||
|
||||
async def get_db_version(
|
||||
ext_id: str, conn: Optional[Connection] = None
|
||||
) -> Optional[DbVersion]:
|
||||
ext_id: str, conn: Connection | None = None
|
||||
) -> DbVersion | None:
|
||||
return await (conn or db).fetchone(
|
||||
"SELECT * FROM dbversions WHERE db = :ext_id",
|
||||
{"ext_id": ext_id},
|
||||
@@ -16,7 +14,7 @@ async def get_db_version(
|
||||
)
|
||||
|
||||
|
||||
async def get_db_versions(conn: Optional[Connection] = None) -> list[DbVersion]:
|
||||
async def get_db_versions(conn: Connection | None = None) -> list[DbVersion]:
|
||||
return await (conn or db).fetchall("SELECT * FROM dbversions", model=DbVersion)
|
||||
|
||||
|
||||
@@ -30,7 +28,7 @@ async def update_migration_version(conn, db_name, version):
|
||||
)
|
||||
|
||||
|
||||
async def delete_dbversion(*, ext_id: str, conn: Optional[Connection] = None) -> None:
|
||||
async def delete_dbversion(*, ext_id: str, conn: Connection | None = None) -> None:
|
||||
await (conn or db).execute(
|
||||
"""
|
||||
DELETE FROM dbversions WHERE db = :ext
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from typing import Optional
|
||||
|
||||
from lnbits.core.db import db
|
||||
from lnbits.core.models.extensions import (
|
||||
InstallableExtension,
|
||||
@@ -10,20 +8,20 @@ from lnbits.db import Connection, Database
|
||||
|
||||
async def create_installed_extension(
|
||||
ext: InstallableExtension,
|
||||
conn: Optional[Connection] = None,
|
||||
conn: Connection | None = None,
|
||||
) -> None:
|
||||
await (conn or db).insert("installed_extensions", ext)
|
||||
|
||||
|
||||
async def update_installed_extension(
|
||||
ext: InstallableExtension,
|
||||
conn: Optional[Connection] = None,
|
||||
conn: Connection | None = None,
|
||||
) -> None:
|
||||
await (conn or db).update("installed_extensions", ext)
|
||||
|
||||
|
||||
async def update_installed_extension_state(
|
||||
*, ext_id: str, active: bool, conn: Optional[Connection] = None
|
||||
*, ext_id: str, active: bool, conn: Connection | None = None
|
||||
) -> None:
|
||||
await (conn or db).execute(
|
||||
"""
|
||||
@@ -34,7 +32,7 @@ async def update_installed_extension_state(
|
||||
|
||||
|
||||
async def delete_installed_extension(
|
||||
*, ext_id: str, conn: Optional[Connection] = None
|
||||
*, ext_id: str, conn: Connection | None = None
|
||||
) -> None:
|
||||
await (conn or db).execute(
|
||||
"""
|
||||
@@ -44,7 +42,7 @@ async def delete_installed_extension(
|
||||
)
|
||||
|
||||
|
||||
async def drop_extension_db(ext_id: str, conn: Optional[Connection] = None) -> None:
|
||||
async def drop_extension_db(ext_id: str, conn: Connection | None = None) -> None:
|
||||
row: dict = await (conn or db).fetchone(
|
||||
"SELECT * FROM dbversions WHERE db = :id",
|
||||
{"id": ext_id},
|
||||
@@ -65,8 +63,8 @@ async def drop_extension_db(ext_id: str, conn: Optional[Connection] = None) -> N
|
||||
|
||||
|
||||
async def get_installed_extension(
|
||||
ext_id: str, conn: Optional[Connection] = None
|
||||
) -> Optional[InstallableExtension]:
|
||||
ext_id: str, conn: Connection | None = None
|
||||
) -> InstallableExtension | None:
|
||||
extension = await (conn or db).fetchone(
|
||||
"SELECT * FROM installed_extensions WHERE id = :id",
|
||||
{"id": ext_id},
|
||||
@@ -76,8 +74,8 @@ async def get_installed_extension(
|
||||
|
||||
|
||||
async def get_installed_extensions(
|
||||
active: Optional[bool] = None,
|
||||
conn: Optional[Connection] = None,
|
||||
active: bool | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> list[InstallableExtension]:
|
||||
query = "SELECT * FROM installed_extensions"
|
||||
if active is not None:
|
||||
@@ -93,8 +91,8 @@ async def get_installed_extensions(
|
||||
|
||||
|
||||
async def get_user_extension(
|
||||
user_id: str, extension: str, conn: Optional[Connection] = None
|
||||
) -> Optional[UserExtension]:
|
||||
user_id: str, extension: str, conn: Connection | None = None
|
||||
) -> UserExtension | None:
|
||||
return await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT * FROM extensions
|
||||
@@ -106,7 +104,7 @@ async def get_user_extension(
|
||||
|
||||
|
||||
async def get_user_extensions(
|
||||
user_id: str, conn: Optional[Connection] = None
|
||||
user_id: str, conn: Connection | None = None
|
||||
) -> list[UserExtension]:
|
||||
return await (conn or db).fetchall(
|
||||
"""SELECT * FROM extensions WHERE "user" = :user""",
|
||||
@@ -116,20 +114,20 @@ async def get_user_extensions(
|
||||
|
||||
|
||||
async def create_user_extension(
|
||||
user_extension: UserExtension, conn: Optional[Connection] = None
|
||||
user_extension: UserExtension, conn: Connection | None = None
|
||||
) -> None:
|
||||
await (conn or db).insert("extensions", user_extension)
|
||||
|
||||
|
||||
async def update_user_extension(
|
||||
user_extension: UserExtension, conn: Optional[Connection] = None
|
||||
user_extension: UserExtension, conn: Connection | None = None
|
||||
) -> None:
|
||||
where = """WHERE extension = :extension AND "user" = :user"""
|
||||
await (conn or db).update("extensions", user_extension, where)
|
||||
|
||||
|
||||
async def get_user_active_extensions_ids(
|
||||
user_id: str, conn: Optional[Connection] = None
|
||||
user_id: str, conn: Connection | None = None
|
||||
) -> list[str]:
|
||||
exts = await (conn or db).fetchall(
|
||||
"""
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from time import time
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
|
||||
from lnbits.core.crud.wallets import get_total_balance, get_wallet, get_wallets_ids
|
||||
from lnbits.core.db import db
|
||||
@@ -23,7 +23,7 @@ def update_payment_extra():
|
||||
pass
|
||||
|
||||
|
||||
async def get_payment(checking_id: str, conn: Optional[Connection] = None) -> Payment:
|
||||
async def get_payment(checking_id: str, conn: Connection | None = None) -> Payment:
|
||||
return await (conn or db).fetchone(
|
||||
"SELECT * FROM apipayments WHERE checking_id = :checking_id",
|
||||
{"checking_id": checking_id},
|
||||
@@ -33,10 +33,10 @@ async def get_payment(checking_id: str, conn: Optional[Connection] = None) -> Pa
|
||||
|
||||
async def get_standalone_payment(
|
||||
checking_id_or_hash: str,
|
||||
conn: Optional[Connection] = None,
|
||||
incoming: Optional[bool] = False,
|
||||
wallet_id: Optional[str] = None,
|
||||
) -> Optional[Payment]:
|
||||
conn: Connection | None = None,
|
||||
incoming: bool | None = False,
|
||||
wallet_id: str | None = None,
|
||||
) -> Payment | None:
|
||||
clause: str = "checking_id = :checking_id OR payment_hash = :hash"
|
||||
values = {
|
||||
"wallet_id": wallet_id,
|
||||
@@ -64,8 +64,8 @@ async def get_standalone_payment(
|
||||
|
||||
|
||||
async def get_wallet_payment(
|
||||
wallet_id: str, payment_hash: str, conn: Optional[Connection] = None
|
||||
) -> Optional[Payment]:
|
||||
wallet_id: str, payment_hash: str, conn: Connection | None = None
|
||||
) -> Payment | None:
|
||||
payment = await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT *
|
||||
@@ -102,17 +102,17 @@ async def get_latest_payments_by_extension(
|
||||
|
||||
async def get_payments_paginated( # noqa: C901
|
||||
*,
|
||||
wallet_id: Optional[str] = None,
|
||||
user_id: Optional[str] = None,
|
||||
wallet_id: str | None = None,
|
||||
user_id: str | None = None,
|
||||
complete: bool = False,
|
||||
pending: bool = False,
|
||||
failed: bool = False,
|
||||
outgoing: bool = False,
|
||||
incoming: bool = False,
|
||||
since: Optional[int] = None,
|
||||
since: int | None = None,
|
||||
exclude_uncheckable: bool = False,
|
||||
filters: Optional[Filters[PaymentFilters]] = None,
|
||||
conn: Optional[Connection] = None,
|
||||
filters: Filters[PaymentFilters] | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> Page[Payment]:
|
||||
"""
|
||||
Filters payments to be returned by:
|
||||
@@ -176,17 +176,17 @@ async def get_payments_paginated( # noqa: C901
|
||||
|
||||
async def get_payments(
|
||||
*,
|
||||
wallet_id: Optional[str] = None,
|
||||
wallet_id: str | None = None,
|
||||
complete: bool = False,
|
||||
pending: bool = False,
|
||||
outgoing: bool = False,
|
||||
incoming: bool = False,
|
||||
since: Optional[int] = None,
|
||||
since: int | None = None,
|
||||
exclude_uncheckable: bool = False,
|
||||
filters: Optional[Filters[PaymentFilters]] = None,
|
||||
conn: Optional[Connection] = None,
|
||||
limit: Optional[int] = None,
|
||||
offset: Optional[int] = None,
|
||||
filters: Filters[PaymentFilters] | None = None,
|
||||
conn: Connection | None = None,
|
||||
limit: int | None = None,
|
||||
offset: int | None = None,
|
||||
) -> list[Payment]:
|
||||
"""
|
||||
Filters payments to be returned by complete | pending | outgoing | incoming.
|
||||
@@ -230,7 +230,7 @@ async def get_payments_status_count() -> PaymentsStatusCount:
|
||||
|
||||
|
||||
async def delete_expired_invoices(
|
||||
conn: Optional[Connection] = None,
|
||||
conn: Connection | None = None,
|
||||
) -> None:
|
||||
# first we delete all invoices older than one month
|
||||
|
||||
@@ -259,7 +259,7 @@ async def create_payment(
|
||||
checking_id: str,
|
||||
data: CreatePayment,
|
||||
status: PaymentState = PaymentState.PENDING,
|
||||
conn: Optional[Connection] = None,
|
||||
conn: Connection | None = None,
|
||||
) -> Payment:
|
||||
# we don't allow the creation of the same invoice twice
|
||||
# note: this can be removed if the db uniqueness constraints are set appropriately
|
||||
@@ -290,7 +290,7 @@ async def create_payment(
|
||||
|
||||
|
||||
async def update_payment_checking_id(
|
||||
checking_id: str, new_checking_id: str, conn: Optional[Connection] = None
|
||||
checking_id: str, new_checking_id: str, conn: Connection | None = None
|
||||
) -> None:
|
||||
await (conn or db).execute(
|
||||
"UPDATE apipayments SET checking_id = :new_id WHERE checking_id = :old_id",
|
||||
@@ -300,8 +300,8 @@ async def update_payment_checking_id(
|
||||
|
||||
async def update_payment(
|
||||
payment: Payment,
|
||||
new_checking_id: Optional[str] = None,
|
||||
conn: Optional[Connection] = None,
|
||||
new_checking_id: str | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> None:
|
||||
await (conn or db).update(
|
||||
"apipayments", payment, "WHERE checking_id = :checking_id"
|
||||
@@ -311,9 +311,9 @@ async def update_payment(
|
||||
|
||||
|
||||
async def get_payments_history(
|
||||
wallet_id: Optional[str] = None,
|
||||
wallet_id: str | None = None,
|
||||
group: DateTrunc = "day",
|
||||
filters: Optional[Filters] = None,
|
||||
filters: Filters | None = None,
|
||||
) -> list[PaymentHistoryPoint]:
|
||||
if not filters:
|
||||
filters = Filters()
|
||||
@@ -376,9 +376,9 @@ async def get_payments_history(
|
||||
|
||||
async def get_payment_count_stats(
|
||||
field: PaymentCountField,
|
||||
filters: Optional[Filters[PaymentFilters]] = None,
|
||||
user_id: Optional[str] = None,
|
||||
conn: Optional[Connection] = None,
|
||||
filters: Filters[PaymentFilters] | None = None,
|
||||
user_id: str | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> list[PaymentCountStat]:
|
||||
|
||||
if not filters:
|
||||
@@ -409,9 +409,9 @@ async def get_payment_count_stats(
|
||||
|
||||
|
||||
async def get_daily_stats(
|
||||
filters: Optional[Filters[PaymentFilters]] = None,
|
||||
user_id: Optional[str] = None,
|
||||
conn: Optional[Connection] = None,
|
||||
filters: Filters[PaymentFilters] | None = None,
|
||||
user_id: str | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> tuple[list[PaymentDailyStats], list[PaymentDailyStats]]:
|
||||
|
||||
if not filters:
|
||||
@@ -459,9 +459,9 @@ async def get_daily_stats(
|
||||
|
||||
|
||||
async def get_wallets_stats(
|
||||
filters: Optional[Filters[PaymentFilters]] = None,
|
||||
user_id: Optional[str] = None,
|
||||
conn: Optional[Connection] = None,
|
||||
filters: Filters[PaymentFilters] | None = None,
|
||||
user_id: str | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> list[PaymentWalletStats]:
|
||||
|
||||
if not filters:
|
||||
@@ -508,7 +508,7 @@ async def get_wallets_stats(
|
||||
|
||||
|
||||
async def delete_wallet_payment(
|
||||
checking_id: str, wallet_id: str, conn: Optional[Connection] = None
|
||||
checking_id: str, wallet_id: str, conn: Connection | None = None
|
||||
) -> None:
|
||||
await (conn or db).execute(
|
||||
"DELETE FROM apipayments WHERE checking_id = :checking_id AND wallet = :wallet",
|
||||
@@ -517,8 +517,8 @@ async def delete_wallet_payment(
|
||||
|
||||
|
||||
async def check_internal(
|
||||
payment_hash: str, conn: Optional[Connection] = None
|
||||
) -> Optional[Payment]:
|
||||
payment_hash: str, conn: Connection | None = None
|
||||
) -> Payment | None:
|
||||
"""
|
||||
Returns the checking_id of the internal payment if it exists,
|
||||
otherwise None
|
||||
@@ -534,7 +534,7 @@ async def check_internal(
|
||||
|
||||
|
||||
async def is_internal_status_success(
|
||||
payment_hash: str, conn: Optional[Connection] = None
|
||||
payment_hash: str, conn: Connection | None = None
|
||||
) -> bool:
|
||||
"""
|
||||
Returns True if the internal payment was found and is successful,
|
||||
@@ -563,7 +563,7 @@ async def mark_webhook_sent(payment_hash: str, status: str) -> None:
|
||||
|
||||
|
||||
async def _only_user_wallets_statement(
|
||||
user_id: str, conn: Optional[Connection] = None
|
||||
user_id: str, conn: Connection | None = None
|
||||
) -> str:
|
||||
wallet_ids = await get_wallets_ids(user_id=user_id, conn=conn) or [
|
||||
"no-wallets-for-user"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import json
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
|
||||
from loguru import logger
|
||||
|
||||
@@ -14,7 +14,7 @@ from lnbits.settings import (
|
||||
)
|
||||
|
||||
|
||||
async def get_super_settings() -> Optional[SuperSettings]:
|
||||
async def get_super_settings() -> SuperSettings | None:
|
||||
data = await get_settings_by_tag("core")
|
||||
if data:
|
||||
super_user = await get_settings_field("super_user")
|
||||
@@ -24,7 +24,7 @@ async def get_super_settings() -> Optional[SuperSettings]:
|
||||
return None
|
||||
|
||||
|
||||
async def get_admin_settings(is_super_user: bool = False) -> Optional[AdminSettings]:
|
||||
async def get_admin_settings(is_super_user: bool = False) -> AdminSettings | None:
|
||||
sets = await get_super_settings()
|
||||
if not sets:
|
||||
return None
|
||||
@@ -41,7 +41,7 @@ async def get_admin_settings(is_super_user: bool = False) -> Optional[AdminSetti
|
||||
|
||||
|
||||
async def update_admin_settings(
|
||||
data: EditableSettings, tag: Optional[str] = "core"
|
||||
data: EditableSettings, tag: str | None = "core"
|
||||
) -> None:
|
||||
editable_settings = await get_settings_by_tag("core") or {}
|
||||
editable_settings.update(data.dict(exclude_unset=True))
|
||||
@@ -61,7 +61,7 @@ async def update_super_user(super_user: str) -> SuperSettings:
|
||||
return settings
|
||||
|
||||
|
||||
async def delete_admin_settings(tag: Optional[str] = "core") -> None:
|
||||
async def delete_admin_settings(tag: str | None = "core") -> None:
|
||||
await db.execute(
|
||||
"DELETE FROM system_settings WHERE tag = :tag",
|
||||
{"tag": tag},
|
||||
@@ -93,8 +93,8 @@ async def create_admin_settings(super_user: str, new_settings: dict) -> SuperSet
|
||||
|
||||
|
||||
async def get_settings_field(
|
||||
id_: str, tag: Optional[str] = "core"
|
||||
) -> Optional[SettingsField]:
|
||||
id_: str, tag: str | None = "core"
|
||||
) -> SettingsField | None:
|
||||
|
||||
row: dict = await db.fetchone(
|
||||
"""
|
||||
@@ -108,9 +108,7 @@ async def get_settings_field(
|
||||
return SettingsField(id=row["id"], value=json.loads(row["value"]), tag=row["tag"])
|
||||
|
||||
|
||||
async def set_settings_field(
|
||||
id_: str, value: Optional[Any], tag: Optional[str] = "core"
|
||||
):
|
||||
async def set_settings_field(id_: str, value: Any | None, tag: str | None = "core"):
|
||||
value = json.dumps(value) if value is not None else None
|
||||
await db.execute(
|
||||
"""
|
||||
@@ -122,7 +120,7 @@ async def set_settings_field(
|
||||
)
|
||||
|
||||
|
||||
async def get_settings_by_tag(tag: str) -> Optional[dict[str, Any]]:
|
||||
async def get_settings_by_tag(tag: str) -> dict[str, Any] | None:
|
||||
rows: list[dict] = await db.fetchall(
|
||||
"SELECT * FROM system_settings WHERE tag = :tag", {"tag": tag}
|
||||
)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from typing import Optional
|
||||
|
||||
import shortuuid
|
||||
|
||||
from lnbits.core.db import db
|
||||
@@ -19,7 +17,7 @@ async def create_tinyurl(domain: str, endless: bool, wallet: str):
|
||||
return await get_tinyurl(tinyurl_id)
|
||||
|
||||
|
||||
async def get_tinyurl(tinyurl_id: str) -> Optional[TinyURL]:
|
||||
async def get_tinyurl(tinyurl_id: str) -> TinyURL | None:
|
||||
return await db.fetchone(
|
||||
"SELECT * FROM tiny_url WHERE id = :tinyurl",
|
||||
{"tinyurl": tinyurl_id},
|
||||
|
||||
+20
-22
@@ -1,6 +1,6 @@
|
||||
from datetime import datetime, timezone
|
||||
from time import time
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from lnbits.core.crud.extensions import get_user_active_extensions_ids
|
||||
@@ -18,8 +18,8 @@ from ..models import (
|
||||
|
||||
|
||||
async def create_account(
|
||||
account: Optional[Account] = None,
|
||||
conn: Optional[Connection] = None,
|
||||
account: Account | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> Account:
|
||||
if account:
|
||||
account.validate_fields()
|
||||
@@ -36,7 +36,7 @@ async def update_account(account: Account) -> Account:
|
||||
return account
|
||||
|
||||
|
||||
async def delete_account(user_id: str, conn: Optional[Connection] = None) -> None:
|
||||
async def delete_account(user_id: str, conn: Connection | None = None) -> None:
|
||||
await (conn or db).execute(
|
||||
"DELETE from accounts WHERE id = :user",
|
||||
{"user": user_id},
|
||||
@@ -44,8 +44,8 @@ async def delete_account(user_id: str, conn: Optional[Connection] = None) -> Non
|
||||
|
||||
|
||||
async def get_accounts(
|
||||
filters: Optional[Filters[AccountFilters]] = None,
|
||||
conn: Optional[Connection] = None,
|
||||
filters: Filters[AccountFilters] | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> Page[AccountOverview]:
|
||||
where_clauses = []
|
||||
values: dict[str, Any] = {}
|
||||
@@ -92,9 +92,7 @@ async def get_accounts(
|
||||
)
|
||||
|
||||
|
||||
async def get_account(
|
||||
user_id: str, conn: Optional[Connection] = None
|
||||
) -> Optional[Account]:
|
||||
async def get_account(user_id: str, conn: Connection | None = None) -> Account | None:
|
||||
if len(user_id) == 0:
|
||||
return None
|
||||
return await (conn or db).fetchone(
|
||||
@@ -106,7 +104,7 @@ async def get_account(
|
||||
|
||||
async def delete_accounts_no_wallets(
|
||||
time_delta: int,
|
||||
conn: Optional[Connection] = None,
|
||||
conn: Connection | None = None,
|
||||
) -> None:
|
||||
delta = int(time()) - time_delta
|
||||
await (conn or db).execute(
|
||||
@@ -125,8 +123,8 @@ async def delete_accounts_no_wallets(
|
||||
|
||||
|
||||
async def get_account_by_username(
|
||||
username: str, conn: Optional[Connection] = None
|
||||
) -> Optional[Account]:
|
||||
username: str, conn: Connection | None = None
|
||||
) -> Account | None:
|
||||
if len(username) == 0:
|
||||
return None
|
||||
return await (conn or db).fetchone(
|
||||
@@ -137,8 +135,8 @@ async def get_account_by_username(
|
||||
|
||||
|
||||
async def get_account_by_pubkey(
|
||||
pubkey: str, conn: Optional[Connection] = None
|
||||
) -> Optional[Account]:
|
||||
pubkey: str, conn: Connection | None = None
|
||||
) -> Account | None:
|
||||
return await (conn or db).fetchone(
|
||||
"SELECT * FROM accounts WHERE LOWER(pubkey) = :pubkey",
|
||||
{"pubkey": pubkey.lower()},
|
||||
@@ -147,8 +145,8 @@ async def get_account_by_pubkey(
|
||||
|
||||
|
||||
async def get_account_by_email(
|
||||
email: str, conn: Optional[Connection] = None
|
||||
) -> Optional[Account]:
|
||||
email: str, conn: Connection | None = None
|
||||
) -> Account | None:
|
||||
if len(email) == 0:
|
||||
return None
|
||||
return await (conn or db).fetchone(
|
||||
@@ -159,8 +157,8 @@ async def get_account_by_email(
|
||||
|
||||
|
||||
async def get_account_by_username_or_email(
|
||||
username_or_email: str, conn: Optional[Connection] = None
|
||||
) -> Optional[Account]:
|
||||
username_or_email: str, conn: Connection | None = None
|
||||
) -> Account | None:
|
||||
return await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT * FROM accounts
|
||||
@@ -171,7 +169,7 @@ async def get_account_by_username_or_email(
|
||||
)
|
||||
|
||||
|
||||
async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[User]:
|
||||
async def get_user(user_id: str, conn: Connection | None = None) -> User | None:
|
||||
account = await get_account(user_id, conn)
|
||||
if not account:
|
||||
return None
|
||||
@@ -179,8 +177,8 @@ async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[
|
||||
|
||||
|
||||
async def get_user_from_account(
|
||||
account: Account, conn: Optional[Connection] = None
|
||||
) -> Optional[User]:
|
||||
account: Account, conn: Connection | None = None
|
||||
) -> User | None:
|
||||
extensions = await get_user_active_extensions_ids(account.id, conn)
|
||||
wallets = await get_wallets(account.id, False, conn=conn)
|
||||
return User(
|
||||
@@ -207,7 +205,7 @@ async def update_user_access_control_list(user_acls: UserAcls):
|
||||
|
||||
|
||||
async def get_user_access_control_lists(
|
||||
user_id: str, conn: Optional[Connection] = None
|
||||
user_id: str, conn: Connection | None = None
|
||||
) -> UserAcls:
|
||||
user_acls = await (conn or db).fetchone(
|
||||
"SELECT id, access_control_list FROM accounts WHERE id = :id",
|
||||
|
||||
+19
-22
@@ -1,6 +1,5 @@
|
||||
from datetime import datetime, timezone
|
||||
from time import time
|
||||
from typing import Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from lnbits.core.db import db
|
||||
@@ -14,8 +13,8 @@ from ..models import Wallet
|
||||
async def create_wallet(
|
||||
*,
|
||||
user_id: str,
|
||||
wallet_name: Optional[str] = None,
|
||||
conn: Optional[Connection] = None,
|
||||
wallet_name: str | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> Wallet:
|
||||
wallet_id = uuid4().hex
|
||||
wallet = Wallet(
|
||||
@@ -32,7 +31,7 @@ async def create_wallet(
|
||||
|
||||
async def update_wallet(
|
||||
wallet: Wallet,
|
||||
conn: Optional[Connection] = None,
|
||||
conn: Connection | None = None,
|
||||
) -> Wallet:
|
||||
wallet.updated_at = datetime.now(timezone.utc)
|
||||
await (conn or db).update("wallets", wallet)
|
||||
@@ -44,7 +43,7 @@ async def delete_wallet(
|
||||
user_id: str,
|
||||
wallet_id: str,
|
||||
deleted: bool = True,
|
||||
conn: Optional[Connection] = None,
|
||||
conn: Connection | None = None,
|
||||
) -> None:
|
||||
now = int(time())
|
||||
await (conn or db).execute(
|
||||
@@ -58,9 +57,7 @@ async def delete_wallet(
|
||||
)
|
||||
|
||||
|
||||
async def force_delete_wallet(
|
||||
wallet_id: str, conn: Optional[Connection] = None
|
||||
) -> None:
|
||||
async def force_delete_wallet(wallet_id: str, conn: Connection | None = None) -> None:
|
||||
await (conn or db).execute(
|
||||
"DELETE FROM wallets WHERE id = :wallet",
|
||||
{"wallet": wallet_id},
|
||||
@@ -68,8 +65,8 @@ async def force_delete_wallet(
|
||||
|
||||
|
||||
async def delete_wallet_by_id(
|
||||
wallet_id: str, conn: Optional[Connection] = None
|
||||
) -> Optional[int]:
|
||||
wallet_id: str, conn: Connection | None = None
|
||||
) -> int | None:
|
||||
now = int(time())
|
||||
result = await (conn or db).execute(
|
||||
# Timestamp placeholder is safe from SQL injection (not user input)
|
||||
@@ -83,13 +80,13 @@ async def delete_wallet_by_id(
|
||||
return result.rowcount
|
||||
|
||||
|
||||
async def remove_deleted_wallets(conn: Optional[Connection] = None) -> None:
|
||||
async def remove_deleted_wallets(conn: Connection | None = None) -> None:
|
||||
await (conn or db).execute("DELETE FROM wallets WHERE deleted = true")
|
||||
|
||||
|
||||
async def delete_unused_wallets(
|
||||
time_delta: int,
|
||||
conn: Optional[Connection] = None,
|
||||
conn: Connection | None = None,
|
||||
) -> None:
|
||||
delta = int(time()) - time_delta
|
||||
await (conn or db).execute(
|
||||
@@ -107,8 +104,8 @@ async def delete_unused_wallets(
|
||||
|
||||
|
||||
async def get_wallet(
|
||||
wallet_id: str, deleted: Optional[bool] = None, conn: Optional[Connection] = None
|
||||
) -> Optional[Wallet]:
|
||||
wallet_id: str, deleted: bool | None = None, conn: Connection | None = None
|
||||
) -> Wallet | None:
|
||||
query = """
|
||||
SELECT *, COALESCE((
|
||||
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
||||
@@ -125,7 +122,7 @@ async def get_wallet(
|
||||
|
||||
|
||||
async def get_wallets(
|
||||
user_id: str, deleted: Optional[bool] = None, conn: Optional[Connection] = None
|
||||
user_id: str, deleted: bool | None = None, conn: Connection | None = None
|
||||
) -> list[Wallet]:
|
||||
query = """
|
||||
SELECT *, COALESCE((
|
||||
@@ -144,9 +141,9 @@ async def get_wallets(
|
||||
|
||||
async def get_wallets_paginated(
|
||||
user_id: str,
|
||||
deleted: Optional[bool] = None,
|
||||
filters: Optional[Filters[WalletsFilters]] = None,
|
||||
conn: Optional[Connection] = None,
|
||||
deleted: bool | None = None,
|
||||
filters: Filters[WalletsFilters] | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> Page[Wallet]:
|
||||
if deleted is None:
|
||||
deleted = False
|
||||
@@ -166,7 +163,7 @@ async def get_wallets_paginated(
|
||||
|
||||
|
||||
async def get_wallets_ids(
|
||||
user_id: str, deleted: Optional[bool] = None, conn: Optional[Connection] = None
|
||||
user_id: str, deleted: bool | None = None, conn: Connection | None = None
|
||||
) -> list[str]:
|
||||
query = """SELECT id FROM wallets WHERE "user" = :user"""
|
||||
if deleted is not None:
|
||||
@@ -186,8 +183,8 @@ async def get_wallets_count():
|
||||
|
||||
async def get_wallet_for_key(
|
||||
key: str,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> Optional[Wallet]:
|
||||
conn: Connection | None = None,
|
||||
) -> Wallet | None:
|
||||
return await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT *, COALESCE((
|
||||
@@ -201,7 +198,7 @@ async def get_wallet_for_key(
|
||||
)
|
||||
|
||||
|
||||
async def get_total_balance(conn: Optional[Connection] = None):
|
||||
async def get_total_balance(conn: Connection | None = None):
|
||||
result = await (conn or db).execute("SELECT SUM(balance) as balance FROM balances")
|
||||
row = result.mappings().first()
|
||||
return row.get("balance", 0) or 0
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from typing import Optional
|
||||
|
||||
from lnbits.core.db import db
|
||||
|
||||
from ..models import WebPushSubscription
|
||||
@@ -7,7 +5,7 @@ from ..models import WebPushSubscription
|
||||
|
||||
async def get_webpush_subscription(
|
||||
endpoint: str, user: str
|
||||
) -> Optional[WebPushSubscription]:
|
||||
) -> WebPushSubscription | None:
|
||||
return await db.fetchone(
|
||||
"""
|
||||
SELECT * FROM webpush_subscriptions
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import importlib
|
||||
import re
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
from uuid import UUID
|
||||
|
||||
@@ -20,7 +20,7 @@ from lnbits.settings import settings
|
||||
|
||||
|
||||
async def migrate_extension_database(
|
||||
ext: InstallableExtension, current_version: Optional[DbVersion] = None
|
||||
ext: InstallableExtension, current_version: DbVersion | None = None
|
||||
):
|
||||
|
||||
try:
|
||||
@@ -38,7 +38,7 @@ async def run_migration(
|
||||
db: Connection,
|
||||
migrations_module: Any,
|
||||
db_name: str,
|
||||
current_version: Optional[DbVersion] = None,
|
||||
current_version: DbVersion | None = None,
|
||||
):
|
||||
matcher = re.compile(r"^m(\d\d\d)_")
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from time import time
|
||||
from typing import Optional
|
||||
|
||||
from lnurl import LnAddress, Lnurl, LnurlPayResponse
|
||||
from pydantic import BaseModel, Field
|
||||
@@ -9,9 +8,9 @@ class CreateLnurlPayment(BaseModel):
|
||||
res: LnurlPayResponse | None = None
|
||||
lnurl: Lnurl | LnAddress | None = None
|
||||
amount: int
|
||||
comment: Optional[str] = None
|
||||
unit: Optional[str] = None
|
||||
internal_memo: Optional[str] = None
|
||||
comment: str | None = None
|
||||
unit: str | None = None
|
||||
internal_memo: str | None = None
|
||||
|
||||
|
||||
class CreateLnurlWithdraw(BaseModel):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable
|
||||
from collections.abc import Callable
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ from .funding_source import (
|
||||
get_balance_delta,
|
||||
switch_to_voidwallet,
|
||||
)
|
||||
from .lnurl import fetch_lnurl_pay_request, get_pr_from_lnurl
|
||||
from .lnurl import fetch_lnurl_pay_request, get_pr_from_lnurl, perform_withdraw
|
||||
from .notifications import enqueue_admin_notification, send_payment_notification
|
||||
from .payments import (
|
||||
calculate_fiat_amounts,
|
||||
@@ -57,6 +57,7 @@ __all__ = [
|
||||
"get_payments_daily_stats",
|
||||
"get_pr_from_lnurl",
|
||||
"pay_invoice",
|
||||
"perform_withdraw",
|
||||
"send_payment_notification",
|
||||
"service_fee",
|
||||
"settle_hold_invoice",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import asyncio
|
||||
import importlib
|
||||
from typing import Optional
|
||||
|
||||
from loguru import logger
|
||||
|
||||
@@ -74,11 +73,13 @@ async def uninstall_extension(ext_id: str):
|
||||
async def activate_extension(ext: Extension):
|
||||
core_app_extra.register_new_ext_routes(ext)
|
||||
await update_installed_extension_state(ext_id=ext.code, active=True)
|
||||
await start_extension_background_work(ext.code)
|
||||
|
||||
|
||||
async def deactivate_extension(ext_id: str):
|
||||
settings.deactivate_extension_paths(ext_id)
|
||||
await update_installed_extension_state(ext_id=ext_id, active=False)
|
||||
await stop_extension_background_work(ext_id)
|
||||
|
||||
|
||||
async def stop_extension_background_work(ext_id: str) -> bool:
|
||||
@@ -145,7 +146,7 @@ async def start_extension_background_work(ext_id: str) -> bool:
|
||||
|
||||
|
||||
async def get_valid_extensions(
|
||||
include_deactivated: Optional[bool] = True,
|
||||
include_deactivated: bool | None = True,
|
||||
) -> list[Extension]:
|
||||
installed_extensions = await get_installed_extensions()
|
||||
valid_extensions = [Extension.from_installable_ext(e) for e in installed_extensions]
|
||||
@@ -164,8 +165,8 @@ async def get_valid_extensions(
|
||||
|
||||
|
||||
async def get_valid_extension(
|
||||
ext_id: str, include_deactivated: Optional[bool] = True
|
||||
) -> Optional[Extension]:
|
||||
ext_id: str, include_deactivated: bool | None = True
|
||||
) -> Extension | None:
|
||||
ext = await get_installed_extension(ext_id)
|
||||
if not ext:
|
||||
return None
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import hashlib
|
||||
import hmac
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from loguru import logger
|
||||
|
||||
@@ -15,7 +14,7 @@ from lnbits.settings import settings
|
||||
|
||||
|
||||
async def handle_fiat_payment_confirmation(
|
||||
payment: Payment, conn: Optional[Connection] = None
|
||||
payment: Payment, conn: Connection | None = None
|
||||
):
|
||||
try:
|
||||
await _credit_fiat_service_fee_wallet(payment, conn=conn)
|
||||
@@ -29,7 +28,7 @@ async def handle_fiat_payment_confirmation(
|
||||
|
||||
|
||||
async def _credit_fiat_service_fee_wallet(
|
||||
payment: Payment, conn: Optional[Connection] = None
|
||||
payment: Payment, conn: Connection | None = None
|
||||
):
|
||||
fiat_provider_name = payment.fiat_provider
|
||||
if not fiat_provider_name:
|
||||
@@ -66,7 +65,7 @@ async def _credit_fiat_service_fee_wallet(
|
||||
|
||||
|
||||
async def _debit_fiat_service_faucet_wallet(
|
||||
payment: Payment, conn: Optional[Connection] = None
|
||||
payment: Payment, conn: Connection | None = None
|
||||
):
|
||||
fiat_provider_name = payment.fiat_provider
|
||||
if not fiat_provider_name:
|
||||
@@ -129,8 +128,8 @@ async def handle_stripe_event(event: dict):
|
||||
|
||||
def check_stripe_signature(
|
||||
payload: bytes,
|
||||
sig_header: Optional[str],
|
||||
secret: Optional[str],
|
||||
sig_header: str | None,
|
||||
secret: str | None,
|
||||
tolerance_seconds=300,
|
||||
):
|
||||
if not sig_header:
|
||||
|
||||
@@ -7,7 +7,10 @@ from lnurl import (
|
||||
LnurlPayActionResponse,
|
||||
LnurlPayResponse,
|
||||
LnurlResponseException,
|
||||
LnurlSuccessResponse,
|
||||
LnurlWithdrawResponse,
|
||||
execute_pay_request,
|
||||
execute_withdraw,
|
||||
handle,
|
||||
)
|
||||
from loguru import logger
|
||||
@@ -15,10 +18,36 @@ from loguru import logger
|
||||
from lnbits.core.crud import update_wallet
|
||||
from lnbits.core.models import CreateLnurlPayment, Wallet
|
||||
from lnbits.core.models.lnurl import StoredPayLink
|
||||
from lnbits.helpers import check_callback_url
|
||||
from lnbits.settings import settings
|
||||
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
|
||||
|
||||
|
||||
async def perform_withdraw(lnurl: str, payment_request: str) -> None:
|
||||
"""
|
||||
Perform an LNURL withdraw to the given LNURL-withdraw link.
|
||||
:param lnurl: The LNURL-withdraw link. bech32 or lud17 format.
|
||||
:param payment_request: The BOLT11 payment request to pay.
|
||||
:raises LnurlResponseException: If the LNURL-withdraw process fails.
|
||||
"""
|
||||
res = await handle(lnurl, user_agent=settings.user_agent, timeout=10)
|
||||
if isinstance(res, LnurlErrorResponse):
|
||||
raise LnurlResponseException(res.reason)
|
||||
if not isinstance(res, LnurlWithdrawResponse):
|
||||
raise LnurlResponseException("Invalid LNURL-withdraw response.")
|
||||
try:
|
||||
check_callback_url(res.callback)
|
||||
except ValueError as exc:
|
||||
raise LnurlResponseException(f"Invalid callback URL: {exc!s}") from exc
|
||||
res2 = await execute_withdraw(
|
||||
res, payment_request, user_agent=settings.user_agent, timeout=10
|
||||
)
|
||||
if isinstance(res2, LnurlErrorResponse):
|
||||
raise LnurlResponseException(res2.reason)
|
||||
if not isinstance(res2, LnurlSuccessResponse):
|
||||
raise LnurlResponseException("Invalid LNURL-withdraw success response.")
|
||||
|
||||
|
||||
async def get_pr_from_lnurl(
|
||||
lnurl: str, amount_msat: int, comment: str | None = None
|
||||
) -> str:
|
||||
|
||||
@@ -4,7 +4,6 @@ import smtplib
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from http import HTTPStatus
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
@@ -71,7 +70,7 @@ async def process_next_notification() -> None:
|
||||
|
||||
async def send_admin_notification(
|
||||
message: str,
|
||||
message_type: Optional[str] = None,
|
||||
message_type: str | None = None,
|
||||
) -> None:
|
||||
return await send_notification(
|
||||
settings.lnbits_telegram_notifications_chat_id,
|
||||
@@ -85,7 +84,7 @@ async def send_admin_notification(
|
||||
async def send_user_notification(
|
||||
user_notifications: UserNotifications,
|
||||
message: str,
|
||||
message_type: Optional[str] = None,
|
||||
message_type: str | None = None,
|
||||
) -> None:
|
||||
|
||||
email_address = (
|
||||
@@ -110,7 +109,7 @@ async def send_notification(
|
||||
nostr_identifiers: list[str] | None,
|
||||
email_addresses: list[str] | None,
|
||||
message: str,
|
||||
message_type: Optional[str] = None,
|
||||
message_type: str | None = None,
|
||||
) -> None:
|
||||
try:
|
||||
if telegram_chat_id and settings.is_telegram_notifications_configured():
|
||||
@@ -278,7 +277,7 @@ async def send_payment_notification(wallet: Wallet, payment: Payment):
|
||||
|
||||
async def send_ws_payment_notification(wallet: Wallet, payment: Payment):
|
||||
# TODO: websocket message should be a clean payment model
|
||||
# await websocket_manager.send_data(payment.json(), wallet.inkey)
|
||||
# await websocket_manager.send(wallet.inkey, payment.json())
|
||||
# TODO: figure out why we send the balance with the payment here.
|
||||
# cleaner would be to have a separate message for the balance
|
||||
# and send it with the id of the wallet so wallets can subscribe to it
|
||||
@@ -289,12 +288,11 @@ async def send_ws_payment_notification(wallet: Wallet, payment: Payment):
|
||||
"payment": json.loads(payment.json()),
|
||||
},
|
||||
)
|
||||
await websocket_manager.send_data(payment_notification, wallet.inkey)
|
||||
await websocket_manager.send_data(payment_notification, wallet.adminkey)
|
||||
|
||||
await websocket_manager.send_data(
|
||||
json.dumps({"pending": payment.pending, "status": payment.status}),
|
||||
await websocket_manager.send(wallet.inkey, payment_notification)
|
||||
await websocket_manager.send(wallet.adminkey, payment_notification)
|
||||
await websocket_manager.send(
|
||||
payment.payment_hash,
|
||||
json.dumps({"pending": payment.pending, "status": payment.status}),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import asyncio
|
||||
import time
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional
|
||||
|
||||
from bolt11 import Bolt11, MilliSatoshi, Tags
|
||||
from bolt11 import decode as bolt11_decode
|
||||
@@ -58,11 +57,11 @@ async def pay_invoice(
|
||||
*,
|
||||
wallet_id: str,
|
||||
payment_request: str,
|
||||
max_sat: Optional[int] = None,
|
||||
extra: Optional[dict] = None,
|
||||
max_sat: int | None = None,
|
||||
extra: dict | None = None,
|
||||
description: str = "",
|
||||
tag: str = "",
|
||||
conn: Optional[Connection] = None,
|
||||
conn: Connection | None = None,
|
||||
) -> Payment:
|
||||
if settings.lnbits_only_allow_incoming_payments:
|
||||
raise PaymentError("Only incoming payments allowed.", status="failed")
|
||||
@@ -110,7 +109,7 @@ async def create_payment_request(
|
||||
|
||||
|
||||
async def create_fiat_invoice(
|
||||
wallet_id: str, invoice_data: CreateInvoice, conn: Optional[Connection] = None
|
||||
wallet_id: str, invoice_data: CreateInvoice, conn: Connection | None = None
|
||||
):
|
||||
fiat_provider_name = invoice_data.fiat_provider
|
||||
if not fiat_provider_name:
|
||||
@@ -140,7 +139,9 @@ async def create_fiat_invoice(
|
||||
payment_hash=internal_payment.payment_hash,
|
||||
currency=invoice_data.unit,
|
||||
memo=invoice_data.memo,
|
||||
extra=invoice_data.extra or {},
|
||||
)
|
||||
|
||||
if fiat_invoice.failed:
|
||||
logger.warning(fiat_invoice.error_message)
|
||||
internal_payment.status = PaymentState.FAILED
|
||||
@@ -231,16 +232,16 @@ async def create_invoice(
|
||||
*,
|
||||
wallet_id: str,
|
||||
amount: float,
|
||||
currency: Optional[str] = "sat",
|
||||
currency: str | None = "sat",
|
||||
memo: str,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
expiry: Optional[int] = None,
|
||||
extra: Optional[dict] = None,
|
||||
webhook: Optional[str] = None,
|
||||
internal: Optional[bool] = False,
|
||||
description_hash: bytes | None = None,
|
||||
unhashed_description: bytes | None = None,
|
||||
expiry: int | None = None,
|
||||
extra: dict | None = None,
|
||||
webhook: str | None = None,
|
||||
internal: bool | None = False,
|
||||
payment_hash: str | None = None,
|
||||
conn: Optional[Connection] = None,
|
||||
conn: Connection | None = None,
|
||||
) -> Payment:
|
||||
if not amount > 0:
|
||||
raise InvoiceError("Amountless invoices not supported.", status="failed")
|
||||
@@ -427,7 +428,7 @@ def service_fee_fiat(amount_msat: int, fiat_provider_name: str) -> int:
|
||||
async def update_wallet_balance(
|
||||
wallet: Wallet,
|
||||
amount: int,
|
||||
conn: Optional[Connection] = None,
|
||||
conn: Connection | None = None,
|
||||
):
|
||||
if amount == 0:
|
||||
raise ValueError("Amount cannot be 0.")
|
||||
@@ -486,14 +487,14 @@ async def update_wallet_balance(
|
||||
|
||||
|
||||
async def check_wallet_limits(
|
||||
wallet_id: str, amount_msat: int, conn: Optional[Connection] = None
|
||||
wallet_id: str, amount_msat: int, conn: Connection | None = None
|
||||
):
|
||||
await check_time_limit_between_transactions(wallet_id, conn)
|
||||
await check_wallet_daily_withdraw_limit(wallet_id, amount_msat, conn)
|
||||
|
||||
|
||||
async def check_time_limit_between_transactions(
|
||||
wallet_id: str, conn: Optional[Connection] = None
|
||||
wallet_id: str, conn: Connection | None = None
|
||||
):
|
||||
limit = settings.lnbits_wallet_limit_secs_between_trans
|
||||
if not limit or limit <= 0:
|
||||
@@ -513,7 +514,7 @@ async def check_time_limit_between_transactions(
|
||||
|
||||
|
||||
async def check_wallet_daily_withdraw_limit(
|
||||
wallet_id: str, amount_msat: int, conn: Optional[Connection] = None
|
||||
wallet_id: str, amount_msat: int, conn: Connection | None = None
|
||||
):
|
||||
limit = settings.lnbits_wallet_limit_daily_max_withdraw
|
||||
if not limit:
|
||||
@@ -546,8 +547,8 @@ async def check_wallet_daily_withdraw_limit(
|
||||
async def calculate_fiat_amounts(
|
||||
amount: float,
|
||||
wallet: Wallet,
|
||||
currency: Optional[str] = None,
|
||||
extra: Optional[dict] = None,
|
||||
currency: str | None = None,
|
||||
extra: dict | None = None,
|
||||
) -> tuple[int, dict]:
|
||||
wallet_currency = wallet.currency or settings.lnbits_default_accounting_currency
|
||||
fiat_amounts: dict = extra or {}
|
||||
@@ -582,9 +583,9 @@ async def calculate_fiat_amounts(
|
||||
|
||||
|
||||
async def check_transaction_status(
|
||||
wallet_id: str, payment_hash: str, conn: Optional[Connection] = None
|
||||
wallet_id: str, payment_hash: str, conn: Connection | None = None
|
||||
) -> PaymentStatus:
|
||||
payment: Optional[Payment] = await get_wallet_payment(
|
||||
payment: Payment | None = await get_wallet_payment(
|
||||
wallet_id, payment_hash, conn=conn
|
||||
)
|
||||
if not payment:
|
||||
@@ -598,7 +599,7 @@ async def check_transaction_status(
|
||||
|
||||
async def get_payments_daily_stats(
|
||||
filters: Filters[PaymentFilters],
|
||||
user_id: Optional[str] = None,
|
||||
user_id: str | None = None,
|
||||
) -> list[PaymentDailyStats]:
|
||||
data_in, data_out = await get_daily_stats(filters, user_id=user_id)
|
||||
balance_total: float = 0
|
||||
@@ -647,7 +648,7 @@ async def get_payments_daily_stats(
|
||||
async def _pay_invoice(
|
||||
wallet_id: str,
|
||||
create_payment_model: CreatePayment,
|
||||
conn: Optional[Connection] = None,
|
||||
conn: Connection | None = None,
|
||||
):
|
||||
async with payment_lock:
|
||||
if wallet_id not in wallets_payments_lock:
|
||||
@@ -670,8 +671,8 @@ async def _pay_invoice(
|
||||
async def _pay_internal_invoice(
|
||||
wallet: Wallet,
|
||||
create_payment_model: CreatePayment,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> Optional[Payment]:
|
||||
conn: Connection | None = None,
|
||||
) -> Payment | None:
|
||||
"""
|
||||
Pay an internal payment.
|
||||
returns None if the payment is not internal.
|
||||
@@ -738,7 +739,7 @@ async def _pay_internal_invoice(
|
||||
async def _pay_external_invoice(
|
||||
wallet: Wallet,
|
||||
create_payment_model: CreatePayment,
|
||||
conn: Optional[Connection] = None,
|
||||
conn: Connection | None = None,
|
||||
) -> Payment:
|
||||
checking_id = create_payment_model.payment_hash
|
||||
amount_msat = create_payment_model.amount_msat
|
||||
@@ -807,7 +808,7 @@ async def _pay_external_invoice(
|
||||
async def update_payment_success_status(
|
||||
payment: Payment,
|
||||
status: PaymentStatus,
|
||||
conn: Optional[Connection] = None,
|
||||
conn: Connection | None = None,
|
||||
) -> Payment:
|
||||
if status.success:
|
||||
service_fee_msat = service_fee(payment.amount, internal=False)
|
||||
@@ -831,7 +832,7 @@ async def _fundingsource_pay_invoice(
|
||||
|
||||
|
||||
async def _verify_external_payment(
|
||||
payment: Payment, conn: Optional[Connection] = None
|
||||
payment: Payment, conn: Connection | None = None
|
||||
) -> Payment:
|
||||
# fail on pending payments
|
||||
if payment.pending:
|
||||
@@ -862,7 +863,7 @@ async def _check_wallet_for_payment(
|
||||
wallet_id: str,
|
||||
tag: str,
|
||||
amount_msat: int,
|
||||
conn: Optional[Connection] = None,
|
||||
conn: Connection | None = None,
|
||||
):
|
||||
wallet = await get_wallet(wallet_id, conn=conn)
|
||||
if not wallet:
|
||||
@@ -878,7 +879,7 @@ async def _check_wallet_for_payment(
|
||||
|
||||
|
||||
def _validate_payment_request(
|
||||
payment_request: str, max_sat: Optional[int] = None
|
||||
payment_request: str, max_sat: int | None = None
|
||||
) -> Bolt11:
|
||||
try:
|
||||
invoice = bolt11_decode(payment_request)
|
||||
@@ -901,7 +902,7 @@ def _validate_payment_request(
|
||||
|
||||
|
||||
async def _credit_service_fee_wallet(
|
||||
wallet: Wallet, payment: Payment, conn: Optional[Connection] = None
|
||||
wallet: Wallet, payment: Payment, conn: Connection | None = None
|
||||
):
|
||||
service_fee_msat = service_fee(payment.amount, internal=payment.is_internal)
|
||||
if not settings.lnbits_service_fee_wallet or not service_fee_msat:
|
||||
@@ -927,7 +928,7 @@ async def _credit_service_fee_wallet(
|
||||
|
||||
|
||||
async def _check_fiat_invoice_limits(
|
||||
amount_sat: int, fiat_provider_name: str, conn: Optional[Connection] = None
|
||||
amount_sat: int, fiat_provider_name: str, conn: Connection | None = None
|
||||
):
|
||||
limits = settings.get_fiat_provider_limits(fiat_provider_name)
|
||||
if not limits:
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from loguru import logger
|
||||
@@ -37,7 +36,7 @@ from .settings import update_cached_settings
|
||||
|
||||
|
||||
async def create_user_account(
|
||||
account: Optional[Account] = None, wallet_name: Optional[str] = None
|
||||
account: Account | None = None, wallet_name: str | None = None
|
||||
) -> User:
|
||||
if not settings.new_accounts_allowed:
|
||||
raise ValueError("Account creation is disabled.")
|
||||
@@ -46,9 +45,9 @@ async def create_user_account(
|
||||
|
||||
|
||||
async def create_user_account_no_ckeck(
|
||||
account: Optional[Account] = None,
|
||||
wallet_name: Optional[str] = None,
|
||||
default_exts: Optional[list[str]] = None,
|
||||
account: Account | None = None,
|
||||
wallet_name: str | None = None,
|
||||
default_exts: list[str] | None = None,
|
||||
) -> User:
|
||||
|
||||
if account:
|
||||
@@ -165,12 +164,12 @@ async def check_admin_settings():
|
||||
settings.first_install = True
|
||||
|
||||
logger.success(
|
||||
"✔️ Admin UI is enabled. run `poetry run lnbits-cli superuser` "
|
||||
"✔️ Admin UI is enabled. run `uv run lnbits-cli superuser` "
|
||||
"to get the superuser."
|
||||
)
|
||||
|
||||
|
||||
async def init_admin_settings(super_user: Optional[str] = None) -> SuperSettings:
|
||||
async def init_admin_settings(super_user: str | None = None) -> SuperSettings:
|
||||
account = None
|
||||
if super_user:
|
||||
account = await get_account(super_user)
|
||||
|
||||
@@ -1,27 +1,65 @@
|
||||
from fastapi import WebSocket
|
||||
from asyncio import Queue
|
||||
from dataclasses import dataclass
|
||||
|
||||
from fastapi import WebSocket, WebSocketDisconnect
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.settings import settings
|
||||
|
||||
|
||||
@dataclass
|
||||
class WebsocketConnection:
|
||||
item_id: str
|
||||
websocket: WebSocket
|
||||
receive_queue: Queue[str]
|
||||
|
||||
|
||||
class WebsocketConnectionManager:
|
||||
def __init__(self) -> None:
|
||||
self.active_connections: list[WebSocket] = []
|
||||
self.active_connections: list[WebsocketConnection] = []
|
||||
|
||||
async def connect(self, websocket: WebSocket, item_id: str):
|
||||
async def connect(self, item_id: str, websocket: WebSocket) -> WebsocketConnection:
|
||||
logger.debug(f"Websocket connected to {item_id}")
|
||||
await websocket.accept()
|
||||
self.active_connections.append(websocket)
|
||||
conn = WebsocketConnection(
|
||||
item_id=item_id,
|
||||
websocket=websocket,
|
||||
receive_queue=Queue(),
|
||||
)
|
||||
self.active_connections.append(conn)
|
||||
return conn
|
||||
|
||||
def disconnect(self, websocket: WebSocket):
|
||||
self.active_connections.remove(websocket)
|
||||
async def listen(self, conn: WebsocketConnection) -> None:
|
||||
while settings.lnbits_running:
|
||||
try:
|
||||
data = await conn.websocket.receive_text()
|
||||
logger.debug(f"WS received data from {conn.item_id}: {data}")
|
||||
conn.receive_queue.put_nowait(data)
|
||||
except WebSocketDisconnect:
|
||||
for _conn in self.active_connections:
|
||||
if _conn.websocket == conn.websocket:
|
||||
self.active_connections.remove(_conn)
|
||||
logger.debug(f"WS disconnected from {conn.item_id}")
|
||||
break # out of the listen and the fastapi route
|
||||
|
||||
async def send_data(self, message: str, item_id: str):
|
||||
for connection in self.active_connections:
|
||||
if connection.path_params["item_id"] == item_id:
|
||||
await connection.send_text(message)
|
||||
def get_connections(self, item_id: str) -> list[WebsocketConnection]:
|
||||
conns = []
|
||||
for conn in self.active_connections:
|
||||
if conn.item_id == item_id:
|
||||
conns.append(conn)
|
||||
return conns
|
||||
|
||||
def has_connection(self, item_id: str) -> bool:
|
||||
return len(self.get_connections(item_id)) > 0
|
||||
|
||||
async def send(self, item_id: str, data: str) -> None:
|
||||
for conn in self.get_connections(item_id):
|
||||
await conn.websocket.send_text(data)
|
||||
|
||||
|
||||
websocket_manager = WebsocketConnectionManager()
|
||||
|
||||
|
||||
async def websocket_updater(item_id: str, data: str):
|
||||
return await websocket_manager.send_data(data, item_id)
|
||||
# deprecated import and use `websocket_manager.send()` instead
|
||||
async def websocket_updater(item_id: str, data: str) -> None:
|
||||
return await websocket_manager.send(item_id, data)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import asyncio
|
||||
import traceback
|
||||
from collections.abc import Coroutine
|
||||
from typing import Callable
|
||||
from collections.abc import Callable, Coroutine
|
||||
|
||||
from loguru import logger
|
||||
|
||||
|
||||
@@ -53,8 +53,7 @@
|
||||
<div class="q-pa-md">
|
||||
<lnbits-qrcode
|
||||
:value="wallet.adminkey"
|
||||
:options="{ width: 250 }"
|
||||
class="rounded-borders"
|
||||
:show-buttons="false"
|
||||
></lnbits-qrcode>
|
||||
</div>
|
||||
</q-popup-proxy>
|
||||
@@ -86,8 +85,7 @@
|
||||
<div class="q-pa-md">
|
||||
<lnbits-qrcode
|
||||
:value="wallet.inkey"
|
||||
:options="{ width: 250 }"
|
||||
class="rounded-borders"
|
||||
:show-buttons="false"
|
||||
></lnbits-qrcode>
|
||||
</div>
|
||||
</q-popup-proxy>
|
||||
|
||||
@@ -328,13 +328,10 @@
|
||||
<q-card v-if="selectedRelease" class="q-pa-lg lnbits__dialog-card">
|
||||
<q-card-section>
|
||||
<div v-if="selectedRelease.paymentRequest">
|
||||
<a :href="'lightning:' + selectedRelease.paymentRequest">
|
||||
<q-responsive :ratio="1" class="q-mx-xl">
|
||||
<lnbits-qrcode
|
||||
:value="'lightning:' + selectedRelease.paymentRequest.toUpperCase()"
|
||||
></lnbits-qrcode>
|
||||
</q-responsive>
|
||||
</a>
|
||||
<lnbits-qrcode
|
||||
:value="'lightning:' + selectedRelease.paymentRequest.toUpperCase()"
|
||||
:href="'lightning:' + selectedRelease.paymentRequest"
|
||||
></lnbits-qrcode>
|
||||
</div>
|
||||
<div v-else>
|
||||
<q-spinner-bars color="primary" size="2.55em"></q-spinner-bars>
|
||||
@@ -702,13 +699,10 @@
|
||||
<q-card-section v-if="selectedExtension.payToEnable.showQRCode">
|
||||
<div class="row q-mt-lg">
|
||||
<div v-if="selectedExtension.payToEnable.paymentRequest" class="col">
|
||||
<a
|
||||
<lnbits-qrcode
|
||||
:value="'lightning:' + selectedExtension.payToEnable.paymentRequest.toUpperCase()"
|
||||
:href="'lightning:' + selectedExtension.payToEnable.paymentRequest"
|
||||
>
|
||||
<lnbits-qrcode
|
||||
:value="'lightning:' + selectedExtension.payToEnable.paymentRequest.toUpperCase()"
|
||||
></lnbits-qrcode>
|
||||
</a>
|
||||
></lnbits-qrcode>
|
||||
</div>
|
||||
<div v-else class="col">
|
||||
<q-spinner-bars color="primary" size="2.55em"></q-spinner-bars>
|
||||
|
||||
@@ -209,16 +209,6 @@
|
||||
></payment-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<div id="hiddenQrCodeContainer" style="display: none">
|
||||
<lnbits-qrcode
|
||||
v-if="receive.fiatPaymentReq"
|
||||
:value="receive.fiatPaymentReq"
|
||||
></lnbits-qrcode>
|
||||
<lnbits-qrcode
|
||||
v-else
|
||||
:value="'lightning:' + (this.receive.paymentReq || '').toUpperCase()"
|
||||
></lnbits-qrcode>
|
||||
</div>
|
||||
</div>
|
||||
{% if HIDE_API %}
|
||||
<div class="col-12 col-md-4 q-gutter-y-md">
|
||||
@@ -277,13 +267,15 @@
|
||||
:label="$t('drain_funds')"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section class="text-center">
|
||||
<a href="lightning:{{wallet.lnurlwithdraw_full}}">
|
||||
<lnbits-qrcode
|
||||
:value="lightning:{{wallet.lnurlwithdraw_full}}"
|
||||
></lnbits-qrcode>
|
||||
</a>
|
||||
<p v-text="$t('drain_funds_desc')"></p>
|
||||
<q-card-section>
|
||||
<lnbits-qrcode
|
||||
value="lightning:{{wallet.lnurlwithdraw_full}}"
|
||||
href="lightning:{{wallet.lnurlwithdraw_full}}"
|
||||
></lnbits-qrcode>
|
||||
<p
|
||||
class="text-center"
|
||||
v-text="$t('drain_funds_desc')"
|
||||
></p>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
@@ -411,21 +403,15 @@
|
||||
:label="$t('export_to_phone')"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section class="text-center">
|
||||
<p v-text="$t('export_to_phone_desc')"></p>
|
||||
<q-card-section>
|
||||
<p
|
||||
class="text-center"
|
||||
v-text="$t('export_to_phone_desc')"
|
||||
></p>
|
||||
<lnbits-qrcode
|
||||
:value="`${baseUrl}wallet?usr=${g.user.id}&wal=${g.wallet.id}`"
|
||||
></lnbits-qrcode>
|
||||
</q-card-section>
|
||||
<span v-text="exportWalletQR"></span>
|
||||
<q-card-actions class="flex-center q-pb-md">
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
:label="$t('copy_wallet_url')"
|
||||
@click="copyText(`${baseUrl}wallet?usr=${g.user.id}&wal=${g.wallet.id}`)"
|
||||
></q-btn>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
</q-card-section>
|
||||
@@ -862,18 +848,19 @@
|
||||
v-else-if="receive.paymentReq && receive.lnurl == null"
|
||||
class="q-pa-lg q-pt-xl lnbits__dialog-card"
|
||||
>
|
||||
<div class="text-center q-mb-lg">
|
||||
<a
|
||||
v-if="receive.fiatPaymentReq"
|
||||
:href="receive.fiatPaymentReq"
|
||||
target="_blank"
|
||||
>
|
||||
<div v-html="invoiceQrCode"></div>
|
||||
</a>
|
||||
<a v-else :href="'lightning:' + receive.paymentReq">
|
||||
<div v-html="invoiceQrCode"></div>
|
||||
</a>
|
||||
</div>
|
||||
<lnbits-qrcode
|
||||
v-if="receive.fiatPaymentReq"
|
||||
:show-buttons="false"
|
||||
:href="receive.fiatPaymentReq"
|
||||
:value="receive.fiatPaymentReq"
|
||||
>
|
||||
</lnbits-qrcode>
|
||||
<lnbits-qrcode
|
||||
v-else
|
||||
:href="'lightning:' + receive.paymentReq"
|
||||
:value="'lightning:' + receive.paymentReq"
|
||||
>
|
||||
</lnbits-qrcode>
|
||||
<div class="text-center">
|
||||
<h3 class="q-my-md">
|
||||
<span v-text="formattedAmount"></span>
|
||||
@@ -898,12 +885,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
@click="copyText(receive.fiatPaymentReq || receive.paymentReq)"
|
||||
:label="$t('copy_invoice')"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
|
||||
@@ -5,7 +5,7 @@ from pathlib import Path
|
||||
from shutil import make_archive, move
|
||||
from subprocess import Popen
|
||||
from tempfile import NamedTemporaryFile
|
||||
from typing import IO, Optional
|
||||
from typing import IO
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import filetype
|
||||
@@ -71,10 +71,10 @@ async def api_test_email():
|
||||
)
|
||||
|
||||
|
||||
@admin_router.get("/api/v1/settings", response_model=Optional[AdminSettings])
|
||||
@admin_router.get("/api/v1/settings")
|
||||
async def api_get_settings(
|
||||
user: User = Depends(check_admin),
|
||||
) -> Optional[AdminSettings]:
|
||||
) -> AdminSettings | None:
|
||||
admin_settings = await get_admin_settings(user.super_user)
|
||||
return admin_settings
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import base64
|
||||
import importlib
|
||||
import json
|
||||
from collections.abc import Callable
|
||||
from http import HTTPStatus
|
||||
from time import time
|
||||
from typing import Callable, Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
@@ -238,7 +238,7 @@ async def api_delete_user_api_token(
|
||||
|
||||
@auth_router.get("/{provider}", description="SSO Provider")
|
||||
async def login_with_sso_provider(
|
||||
request: Request, provider: str, user_id: Optional[str] = None
|
||||
request: Request, provider: str, user_id: str | None = None
|
||||
):
|
||||
provider_sso = _new_sso(provider)
|
||||
if not provider_sso:
|
||||
@@ -319,7 +319,7 @@ async def update_pubkey(
|
||||
data: UpdateUserPubkey,
|
||||
user: User = Depends(check_user_exists),
|
||||
payload: AccessTokenPayload = Depends(access_token_payload),
|
||||
) -> Optional[User]:
|
||||
) -> User | None:
|
||||
if data.user_id != user.id:
|
||||
raise ValueError("Invalid user ID.")
|
||||
|
||||
@@ -345,7 +345,7 @@ async def update_password(
|
||||
data: UpdateUserPassword,
|
||||
user: User = Depends(check_user_exists),
|
||||
payload: AccessTokenPayload = Depends(access_token_payload),
|
||||
) -> Optional[User]:
|
||||
) -> User | None:
|
||||
_validate_auth_timeout(payload.auth_time)
|
||||
if data.user_id != user.id:
|
||||
raise ValueError("Invalid user ID.")
|
||||
@@ -419,7 +419,7 @@ async def reset_password(data: ResetUserPassword) -> JSONResponse:
|
||||
@auth_router.put("/update")
|
||||
async def update(
|
||||
data: UpdateUser, user: User = Depends(check_user_exists)
|
||||
) -> Optional[User]:
|
||||
) -> User | None:
|
||||
if data.user_id != user.id:
|
||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid user ID.")
|
||||
if data.username and not is_valid_username(data.username):
|
||||
@@ -461,7 +461,7 @@ async def first_install(data: UpdateSuperuserPassword) -> JSONResponse:
|
||||
return _auth_success_response(account.username, account.id, account.email)
|
||||
|
||||
|
||||
async def _handle_sso_login(userinfo: OpenID, verified_user_id: Optional[str] = None):
|
||||
async def _handle_sso_login(userinfo: OpenID, verified_user_id: str | None = None):
|
||||
email = userinfo.email
|
||||
if not email or not is_valid_email_address(email):
|
||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.")
|
||||
@@ -490,9 +490,9 @@ async def _handle_sso_login(userinfo: OpenID, verified_user_id: Optional[str] =
|
||||
|
||||
|
||||
def _auth_success_response(
|
||||
username: Optional[str] = None,
|
||||
user_id: Optional[str] = None,
|
||||
email: Optional[str] = None,
|
||||
username: str | None = None,
|
||||
user_id: str | None = None,
|
||||
email: str | None = None,
|
||||
) -> JSONResponse:
|
||||
payload = AccessTokenPayload(
|
||||
sub=username or "", usr=user_id, email=email, auth_time=int(time())
|
||||
@@ -533,7 +533,7 @@ def _auth_redirect_response(path: str, email: str) -> RedirectResponse:
|
||||
return response
|
||||
|
||||
|
||||
def _new_sso(provider: str) -> Optional[SSOBase]:
|
||||
def _new_sso(provider: str) -> SSOBase | None:
|
||||
try:
|
||||
if not settings.is_auth_method_allowed(AuthMethods(f"{provider}-auth")):
|
||||
return None
|
||||
@@ -610,7 +610,7 @@ def _nostr_nip98_event(request: Request) -> dict:
|
||||
|
||||
|
||||
def _check_nostr_event_tags(event: dict):
|
||||
method: Optional[str] = next((v for k, v in event["tags"] if k == "method"), None)
|
||||
method: str | None = next((v for k, v in event["tags"] if k == "method"), None)
|
||||
if not method:
|
||||
raise ValueError("Tag 'method' is missing.")
|
||||
if not method.upper() == "POST":
|
||||
@@ -625,7 +625,7 @@ def _check_nostr_event_tags(event: dict):
|
||||
raise ValueError(f"Invalid value for tag 'u': '{url}'.")
|
||||
|
||||
|
||||
def _validate_auth_timeout(auth_time: Optional[int] = 0):
|
||||
def _validate_auth_timeout(auth_time: int | None = 0):
|
||||
if abs(time() - (auth_time or 0)) > settings.auth_credetials_update_threshold:
|
||||
raise HTTPException(
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from http import HTTPStatus
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from lnbits.core.models.misc import SimpleStatus
|
||||
from lnbits.core.services.fiat_providers import test_connection
|
||||
from lnbits.decorators import check_admin
|
||||
from lnbits.fiat import StripeWallet, get_fiat_provider
|
||||
|
||||
fiat_router = APIRouter(tags=["Fiat API"], prefix="/api/v1/fiat")
|
||||
|
||||
@@ -16,3 +17,29 @@ fiat_router = APIRouter(tags=["Fiat API"], prefix="/api/v1/fiat")
|
||||
)
|
||||
async def api_test_fiat_provider(provider: str) -> SimpleStatus:
|
||||
return await test_connection(provider)
|
||||
|
||||
|
||||
@fiat_router.post(
|
||||
"/{provider}/connection_token",
|
||||
status_code=HTTPStatus.OK,
|
||||
dependencies=[Depends(check_admin)],
|
||||
)
|
||||
async def connection_token(provider: str):
|
||||
provider_wallet = await get_fiat_provider(provider)
|
||||
if provider == "stripe":
|
||||
if not isinstance(provider_wallet, StripeWallet):
|
||||
raise HTTPException(
|
||||
status_code=500, detail="Stripe wallet/provider not configured"
|
||||
)
|
||||
try:
|
||||
tok = await provider_wallet.create_terminal_connection_token()
|
||||
secret = tok.get("secret")
|
||||
if not secret:
|
||||
raise HTTPException(
|
||||
status_code=502, detail="Stripe returned no connection token"
|
||||
)
|
||||
return {"secret": secret}
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500, detail="Failed to create connection token"
|
||||
) from e
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from http import HTTPStatus
|
||||
from typing import Annotated, Optional, Union
|
||||
from typing import Annotated
|
||||
from urllib.parse import urlencode, urlparse
|
||||
|
||||
import httpx
|
||||
@@ -161,9 +161,9 @@ async def extensions(request: Request, user: User = Depends(check_user_exists)):
|
||||
)
|
||||
async def wallet(
|
||||
request: Request,
|
||||
lnbits_last_active_wallet: Annotated[Union[str, None], Cookie()] = None,
|
||||
lnbits_last_active_wallet: Annotated[str | None, Cookie()] = None,
|
||||
user: User = Depends(check_user_exists),
|
||||
wal: Optional[UUID4] = Query(None),
|
||||
wal: UUID4 | None = Query(None),
|
||||
):
|
||||
if wal:
|
||||
wallet = await get_wallet(wal.hex)
|
||||
|
||||
@@ -6,12 +6,8 @@ from fastapi import (
|
||||
Depends,
|
||||
HTTPException,
|
||||
)
|
||||
from lnurl import (
|
||||
LnurlResponseException,
|
||||
LnurlSuccessResponse,
|
||||
)
|
||||
from lnurl import LnurlResponseException
|
||||
from lnurl import execute_login as lnurlauth
|
||||
from lnurl import execute_withdraw as lnurl_withdraw
|
||||
from lnurl import handle as lnurl_handle
|
||||
from lnurl.models import (
|
||||
LnurlAuthResponse,
|
||||
@@ -22,7 +18,7 @@ from lnurl.models import (
|
||||
)
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.core.models import CreateLnurlWithdraw, Payment
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.core.models.lnurl import CreateLnurlPayment, LnurlScan
|
||||
from lnbits.decorators import (
|
||||
WalletTypeInfo,
|
||||
@@ -60,7 +56,7 @@ async def _handle(lnurl: str) -> LnurlResponseModel:
|
||||
)
|
||||
async def api_lnurlscan(code: str) -> LnurlResponseModel:
|
||||
res = await _handle(code)
|
||||
if isinstance(res, (LnurlPayResponse, LnurlWithdrawResponse, LnurlAuthResponse)):
|
||||
if isinstance(res, LnurlPayResponse | LnurlWithdrawResponse | LnurlAuthResponse):
|
||||
check_callback_url(res.callback)
|
||||
return res
|
||||
|
||||
@@ -122,8 +118,8 @@ async def api_payments_pay_lnurl(
|
||||
extra: dict[str, Any] = {}
|
||||
if res2.disposable is False:
|
||||
extra["stored"] = True
|
||||
if res2.success_action:
|
||||
extra["success_action"] = res2.success_action.json()
|
||||
if res2.successAction:
|
||||
extra["success_action"] = res2.successAction.json()
|
||||
if data.comment:
|
||||
extra["comment"] = data.comment
|
||||
if data.unit and data.unit != "sat":
|
||||
@@ -138,38 +134,3 @@ async def api_payments_pay_lnurl(
|
||||
)
|
||||
|
||||
return payment
|
||||
|
||||
|
||||
@lnurl_router.post(
|
||||
"/api/v1/payments/{payment_request}/pay-with-nfc", status_code=HTTPStatus.OK
|
||||
)
|
||||
async def api_payment_pay_with_nfc(
|
||||
payment_request: str,
|
||||
lnurl_data: CreateLnurlWithdraw,
|
||||
) -> LnurlErrorResponse | LnurlSuccessResponse:
|
||||
if not lnurl_data.lnurl_w.lud17:
|
||||
return LnurlErrorResponse(reason="LNURL-withdraw lud17 not provided.")
|
||||
try:
|
||||
url = lnurl_data.lnurl_w.lud17
|
||||
res = await lnurl_handle(url, user_agent=settings.user_agent, timeout=10)
|
||||
except (LnurlResponseException, Exception) as exc:
|
||||
return LnurlErrorResponse(reason=str(exc))
|
||||
|
||||
if not isinstance(res, LnurlWithdrawResponse):
|
||||
return LnurlErrorResponse(reason="Invalid LNURL-withdraw response.")
|
||||
try:
|
||||
check_callback_url(res.callback)
|
||||
except ValueError as exc:
|
||||
return LnurlErrorResponse(reason=f"Invalid callback URL: {exc!s}")
|
||||
|
||||
try:
|
||||
res2 = await lnurl_withdraw(
|
||||
res, payment_request, user_agent=settings.user_agent, timeout=10
|
||||
)
|
||||
except (LnurlResponseException, Exception) as exc:
|
||||
logger.warning(exc)
|
||||
return LnurlErrorResponse(reason=str(exc))
|
||||
if not isinstance(res2, (LnurlSuccessResponse, LnurlErrorResponse)):
|
||||
return LnurlErrorResponse(reason="Invalid LNURL-withdraw response.")
|
||||
|
||||
return res2
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from http import HTTPStatus
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException
|
||||
@@ -89,14 +88,14 @@ async def api_get_public_info(node: Node = Depends(require_node)) -> PublicNodeI
|
||||
@node_router.get("/info")
|
||||
async def api_get_info(
|
||||
node: Node = Depends(require_node),
|
||||
) -> Optional[NodeInfoResponse]:
|
||||
) -> NodeInfoResponse | None:
|
||||
return await node.get_info()
|
||||
|
||||
|
||||
@node_router.get("/channels")
|
||||
async def api_get_channels(
|
||||
node: Node = Depends(require_node),
|
||||
) -> Optional[list[NodeChannel]]:
|
||||
) -> list[NodeChannel] | None:
|
||||
return await node.get_channels()
|
||||
|
||||
|
||||
@@ -104,7 +103,7 @@ async def api_get_channels(
|
||||
async def api_get_channel(
|
||||
channel_id: str,
|
||||
node: Node = Depends(require_node),
|
||||
) -> Optional[NodeChannel]:
|
||||
) -> NodeChannel | None:
|
||||
return await node.get_channel(channel_id)
|
||||
|
||||
|
||||
@@ -113,20 +112,20 @@ async def api_create_channel(
|
||||
node: Node = Depends(require_node),
|
||||
peer_id: str = Body(),
|
||||
funding_amount: int = Body(),
|
||||
push_amount: Optional[int] = Body(None),
|
||||
fee_rate: Optional[int] = Body(None),
|
||||
push_amount: int | None = Body(None),
|
||||
fee_rate: int | None = Body(None),
|
||||
):
|
||||
return await node.open_channel(peer_id, funding_amount, push_amount, fee_rate)
|
||||
|
||||
|
||||
@super_node_router.delete("/channels")
|
||||
async def api_delete_channel(
|
||||
short_id: Optional[str],
|
||||
funding_txid: Optional[str],
|
||||
output_index: Optional[int],
|
||||
short_id: str | None,
|
||||
funding_txid: str | None,
|
||||
output_index: int | None,
|
||||
force: bool = False,
|
||||
node: Node = Depends(require_node),
|
||||
) -> Optional[list[NodeChannel]]:
|
||||
) -> list[NodeChannel] | None:
|
||||
return await node.close_channel(
|
||||
short_id,
|
||||
(
|
||||
@@ -152,7 +151,7 @@ async def api_set_channel_fees(
|
||||
async def api_get_payments(
|
||||
node: Node = Depends(require_node),
|
||||
filters: Filters = Depends(parse_filters(NodePaymentsFilters)),
|
||||
) -> Optional[Page[NodePayment]]:
|
||||
) -> Page[NodePayment] | None:
|
||||
if not settings.lnbits_node_ui_transactions:
|
||||
raise HTTPException(
|
||||
HTTP_503_SERVICE_UNAVAILABLE,
|
||||
@@ -165,7 +164,7 @@ async def api_get_payments(
|
||||
async def api_get_invoices(
|
||||
node: Node = Depends(require_node),
|
||||
filters: Filters = Depends(parse_filters(NodeInvoiceFilters)),
|
||||
) -> Optional[Page[NodeInvoice]]:
|
||||
) -> Page[NodeInvoice] | None:
|
||||
if not settings.lnbits_node_ui_transactions:
|
||||
raise HTTPException(
|
||||
HTTP_503_SERVICE_UNAVAILABLE,
|
||||
@@ -192,25 +191,25 @@ async def api_disconnect_peer(peer_id: str, node: Node = Depends(require_node)):
|
||||
|
||||
|
||||
class NodeRank(BaseModel):
|
||||
capacity: Optional[int]
|
||||
channelcount: Optional[int]
|
||||
age: Optional[int]
|
||||
growth: Optional[int]
|
||||
availability: Optional[int]
|
||||
capacity: int | None
|
||||
channelcount: int | None
|
||||
age: int | None
|
||||
growth: int | None
|
||||
availability: int | None
|
||||
|
||||
|
||||
# Same for public and private api
|
||||
@node_router.get(
|
||||
"/rank",
|
||||
description="Retrieve node ranks from https://1ml.com",
|
||||
response_model=Optional[NodeRank],
|
||||
response_model=NodeRank | None,
|
||||
)
|
||||
@public_node_router.get(
|
||||
"/rank",
|
||||
description="Retrieve node ranks from https://1ml.com",
|
||||
response_model=Optional[NodeRank],
|
||||
response_model=NodeRank | None,
|
||||
)
|
||||
async def api_get_1ml_stats(node: Node = Depends(require_node)) -> Optional[NodeRank]:
|
||||
async def api_get_1ml_stats(node: Node = Depends(require_node)) -> NodeRank | None:
|
||||
node_id = await node.get_id()
|
||||
headers = {"User-Agent": settings.user_agent}
|
||||
async with httpx.AsyncClient(headers=headers) as client:
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from hashlib import sha256
|
||||
from http import HTTPStatus
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import (
|
||||
APIRouter,
|
||||
@@ -20,6 +19,7 @@ from lnbits.core.crud.payments import (
|
||||
from lnbits.core.models import (
|
||||
CancelInvoice,
|
||||
CreateInvoice,
|
||||
CreateLnurlWithdraw,
|
||||
DecodePayment,
|
||||
KeyType,
|
||||
Payment,
|
||||
@@ -30,6 +30,7 @@ from lnbits.core.models import (
|
||||
PaymentHistoryPoint,
|
||||
PaymentWalletStats,
|
||||
SettleInvoice,
|
||||
SimpleStatus,
|
||||
)
|
||||
from lnbits.core.models.users import User
|
||||
from lnbits.db import Filters, Page
|
||||
@@ -60,6 +61,7 @@ from ..services import (
|
||||
fee_reserve_total,
|
||||
get_payments_daily_stats,
|
||||
pay_invoice,
|
||||
perform_withdraw,
|
||||
settle_hold_invoice,
|
||||
update_pending_payment,
|
||||
update_pending_payments,
|
||||
@@ -275,7 +277,7 @@ async def api_payments_fee_reserve(invoice: str = Query("invoice")) -> JSONRespo
|
||||
|
||||
# TODO: refactor this route into a public and admin one
|
||||
@payment_router.get("/{payment_hash}")
|
||||
async def api_payment(payment_hash, x_api_key: Optional[str] = Header(None)):
|
||||
async def api_payment(payment_hash, x_api_key: str | None = Header(None)):
|
||||
# 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
|
||||
wallet = await get_wallet_for_key(x_api_key) if isinstance(x_api_key, str) else None
|
||||
@@ -360,3 +362,23 @@ async def api_payments_cancel(
|
||||
detail="Payment does not exist or does not belong to this wallet.",
|
||||
)
|
||||
return await cancel_hold_invoice(payment)
|
||||
|
||||
|
||||
@payment_router.post("/{payment_request}/pay-with-nfc")
|
||||
async def api_payment_pay_with_nfc(
|
||||
payment_request: str,
|
||||
lnurl_data: CreateLnurlWithdraw,
|
||||
) -> SimpleStatus:
|
||||
if not lnurl_data.lnurl_w.lud17:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail="LNURL-withdraw lud17 not provided.",
|
||||
)
|
||||
try:
|
||||
await perform_withdraw(lnurl_data.lnurl_w.lud17, payment_request)
|
||||
except Exception as exc:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST, detail=str(exc)
|
||||
) from exc
|
||||
|
||||
return SimpleStatus(success=True, message="Payment sent with NFC.")
|
||||
|
||||
@@ -2,7 +2,6 @@ import base64
|
||||
import json
|
||||
import time
|
||||
from http import HTTPStatus
|
||||
from typing import Optional
|
||||
from uuid import uuid4
|
||||
|
||||
import shortuuid
|
||||
@@ -48,6 +47,7 @@ from lnbits.helpers import (
|
||||
)
|
||||
from lnbits.settings import EditableSettings, settings
|
||||
from lnbits.utils.exchange_rates import allowed_currencies
|
||||
from lnbits.utils.nostr import normalize_public_key
|
||||
|
||||
users_router = APIRouter(
|
||||
prefix="/users/api/v1", dependencies=[Depends(check_admin)], tags=["Users"]
|
||||
@@ -95,6 +95,9 @@ async def api_create_user(data: CreateUser) -> CreateUser:
|
||||
data.extra = data.extra or UserExtra()
|
||||
data.extra.provider = data.extra.provider or "lnbits"
|
||||
|
||||
if data.pubkey:
|
||||
data.pubkey = normalize_public_key(data.pubkey)
|
||||
|
||||
account = Account(
|
||||
id=uuid4().hex,
|
||||
username=data.username,
|
||||
@@ -128,6 +131,9 @@ async def api_update_user(
|
||||
HTTPStatus.BAD_REQUEST, "Use 'reset password' functionality."
|
||||
)
|
||||
|
||||
if data.pubkey:
|
||||
data.pubkey = normalize_public_key(data.pubkey)
|
||||
|
||||
account = Account(
|
||||
id=user_id,
|
||||
username=data.username,
|
||||
@@ -223,7 +229,7 @@ async def api_users_get_user_wallet(user_id: str) -> list[Wallet]:
|
||||
|
||||
@users_router.post("/user/{user_id}/wallet", name="Create a new wallet for user")
|
||||
async def api_users_create_user_wallet(
|
||||
user_id: str, name: Optional[str] = Body(None), currency: Optional[str] = Body(None)
|
||||
user_id: str, name: str | None = Body(None), currency: str | None = Body(None)
|
||||
):
|
||||
if currency and currency not in allowed_currencies():
|
||||
raise ValueError(f"Currency '{currency}' not allowed.")
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from http import HTTPStatus
|
||||
from typing import Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import (
|
||||
@@ -110,11 +109,11 @@ async def api_put_stored_paylinks(
|
||||
|
||||
@wallet_router.patch("")
|
||||
async def api_update_wallet(
|
||||
name: Optional[str] = Body(None),
|
||||
icon: Optional[str] = Body(None),
|
||||
color: Optional[str] = Body(None),
|
||||
currency: Optional[str] = Body(None),
|
||||
pinned: Optional[bool] = Body(None),
|
||||
name: str | None = Body(None),
|
||||
icon: str | None = Body(None),
|
||||
color: str | None = Body(None),
|
||||
currency: str | None = Body(None),
|
||||
pinned: bool | None = Body(None),
|
||||
key_info: WalletTypeInfo = Depends(require_admin_key),
|
||||
) -> Wallet:
|
||||
wallet = await get_wallet(key_info.wallet.id)
|
||||
|
||||
@@ -1,33 +1,20 @@
|
||||
from fastapi import (
|
||||
APIRouter,
|
||||
WebSocket,
|
||||
WebSocketDisconnect,
|
||||
)
|
||||
from fastapi import APIRouter, WebSocket
|
||||
|
||||
from lnbits.settings import settings
|
||||
|
||||
from ..services import (
|
||||
websocket_manager,
|
||||
websocket_updater,
|
||||
)
|
||||
from ..services import websocket_manager
|
||||
|
||||
websocket_router = APIRouter(prefix="/api/v1/ws", tags=["Websocket"])
|
||||
|
||||
|
||||
@websocket_router.websocket("/{item_id}")
|
||||
async def websocket_connect(websocket: WebSocket, item_id: str):
|
||||
await websocket_manager.connect(websocket, item_id)
|
||||
try:
|
||||
while settings.lnbits_running:
|
||||
await websocket.receive_text()
|
||||
except WebSocketDisconnect:
|
||||
websocket_manager.disconnect(websocket)
|
||||
async def websocket_connect(websocket: WebSocket, item_id: str) -> None:
|
||||
conn = await websocket_manager.connect(item_id, websocket)
|
||||
await websocket_manager.listen(conn)
|
||||
|
||||
|
||||
@websocket_router.post("/{item_id}")
|
||||
async def websocket_update_post(item_id: str, data: str):
|
||||
try:
|
||||
await websocket_updater(item_id, data)
|
||||
await websocket_manager.send(item_id, data)
|
||||
return {"sent": True, "data": data}
|
||||
except Exception:
|
||||
return {"sent": False, "data": data}
|
||||
@@ -36,7 +23,7 @@ async def websocket_update_post(item_id: str, data: str):
|
||||
@websocket_router.get("/{item_id}/{data}")
|
||||
async def websocket_update_get(item_id: str, data: str):
|
||||
try:
|
||||
await websocket_updater(item_id, data)
|
||||
await websocket_manager.send(item_id, data)
|
||||
return {"sent": True, "data": data}
|
||||
except Exception:
|
||||
return {"sent": False, "data": data}
|
||||
|
||||
+21
-21
@@ -1,5 +1,5 @@
|
||||
from http import HTTPStatus
|
||||
from typing import Annotated, Literal, Optional, Union
|
||||
from typing import Annotated, Literal
|
||||
|
||||
import jwt
|
||||
from fastapi import Cookie, Depends, Query, Request, Security
|
||||
@@ -55,8 +55,8 @@ api_key_query = APIKeyQuery(
|
||||
class KeyChecker(SecurityBase):
|
||||
def __init__(
|
||||
self,
|
||||
api_key: Optional[str] = None,
|
||||
expected_key_type: Optional[KeyType] = None,
|
||||
api_key: str | None = None,
|
||||
expected_key_type: KeyType | None = None,
|
||||
):
|
||||
self.auto_error: bool = True
|
||||
self.expected_key_type = expected_key_type
|
||||
@@ -137,17 +137,17 @@ async def require_invoice_key(
|
||||
|
||||
|
||||
async def check_access_token(
|
||||
header_access_token: Annotated[Union[str, None], Depends(oauth2_scheme)],
|
||||
cookie_access_token: Annotated[Union[str, None], Cookie()] = None,
|
||||
bearer_access_token: Annotated[Union[str, None], Depends(http_bearer)] = None,
|
||||
) -> Optional[str]:
|
||||
header_access_token: Annotated[str | None, Depends(oauth2_scheme)],
|
||||
cookie_access_token: Annotated[str | None, Cookie()] = None,
|
||||
bearer_access_token: Annotated[str | None, Depends(http_bearer)] = None,
|
||||
) -> str | None:
|
||||
return header_access_token or cookie_access_token or bearer_access_token
|
||||
|
||||
|
||||
async def check_user_exists(
|
||||
r: Request,
|
||||
access_token: Annotated[Optional[str], Depends(check_access_token)],
|
||||
usr: Optional[UUID4] = None,
|
||||
access_token: Annotated[str | None, Depends(check_access_token)],
|
||||
usr: UUID4 | None = None,
|
||||
) -> User:
|
||||
if access_token:
|
||||
account = await _get_account_from_token(access_token, r["path"], r["method"])
|
||||
@@ -176,9 +176,9 @@ async def check_user_exists(
|
||||
|
||||
async def optional_user_id(
|
||||
r: Request,
|
||||
access_token: Annotated[Optional[str], Depends(check_access_token)],
|
||||
usr: Optional[UUID4] = None,
|
||||
) -> Optional[str]:
|
||||
access_token: Annotated[str | None, Depends(check_access_token)],
|
||||
usr: UUID4 | None = None,
|
||||
) -> str | None:
|
||||
if usr and settings.is_auth_method_allowed(AuthMethods.user_id_only):
|
||||
return usr.hex
|
||||
if access_token:
|
||||
@@ -189,7 +189,7 @@ async def optional_user_id(
|
||||
|
||||
|
||||
async def access_token_payload(
|
||||
access_token: Annotated[Optional[str], Depends(check_access_token)],
|
||||
access_token: Annotated[str | None, Depends(check_access_token)],
|
||||
) -> AccessTokenPayload:
|
||||
if not access_token:
|
||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Missing access token.")
|
||||
@@ -231,11 +231,11 @@ def parse_filters(model: type[TFilterModel]):
|
||||
|
||||
def dependency(
|
||||
request: Request,
|
||||
limit: Optional[int] = None,
|
||||
offset: Optional[int] = None,
|
||||
sortby: Optional[str] = None,
|
||||
direction: Optional[Literal["asc", "desc"]] = None,
|
||||
search: Optional[str] = Query(None, description="Text based search"),
|
||||
limit: int | None = None,
|
||||
offset: int | None = None,
|
||||
sortby: str | None = None,
|
||||
direction: Literal["asc", "desc"] | None = None,
|
||||
search: str | None = Query(None, description="Text based search"),
|
||||
):
|
||||
params = request.query_params
|
||||
filters = []
|
||||
@@ -259,7 +259,7 @@ def parse_filters(model: type[TFilterModel]):
|
||||
|
||||
|
||||
async def check_user_extension_access(
|
||||
user_id: str, ext_id: str, conn: Optional[Connection] = None
|
||||
user_id: str, ext_id: str, conn: Connection | None = None
|
||||
) -> SimpleStatus:
|
||||
"""
|
||||
Check if the user has access to a particular extension.
|
||||
@@ -292,7 +292,7 @@ async def _check_user_extension_access(user_id: str, path: str):
|
||||
|
||||
async def _get_account_from_token(
|
||||
access_token: str, path: str, method: str
|
||||
) -> Optional[Account]:
|
||||
) -> Account | None:
|
||||
try:
|
||||
payload: dict = jwt.decode(access_token, settings.auth_secret_key, ["HS256"])
|
||||
return await _get_account_from_jwt_payload(
|
||||
@@ -310,7 +310,7 @@ async def _get_account_from_token(
|
||||
|
||||
async def _get_account_from_jwt_payload(
|
||||
payload: AccessTokenPayload, path: str, method: str
|
||||
) -> Optional[Account]:
|
||||
) -> Account | None:
|
||||
account = None
|
||||
if payload.sub:
|
||||
account = await get_account_by_username(payload.sub)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import sys
|
||||
import traceback
|
||||
from http import HTTPStatus
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Request
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
@@ -30,7 +29,7 @@ class UnsupportedError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def render_html_error(request: Request, exc: Exception) -> Optional[Response]:
|
||||
def render_html_error(request: Request, exc: Exception) -> Response | None:
|
||||
# Only the browser sends "text/html" request
|
||||
# not fail proof, but everything else get's a JSON response
|
||||
|
||||
|
||||
+2
-1
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import AsyncGenerator, Coroutine
|
||||
from typing import TYPE_CHECKING, NamedTuple
|
||||
from typing import TYPE_CHECKING, Any, NamedTuple
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
@@ -106,6 +106,7 @@ class FiatProvider(ABC):
|
||||
payment_hash: str,
|
||||
currency: str,
|
||||
memo: str | None = None,
|
||||
extra: dict[str, Any] | None = None,
|
||||
**kwargs,
|
||||
) -> Coroutine[None, None, FiatInvoiceResponse]:
|
||||
pass
|
||||
|
||||
+225
-65
@@ -2,11 +2,12 @@ import asyncio
|
||||
import json
|
||||
from collections.abc import AsyncGenerator
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional
|
||||
from typing import Any, Literal
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel, Field, ValidationError
|
||||
|
||||
from lnbits.helpers import normalize_endpoint
|
||||
from lnbits.settings import settings
|
||||
@@ -22,6 +23,34 @@ from .base import (
|
||||
FiatStatusResponse,
|
||||
)
|
||||
|
||||
FiatMethod = Literal["checkout", "terminal"]
|
||||
|
||||
|
||||
class StripeTerminalOptions(BaseModel):
|
||||
class Config:
|
||||
extra = "ignore"
|
||||
|
||||
capture_method: Literal["automatic", "manual"] = "automatic"
|
||||
metadata: dict[str, str] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class StripeCheckoutOptions(BaseModel):
|
||||
class Config:
|
||||
extra = "ignore"
|
||||
|
||||
success_url: str | None = None
|
||||
metadata: dict[str, str] = Field(default_factory=dict)
|
||||
line_item_name: str | None = None
|
||||
|
||||
|
||||
class StripeCreateInvoiceOptions(BaseModel):
|
||||
class Config:
|
||||
extra = "ignore"
|
||||
|
||||
fiat_method: FiatMethod = "checkout"
|
||||
terminal: StripeTerminalOptions | None = None
|
||||
checkout: StripeCheckoutOptions | None = None
|
||||
|
||||
|
||||
class StripeWallet(FiatProvider):
|
||||
"""https://docs.stripe.com/api"""
|
||||
@@ -31,9 +60,9 @@ class StripeWallet(FiatProvider):
|
||||
self._settings_fields = self._settings_connection_fields()
|
||||
if not settings.stripe_api_endpoint:
|
||||
raise ValueError("Cannot initialize StripeWallet: missing endpoint.")
|
||||
|
||||
if not settings.stripe_api_secret_key:
|
||||
raise ValueError("Cannot initialize StripeWallet: missing API secret key.")
|
||||
|
||||
self.endpoint = normalize_endpoint(settings.stripe_api_endpoint)
|
||||
self.headers = {
|
||||
"Authorization": f"Bearer {settings.stripe_api_secret_key}",
|
||||
@@ -49,7 +78,7 @@ class StripeWallet(FiatProvider):
|
||||
logger.warning(f"Error closing stripe wallet connection: {e}")
|
||||
|
||||
async def status(
|
||||
self, only_check_settings: Optional[bool] = False
|
||||
self, only_check_settings: bool | None = False
|
||||
) -> FiatStatusResponse:
|
||||
if only_check_settings:
|
||||
if self._settings_fields != self._settings_connection_fields():
|
||||
@@ -61,8 +90,10 @@ class StripeWallet(FiatProvider):
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
|
||||
available_balance = data.get("available", [{}])[0].get("amount", 0)
|
||||
# pending_balance = data.get("pending", {}).get("amount", 0)
|
||||
available = data.get("available") or []
|
||||
available_balance = 0
|
||||
if available and isinstance(available, list):
|
||||
available_balance = int(available[0].get("amount", 0))
|
||||
|
||||
return FiatStatusResponse(balance=available_balance)
|
||||
except json.JSONDecodeError:
|
||||
@@ -76,82 +107,48 @@ class StripeWallet(FiatProvider):
|
||||
amount: float,
|
||||
payment_hash: str,
|
||||
currency: str,
|
||||
memo: Optional[str] = None,
|
||||
memo: str | None = None,
|
||||
extra: dict[str, Any] | None = None,
|
||||
**kwargs,
|
||||
) -> FiatInvoiceResponse:
|
||||
amount_cents = int(amount * 100)
|
||||
form_data = [
|
||||
("mode", "payment"),
|
||||
(
|
||||
"success_url",
|
||||
settings.stripe_payment_success_url or "https://lnbits.com",
|
||||
),
|
||||
("metadata[payment_hash]", payment_hash),
|
||||
("line_items[0][price_data][currency]", currency.lower()),
|
||||
("line_items[0][price_data][product_data][name]", memo or "LNbits Invoice"),
|
||||
("line_items[0][price_data][unit_amount]", amount_cents),
|
||||
("line_items[0][quantity]", "1"),
|
||||
]
|
||||
encoded_data = urlencode(form_data)
|
||||
opts = self._parse_create_opts(extra or {})
|
||||
if not opts:
|
||||
return FiatInvoiceResponse(ok=False, error_message="Invalid Stripe options")
|
||||
|
||||
try:
|
||||
headers = self.headers.copy()
|
||||
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
r = await self.client.post(
|
||||
url="/v1/checkout/sessions", headers=headers, content=encoded_data
|
||||
if opts.fiat_method == "checkout":
|
||||
return await self._create_checkout_invoice(
|
||||
amount_cents, currency, payment_hash, memo, opts
|
||||
)
|
||||
if opts.fiat_method == "terminal":
|
||||
return await self._create_terminal_invoice(
|
||||
amount_cents, currency, payment_hash, opts
|
||||
)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
|
||||
session_id = data.get("id")
|
||||
if not session_id:
|
||||
return FiatInvoiceResponse(
|
||||
ok=False, error_message="Server error: 'missing session id'"
|
||||
)
|
||||
payment_request = data.get("url")
|
||||
if not payment_request:
|
||||
return FiatInvoiceResponse(
|
||||
ok=False, error_message="Server error: 'missing payment URL'"
|
||||
)
|
||||
|
||||
return FiatInvoiceResponse(
|
||||
ok=True, checking_id=session_id, payment_request=payment_request
|
||||
)
|
||||
except json.JSONDecodeError:
|
||||
return FiatInvoiceResponse(
|
||||
ok=False, error_message="Server error: 'invalid json response'"
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
return FiatInvoiceResponse(
|
||||
ok=False, error_message=f"Unable to connect to {self.endpoint}."
|
||||
)
|
||||
return FiatInvoiceResponse(
|
||||
ok=False, error_message=f"Unsupported fiat_method: {opts.fiat_method}"
|
||||
)
|
||||
|
||||
async def pay_invoice(self, payment_request: str) -> FiatPaymentResponse:
|
||||
raise NotImplementedError("Stripe does not support paying invoices directly.")
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> FiatPaymentStatus:
|
||||
try:
|
||||
r = await self.client.get(
|
||||
url=f"/v1/checkout/sessions/{checking_id}",
|
||||
)
|
||||
r.raise_for_status()
|
||||
stripe_id = self._normalize_stripe_id(checking_id)
|
||||
|
||||
data = r.json()
|
||||
payment_status = data.get("payment_status")
|
||||
if not payment_status:
|
||||
return FiatPaymentPendingStatus()
|
||||
if payment_status == "paid":
|
||||
# todo: handle fee
|
||||
return FiatPaymentSuccessStatus()
|
||||
if stripe_id.startswith("cs_"):
|
||||
r = await self.client.get(f"/v1/checkout/sessions/{stripe_id}")
|
||||
r.raise_for_status()
|
||||
return self._status_from_checkout_session(r.json())
|
||||
|
||||
expires_at = data.get("expires_at")
|
||||
_24_hours_ago = datetime.now(timezone.utc) - timedelta(hours=24)
|
||||
if expires_at and expires_at < _24_hours_ago.timestamp():
|
||||
# be defensive: add a 24 hour buffer
|
||||
return FiatPaymentFailedStatus()
|
||||
if stripe_id.startswith("pi_"):
|
||||
r = await self.client.get(f"/v1/payment_intents/{stripe_id}")
|
||||
r.raise_for_status()
|
||||
return self._status_from_payment_intent(r.json())
|
||||
|
||||
logger.debug(f"Unknown Stripe id prefix: {checking_id}")
|
||||
return FiatPaymentPendingStatus()
|
||||
|
||||
except Exception as exc:
|
||||
logger.debug(f"Error getting invoice status: {exc}")
|
||||
return FiatPaymentPendingStatus()
|
||||
@@ -168,6 +165,169 @@ class StripeWallet(FiatProvider):
|
||||
value = await mock_queue.get()
|
||||
yield value
|
||||
|
||||
async def create_terminal_connection_token(self) -> dict:
|
||||
r = await self.client.post("/v1/terminal/connection_tokens")
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
async def _create_checkout_invoice(
|
||||
self,
|
||||
amount_cents: int,
|
||||
currency: str,
|
||||
payment_hash: str,
|
||||
memo: str | None,
|
||||
opts: StripeCreateInvoiceOptions,
|
||||
) -> FiatInvoiceResponse:
|
||||
co = opts.checkout or StripeCheckoutOptions()
|
||||
success_url = (
|
||||
co.success_url
|
||||
or settings.stripe_payment_success_url
|
||||
or "https://lnbits.com"
|
||||
)
|
||||
line_item_name = co.line_item_name or memo or "LNbits Invoice"
|
||||
|
||||
form_data: list[tuple[str, str]] = [
|
||||
("mode", "payment"),
|
||||
("success_url", success_url),
|
||||
("metadata[payment_hash]", payment_hash),
|
||||
("line_items[0][price_data][currency]", currency.lower()),
|
||||
("line_items[0][price_data][product_data][name]", line_item_name),
|
||||
("line_items[0][price_data][unit_amount]", str(amount_cents)),
|
||||
("line_items[0][quantity]", "1"),
|
||||
]
|
||||
form_data += self._encode_metadata("metadata", co.metadata)
|
||||
|
||||
try:
|
||||
r = await self.client.post(
|
||||
"/v1/checkout/sessions",
|
||||
headers=self._build_headers_form(),
|
||||
content=urlencode(form_data),
|
||||
)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
session_id, url = data.get("id"), data.get("url")
|
||||
if not session_id or not url:
|
||||
return FiatInvoiceResponse(
|
||||
ok=False, error_message="Server error: missing id or url"
|
||||
)
|
||||
return FiatInvoiceResponse(
|
||||
ok=True, checking_id=session_id, payment_request=url
|
||||
)
|
||||
except json.JSONDecodeError:
|
||||
return FiatInvoiceResponse(
|
||||
ok=False, error_message="Server error: invalid json response"
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
return FiatInvoiceResponse(
|
||||
ok=False, error_message=f"Unable to connect to {self.endpoint}."
|
||||
)
|
||||
|
||||
async def _create_terminal_invoice(
|
||||
self,
|
||||
amount_cents: int,
|
||||
currency: str,
|
||||
payment_hash: str,
|
||||
opts: StripeCreateInvoiceOptions,
|
||||
) -> FiatInvoiceResponse:
|
||||
term = opts.terminal or StripeTerminalOptions()
|
||||
data: dict[str, str] = {
|
||||
"amount": str(amount_cents),
|
||||
"currency": currency.lower(),
|
||||
"payment_method_types[]": "card_present",
|
||||
"capture_method": term.capture_method,
|
||||
"metadata[payment_hash]": payment_hash,
|
||||
"metadata[source]": "lnbits",
|
||||
}
|
||||
for k, v in (term.metadata or {}).items():
|
||||
data[f"metadata[{k}]"] = str(v)
|
||||
|
||||
try:
|
||||
r = await self.client.post("/v1/payment_intents", data=data)
|
||||
r.raise_for_status()
|
||||
pi = r.json()
|
||||
pi_id, client_secret = pi.get("id"), pi.get("client_secret")
|
||||
if not pi_id or not client_secret:
|
||||
return FiatInvoiceResponse(
|
||||
ok=False,
|
||||
error_message="Error: missing PaymentIntent or client_secret",
|
||||
)
|
||||
return FiatInvoiceResponse(
|
||||
ok=True, checking_id=pi_id, payment_request=client_secret
|
||||
)
|
||||
except json.JSONDecodeError:
|
||||
return FiatInvoiceResponse(
|
||||
ok=False, error_message="Error: invalid json response"
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
return FiatInvoiceResponse(
|
||||
ok=False, error_message=f"Unable to connect to {self.endpoint}."
|
||||
)
|
||||
|
||||
def _normalize_stripe_id(self, checking_id: str) -> str:
|
||||
"""Remove our internal prefix so Stripe sees a real id."""
|
||||
return (
|
||||
checking_id.replace("fiat_stripe_", "", 1)
|
||||
if checking_id.startswith("fiat_stripe_")
|
||||
else checking_id
|
||||
)
|
||||
|
||||
def _status_from_checkout_session(self, data: dict) -> FiatPaymentStatus:
|
||||
"""Map a Checkout Session to LNbits fiat status."""
|
||||
if data.get("payment_status") == "paid":
|
||||
return FiatPaymentSuccessStatus()
|
||||
|
||||
# Consider an expired session a fail (existing 24h rule).
|
||||
expires_at = data.get("expires_at")
|
||||
_24h_ago = datetime.now(timezone.utc) - timedelta(hours=24)
|
||||
if expires_at and float(expires_at) < _24h_ago.timestamp():
|
||||
return FiatPaymentFailedStatus()
|
||||
|
||||
return FiatPaymentPendingStatus()
|
||||
|
||||
def _status_from_payment_intent(self, pi: dict) -> FiatPaymentStatus:
|
||||
"""Map a PaymentIntent to LNbits fiat status (card_present friendly)."""
|
||||
status = pi.get("status")
|
||||
|
||||
if status == "succeeded":
|
||||
return FiatPaymentSuccessStatus()
|
||||
|
||||
if status in ("canceled", "payment_failed"):
|
||||
return FiatPaymentFailedStatus()
|
||||
|
||||
if status == "requires_payment_method":
|
||||
if pi.get("last_payment_error"):
|
||||
return FiatPaymentFailedStatus()
|
||||
|
||||
now_ts = datetime.now(timezone.utc).timestamp()
|
||||
created_ts = float(pi.get("created") or now_ts)
|
||||
is_stale = (now_ts - created_ts) > 300
|
||||
if is_stale:
|
||||
return FiatPaymentFailedStatus()
|
||||
|
||||
return FiatPaymentPendingStatus()
|
||||
|
||||
def _build_headers_form(self) -> dict[str, str]:
|
||||
return {**self.headers, "Content-Type": "application/x-www-form-urlencoded"}
|
||||
|
||||
def _encode_metadata(
|
||||
self, prefix: str, md: dict[str, Any]
|
||||
) -> list[tuple[str, str]]:
|
||||
out: list[tuple[str, str]] = []
|
||||
for k, v in (md or {}).items():
|
||||
out.append((f"{prefix}[{k}]", str(v)))
|
||||
return out
|
||||
|
||||
def _parse_create_opts(
|
||||
self, raw_opts: dict[str, Any]
|
||||
) -> StripeCreateInvoiceOptions | None:
|
||||
try:
|
||||
return StripeCreateInvoiceOptions.parse_obj(raw_opts)
|
||||
except ValidationError as e:
|
||||
logger.warning(f"Invalid Stripe options: {e}")
|
||||
return None
|
||||
|
||||
def _settings_connection_fields(self) -> str:
|
||||
return "-".join(
|
||||
[str(settings.stripe_api_endpoint), str(settings.stripe_api_secret_key)]
|
||||
|
||||
+9
-13
@@ -3,7 +3,7 @@ import json
|
||||
import re
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
from urllib import request
|
||||
from urllib.parse import urlparse
|
||||
|
||||
@@ -39,7 +39,7 @@ def urlsafe_short_hash() -> str:
|
||||
return shortuuid.uuid()
|
||||
|
||||
|
||||
def url_for(endpoint: str, external: Optional[bool] = False, **params: Any) -> str:
|
||||
def url_for(endpoint: str, external: bool | None = False, **params: Any) -> str:
|
||||
base = f"http://{settings.host}:{settings.port}" if external else ""
|
||||
url_params = "?"
|
||||
for key, value in params.items():
|
||||
@@ -52,7 +52,7 @@ def static_url_for(static: str, path: str) -> str:
|
||||
return f"/{static}/{path}?v={settings.server_startup_time}"
|
||||
|
||||
|
||||
def template_renderer(additional_folders: Optional[list] = None) -> Jinja2Templates:
|
||||
def template_renderer(additional_folders: list | None = None) -> Jinja2Templates:
|
||||
folders = ["lnbits/templates", "lnbits/core/templates"]
|
||||
if additional_folders:
|
||||
additional_folders += [
|
||||
@@ -211,7 +211,7 @@ def is_valid_pubkey(pubkey: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def create_access_token(data: dict, token_expire_minutes: Optional[int] = None) -> str:
|
||||
def create_access_token(data: dict, token_expire_minutes: int | None = None) -> str:
|
||||
minutes = token_expire_minutes or settings.auth_token_expire_minutes
|
||||
expire = datetime.now(timezone.utc) + timedelta(minutes=minutes)
|
||||
to_encode = {k: v for k, v in data.items() if v is not None}
|
||||
@@ -219,9 +219,7 @@ def create_access_token(data: dict, token_expire_minutes: Optional[int] = None)
|
||||
return jwt.encode(to_encode, settings.auth_secret_key, "HS256")
|
||||
|
||||
|
||||
def encrypt_internal_message(
|
||||
m: Optional[str] = None, urlsafe: bool = False
|
||||
) -> Optional[str]:
|
||||
def encrypt_internal_message(m: str | None = None, urlsafe: bool = False) -> str | None:
|
||||
"""
|
||||
Encrypt message with the internal secret key
|
||||
|
||||
@@ -234,9 +232,7 @@ def encrypt_internal_message(
|
||||
return AESCipher(key=settings.auth_secret_key).encrypt(m.encode(), urlsafe=urlsafe)
|
||||
|
||||
|
||||
def decrypt_internal_message(
|
||||
m: Optional[str] = None, urlsafe: bool = False
|
||||
) -> Optional[str]:
|
||||
def decrypt_internal_message(m: str | None = None, urlsafe: bool = False) -> str | None:
|
||||
"""
|
||||
Decrypt message with the internal secret key
|
||||
|
||||
@@ -249,7 +245,7 @@ def decrypt_internal_message(
|
||||
return AESCipher(key=settings.auth_secret_key).decrypt(m, urlsafe=urlsafe)
|
||||
|
||||
|
||||
def filter_dict_keys(data: dict, filter_keys: Optional[list[str]]) -> dict:
|
||||
def filter_dict_keys(data: dict, filter_keys: list[str] | None) -> dict:
|
||||
if not filter_keys:
|
||||
# return shallow clone of the dict even if there are no filters
|
||||
return {**data}
|
||||
@@ -270,7 +266,7 @@ def version_parse(v: str):
|
||||
|
||||
|
||||
def is_lnbits_version_ok(
|
||||
min_lnbits_version: Optional[str], max_lnbits_version: Optional[str]
|
||||
min_lnbits_version: str | None, max_lnbits_version: str | None
|
||||
) -> bool:
|
||||
if min_lnbits_version and (
|
||||
version_parse(min_lnbits_version) > version_parse(settings.version)
|
||||
@@ -347,7 +343,7 @@ def path_segments(path: str) -> list[str]:
|
||||
return segments[0:]
|
||||
|
||||
|
||||
def normalize_path(path: Optional[str]) -> str:
|
||||
def normalize_path(path: str | None) -> str:
|
||||
path = path or ""
|
||||
return "/" + "/".join(path_segments(path))
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import asyncio
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from http import HTTPStatus
|
||||
from typing import Any, Optional, Union
|
||||
from typing import Any
|
||||
|
||||
from fastapi import FastAPI, Request, Response
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
||||
@@ -62,7 +62,7 @@ class InstalledExtensionMiddleware:
|
||||
|
||||
def _response_by_accepted_type(
|
||||
self, scope: Scope, headers: list[Any], msg: str, status_code: HTTPStatus
|
||||
) -> Union[HTMLResponse, JSONResponse]:
|
||||
) -> HTMLResponse | JSONResponse:
|
||||
"""
|
||||
Build an HTTP response containing the `msg` as HTTP body and the `status_code`
|
||||
as HTTP code. If the `accept` HTTP header is present int the request and
|
||||
@@ -127,7 +127,7 @@ class AuditMiddleware(BaseHTTPMiddleware):
|
||||
async def dispatch(self, request: Request, call_next) -> Response:
|
||||
start_time = datetime.now(timezone.utc)
|
||||
request_details = await self._request_details(request)
|
||||
response: Optional[Response] = None
|
||||
response: Response | None = None
|
||||
|
||||
try:
|
||||
response = await call_next(request)
|
||||
@@ -140,13 +140,13 @@ class AuditMiddleware(BaseHTTPMiddleware):
|
||||
async def _log_audit(
|
||||
self,
|
||||
request: Request,
|
||||
response: Optional[Response],
|
||||
response: Response | None,
|
||||
duration: float,
|
||||
request_details: Optional[str],
|
||||
request_details: str | None,
|
||||
):
|
||||
try:
|
||||
http_method = request.scope.get("method", None)
|
||||
path: Optional[str] = getattr(request.scope.get("route", {}), "path", None)
|
||||
path: str | None = getattr(request.scope.get("route", {}), "path", None)
|
||||
response_code = str(response.status_code) if response else None
|
||||
if not settings.audit_http_request(http_method, path, response_code):
|
||||
return None
|
||||
@@ -178,7 +178,7 @@ class AuditMiddleware(BaseHTTPMiddleware):
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
|
||||
async def _request_details(self, request: Request) -> Optional[str]:
|
||||
async def _request_details(self, request: Request) -> str | None:
|
||||
if not settings.audit_http_request_details():
|
||||
return None
|
||||
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ def main(
|
||||
ssl_certfile: str,
|
||||
reload: bool,
|
||||
):
|
||||
"""Launched with `poetry run lnbits` at root level"""
|
||||
"""Launched with `uv run lnbits` at root level"""
|
||||
|
||||
# create data dir if it does not exist
|
||||
Path(settings.lnbits_data_folder).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+3
-3
File diff suppressed because one or more lines are too long
@@ -115,6 +115,7 @@ window.localisation.en = {
|
||||
payment_check: 'Check payment',
|
||||
not_enough_funds: 'Not enough funds!',
|
||||
search_by_tag_memo_amount: 'Search by tag, memo, amount',
|
||||
search: 'Search',
|
||||
invoice_waiting: 'Invoice waiting to be paid',
|
||||
payment_received: 'Payment Received',
|
||||
payment_sent: 'Payment Sent',
|
||||
|
||||
@@ -24,7 +24,11 @@ window.app.component('lnbits-qrcode-lnurl', {
|
||||
const bech32 = NostrTools.nip19.encodeBytes('lnurl', bytes)
|
||||
this.lnurl = `lightning:${bech32.toUpperCase()}`
|
||||
} else if (this.tab == 'lud17') {
|
||||
this.lnurl = this.url.replace('https://', this.prefix + '://')
|
||||
if (this.url.startsWith('http://')) {
|
||||
this.lnurl = this.url.replace('http://', this.prefix + '://')
|
||||
} else {
|
||||
this.lnurl = this.url.replace('https://', this.prefix + '://')
|
||||
}
|
||||
}
|
||||
this.$emit('update:lnurl', this.lnurl)
|
||||
}
|
||||
|
||||
@@ -9,19 +9,123 @@ window.app.component('lnbits-qrcode', {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
options: Object
|
||||
nfc: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showButtons: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
href: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
margin: {
|
||||
type: Number,
|
||||
default: 3
|
||||
},
|
||||
maxWidth: {
|
||||
type: Number,
|
||||
default: 450
|
||||
},
|
||||
logo: {
|
||||
type: String,
|
||||
default: LNBITS_QR_LOGO
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
custom: {
|
||||
margin: 3,
|
||||
width: 350,
|
||||
size: 350,
|
||||
logo: LNBITS_QR_LOGO
|
||||
}
|
||||
nfcTagWriting: false,
|
||||
nfcSupported: typeof NDEFReader != 'undefined'
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.custom = {...this.custom, ...this.options}
|
||||
methods: {
|
||||
clickQrCode(event) {
|
||||
if (this.href === '') {
|
||||
this.copyText(this.value)
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return false
|
||||
}
|
||||
},
|
||||
async writeNfcTag() {
|
||||
try {
|
||||
if (!this.nfcSupported) {
|
||||
throw {
|
||||
toString: function () {
|
||||
return 'NFC not supported on this device or browser.'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ndef = new NDEFReader()
|
||||
|
||||
this.nfcTagWriting = true
|
||||
this.$q.notify({
|
||||
message: 'Tap your NFC tag to write the LNURL-withdraw link to it.'
|
||||
})
|
||||
|
||||
await ndef.write({
|
||||
records: [{recordType: 'url', data: this.value, lang: 'en'}]
|
||||
})
|
||||
|
||||
this.nfcTagWriting = false
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'NFC tag written successfully.'
|
||||
})
|
||||
} catch (error) {
|
||||
this.nfcTagWriting = false
|
||||
this.$q.notify({
|
||||
type: 'negative',
|
||||
message: error
|
||||
? error.toString()
|
||||
: 'An unexpected error has occurred.'
|
||||
})
|
||||
}
|
||||
},
|
||||
downloadSVG() {
|
||||
const filename = 'qrcode.svg'
|
||||
const svg = this.$refs.qrCode.$el
|
||||
if (!svg) {
|
||||
console.error('SVG element not found')
|
||||
return
|
||||
}
|
||||
|
||||
// Serialize the SVG content
|
||||
const serializer = new XMLSerializer()
|
||||
let source = serializer.serializeToString(svg)
|
||||
|
||||
// Add SVG namespace if not present
|
||||
if (!source.match(/^<svg[^>]+xmlns="http:\/\/www\.w3\.org\/2000\/svg"/)) {
|
||||
source = source.replace(
|
||||
/^<svg/,
|
||||
'<svg xmlns="http://www.w3.org/2000/svg"'
|
||||
)
|
||||
}
|
||||
|
||||
// Add XML declaration
|
||||
source = '<?xml version="1.0" standalone="no"?>\n' + source
|
||||
|
||||
// Convert to Blob and trigger download
|
||||
const blob = new Blob([source], {type: 'image/svg+xml;charset=utf-8'})
|
||||
const url = URL.createObjectURL(blob)
|
||||
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = filename
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$refs.qrCode.$el.style.maxWidth = this.maxWidth + 'px'
|
||||
this.$refs.qrCode.$el.setAttribute('width', '100%')
|
||||
this.$refs.qrCode.$el.removeAttribute('height')
|
||||
}
|
||||
})
|
||||
|
||||
@@ -154,7 +154,7 @@ function confettiStars() {
|
||||
setTimeout(shoot, 200)
|
||||
}
|
||||
!(function (t, e) {
|
||||
!(function t(e, n, a, i) {
|
||||
;(!(function t(e, n, a, i) {
|
||||
var o = !!(
|
||||
e.Worker &&
|
||||
e.Blob &&
|
||||
@@ -248,12 +248,12 @@ function confettiStars() {
|
||||
function e(e, n) {
|
||||
t.postMessage({options: e || {}, callback: n})
|
||||
}
|
||||
;(t.init = function (e) {
|
||||
;((t.init = function (e) {
|
||||
var n = e.transferControlToOffscreen()
|
||||
t.postMessage({canvas: n}, [n])
|
||||
}),
|
||||
(t.fire = function (n, a, i) {
|
||||
if (g) return e(n, null), g
|
||||
if (g) return (e(n, null), g)
|
||||
var o = Math.random().toString(36).slice(2)
|
||||
return (g = l(function (a) {
|
||||
function r(e) {
|
||||
@@ -264,15 +264,15 @@ function confettiStars() {
|
||||
i(),
|
||||
a())
|
||||
}
|
||||
t.addEventListener('message', r),
|
||||
;(t.addEventListener('message', r),
|
||||
e(n, o),
|
||||
(m[o] = r.bind(null, {data: {callback: o}}))
|
||||
(m[o] = r.bind(null, {data: {callback: o}})))
|
||||
}))
|
||||
}),
|
||||
(t.reset = function () {
|
||||
for (var e in (t.postMessage({reset: !0}), m))
|
||||
m[e](), delete m[e]
|
||||
})
|
||||
(m[e](), delete m[e])
|
||||
}))
|
||||
})(h)
|
||||
}
|
||||
return h
|
||||
@@ -328,12 +328,12 @@ function confettiStars() {
|
||||
)
|
||||
}
|
||||
function k(t) {
|
||||
;(t.width = document.documentElement.clientWidth),
|
||||
(t.height = document.documentElement.clientHeight)
|
||||
;((t.width = document.documentElement.clientWidth),
|
||||
(t.height = document.documentElement.clientHeight))
|
||||
}
|
||||
function I(t) {
|
||||
var e = t.getBoundingClientRect()
|
||||
;(t.width = e.width), (t.height = e.height)
|
||||
;((t.width = e.width), (t.height = e.height))
|
||||
}
|
||||
function T(t, e, n, o, r) {
|
||||
var c,
|
||||
@@ -342,10 +342,10 @@ function confettiStars() {
|
||||
d = t.getContext('2d'),
|
||||
f = l(function (e) {
|
||||
function l() {
|
||||
;(c = s = null), d.clearRect(0, 0, o.width, o.height), r(), e()
|
||||
;((c = s = null), d.clearRect(0, 0, o.width, o.height), r(), e())
|
||||
}
|
||||
;(c = b.frame(function e() {
|
||||
!a ||
|
||||
;((c = b.frame(function e() {
|
||||
;(!a ||
|
||||
(o.width === i.width && o.height === i.height) ||
|
||||
((o.width = t.width = i.width), (o.height = t.height = i.height)),
|
||||
o.width ||
|
||||
@@ -354,7 +354,7 @@ function confettiStars() {
|
||||
d.clearRect(0, 0, o.width, o.height),
|
||||
(u = u.filter(function (t) {
|
||||
return (function (t, e) {
|
||||
;(e.x += Math.cos(e.angle2D) * e.velocity + e.drift),
|
||||
;((e.x += Math.cos(e.angle2D) * e.velocity + e.drift),
|
||||
(e.y += Math.sin(e.angle2D) * e.velocity + e.gravity),
|
||||
(e.wobble += 0.1),
|
||||
(e.velocity *= e.decay),
|
||||
@@ -363,7 +363,7 @@ function confettiStars() {
|
||||
(e.tiltCos = Math.cos(e.tiltAngle)),
|
||||
(e.random = Math.random() + 5),
|
||||
(e.wobbleX = e.x + 10 * e.scalar * Math.cos(e.wobble)),
|
||||
(e.wobbleY = e.y + 10 * e.scalar * Math.sin(e.wobble))
|
||||
(e.wobbleY = e.y + 10 * e.scalar * Math.sin(e.wobble)))
|
||||
var n = e.tick++ / e.totalTicks,
|
||||
a = e.x + e.random * e.tiltCos,
|
||||
i = e.y + e.random * e.tiltSin,
|
||||
@@ -393,12 +393,12 @@ function confettiStars() {
|
||||
2 * Math.PI
|
||||
)
|
||||
: (function (t, e, n, a, i, o, r, l, c) {
|
||||
t.save(),
|
||||
;(t.save(),
|
||||
t.translate(e, n),
|
||||
t.rotate(o),
|
||||
t.scale(a, i),
|
||||
t.arc(0, 0, 1, r, l, c),
|
||||
t.restore()
|
||||
t.restore())
|
||||
})(
|
||||
t,
|
||||
e.x,
|
||||
@@ -420,18 +420,18 @@ function confettiStars() {
|
||||
})(d, t)
|
||||
})).length
|
||||
? (c = b.frame(e))
|
||||
: l()
|
||||
: l())
|
||||
})),
|
||||
(s = l)
|
||||
(s = l))
|
||||
})
|
||||
return {
|
||||
addFettis: function (t) {
|
||||
return (u = u.concat(t)), f
|
||||
return ((u = u.concat(t)), f)
|
||||
},
|
||||
canvas: t,
|
||||
promise: f,
|
||||
reset: function () {
|
||||
c && b.cancel(c), s && s()
|
||||
;(c && b.cancel(c), s && s())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -466,7 +466,7 @@ function confettiStars() {
|
||||
k = p(e, 'scalar'),
|
||||
I = (function (t) {
|
||||
var e = p(t, 'origin', Object)
|
||||
return (e.x = p(e, 'x', Number)), (e.y = p(e, 'y', Number)), e
|
||||
return ((e.x = p(e, 'x', Number)), (e.y = p(e, 'y', Number)), e)
|
||||
})(e),
|
||||
E = d,
|
||||
S = [],
|
||||
@@ -531,7 +531,7 @@ function confettiStars() {
|
||||
return l(function (t) {
|
||||
t()
|
||||
})
|
||||
i && a
|
||||
;(i && a
|
||||
? (t = a.canvas)
|
||||
: i &&
|
||||
!t &&
|
||||
@@ -547,7 +547,7 @@ function confettiStars() {
|
||||
)
|
||||
})(g)),
|
||||
document.body.appendChild(t)),
|
||||
r && !d && u(t)
|
||||
r && !d && u(t))
|
||||
var m = {width: t.width, height: t.height}
|
||||
function b() {
|
||||
if (s) {
|
||||
@@ -564,9 +564,9 @@ function confettiStars() {
|
||||
m.width = m.height = null
|
||||
}
|
||||
function v() {
|
||||
;(a = null),
|
||||
;((a = null),
|
||||
r && e.removeEventListener('resize', b),
|
||||
i && t && (document.body.removeChild(t), (t = null), (d = !1))
|
||||
i && t && (document.body.removeChild(t), (t = null), (d = !1)))
|
||||
}
|
||||
return (
|
||||
s && !d && s.init(t),
|
||||
@@ -578,12 +578,13 @@ function confettiStars() {
|
||||
}
|
||||
return (
|
||||
(g.reset = function () {
|
||||
s && s.reset(), a && a.reset()
|
||||
;(s && s.reset(), a && a.reset())
|
||||
}),
|
||||
g
|
||||
)
|
||||
}
|
||||
;(n.exports = E(null, {useWorker: !0, resize: !0})), (n.exports.create = E)
|
||||
;((n.exports = E(null, {useWorker: !0, resize: !0})),
|
||||
(n.exports.create = E))
|
||||
})(
|
||||
(function () {
|
||||
return void 0 !== t ? t : 'undefined' != typeof self ? self : this || {}
|
||||
@@ -591,5 +592,5 @@ function confettiStars() {
|
||||
e,
|
||||
!1
|
||||
),
|
||||
(t.confetti = e.exports)
|
||||
(t.confetti = e.exports))
|
||||
})(window, {})
|
||||
|
||||
@@ -45,7 +45,6 @@ window.WalletPageLogic = {
|
||||
payment_hash: null
|
||||
}
|
||||
},
|
||||
invoiceQrCode: '',
|
||||
disclaimerDialog: {
|
||||
show: false,
|
||||
location: window.location
|
||||
@@ -313,13 +312,6 @@ window.WalletPageLogic = {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Hack as rendering in dialog causes reactivity issues. Does speed up, as only rendering lnbits-qrcode once.
|
||||
this.$nextTick(() => {
|
||||
this.invoiceQrCode = document.getElementById(
|
||||
'hiddenQrCodeContainer'
|
||||
).innerHTML
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
@@ -583,6 +575,7 @@ window.WalletPageLogic = {
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
authLnurl() {
|
||||
const dismissAuthMsg = Quasar.Notify.create({
|
||||
|
||||
Vendored
+427
-305
File diff suppressed because it is too large
Load Diff
+2
-6
@@ -1,11 +1,7 @@
|
||||
import asyncio
|
||||
import traceback
|
||||
import uuid
|
||||
from collections.abc import Coroutine
|
||||
from typing import (
|
||||
Callable,
|
||||
Optional,
|
||||
)
|
||||
from collections.abc import Callable, Coroutine
|
||||
|
||||
from loguru import logger
|
||||
|
||||
@@ -84,7 +80,7 @@ invoice_listeners: dict[str, asyncio.Queue] = {}
|
||||
|
||||
# TODO: name should not be optional
|
||||
# some extensions still dont use a name, but they should
|
||||
def register_invoice_listener(send_chan: asyncio.Queue, name: Optional[str] = None):
|
||||
def register_invoice_listener(send_chan: asyncio.Queue, name: str | None = None):
|
||||
"""
|
||||
A method intended for extensions (and core/tasks.py) to call when they want to be
|
||||
notified about new invoice payments incoming. Will emit all incoming payments.
|
||||
|
||||
@@ -617,21 +617,65 @@
|
||||
</template>
|
||||
|
||||
<template id="lnbits-qrcode">
|
||||
<div class="qrcode__wrapper">
|
||||
<qrcode-vue
|
||||
:value="value"
|
||||
level="Q"
|
||||
render-as="svg"
|
||||
:margin="custom.margin"
|
||||
:size="custom.width"
|
||||
class="rounded-borders"
|
||||
></qrcode-vue>
|
||||
<img
|
||||
v-if="custom.logo"
|
||||
class="qrcode__image"
|
||||
:src="custom.logo"
|
||||
alt="qrcode icon"
|
||||
/>
|
||||
<div
|
||||
class="qrcode__outer"
|
||||
:style="`margin: 13px auto; max-width: ${maxWidth}px`"
|
||||
>
|
||||
<div ref="qrWrapper" class="qrcode__wrapper">
|
||||
<a
|
||||
:href="href"
|
||||
:title="href === '' ? value : href"
|
||||
@click="clickQrCode"
|
||||
class="no-link full-width"
|
||||
>
|
||||
<qrcode-vue
|
||||
ref="qrCode"
|
||||
:value="value"
|
||||
:margin="margin"
|
||||
:size="size"
|
||||
level="Q"
|
||||
render-as="svg"
|
||||
class="rounded-borders q-mb-sm"
|
||||
>
|
||||
<q-tooltip :model-value="href === '' ? value : href"></q-tooltip>
|
||||
</qrcode-vue>
|
||||
</a>
|
||||
<img
|
||||
:src="logo"
|
||||
class="qrcode__image"
|
||||
alt="qrcode icon"
|
||||
style="pointer-events: none"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="showButtons"
|
||||
class="qrcode__buttons row q-gutter-x-sm"
|
||||
style="justify-content: flex-end"
|
||||
>
|
||||
<q-btn
|
||||
v-if="nfc && nfcSupported"
|
||||
:disabled="nfcTagWriting"
|
||||
flat
|
||||
dense
|
||||
class="text-grey"
|
||||
icon="nfc"
|
||||
@click="writeNfcTag"
|
||||
>
|
||||
<q-tooltip>Write NFC Tag</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn flat dense class="text-grey" icon="download" @click="downloadSVG">
|
||||
<q-tooltip>Download SVG</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
class="text-grey"
|
||||
@click="copyText(value)"
|
||||
icon="content_copy"
|
||||
>
|
||||
<q-tooltip>Copy</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -640,17 +684,16 @@
|
||||
<q-tabs
|
||||
v-model="tab"
|
||||
dense
|
||||
class="text-grey q-mb-md"
|
||||
class="text-grey"
|
||||
active-color="primary"
|
||||
indicator-color="primary"
|
||||
align="justify"
|
||||
narrow-indicator
|
||||
inline-label
|
||||
>
|
||||
<q-tab name="bech32" icon="qr_code" label="bech32"></q-tab>
|
||||
<q-tab name="lud17" icon="link" label="url (lud17)"></q-tab>
|
||||
</q-tabs>
|
||||
<lnbits-qrcode :value="lnurl" class="rounded-borders"></lnbits-qrcode>
|
||||
<lnbits-qrcode :value="lnurl" nfc="true"></lnbits-qrcode>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1044,42 +1087,22 @@
|
||||
|
||||
<div
|
||||
v-if="props.row.isIn && props.row.isPending && props.row.bolt11"
|
||||
class="text-center q-my-lg"
|
||||
>
|
||||
<div v-if="props.row.extra.fiat_payment_request">
|
||||
<a
|
||||
<lnbits-qrcode
|
||||
:value="props.row.extra.fiat_payment_request"
|
||||
:href="props.row.extra.fiat_payment_request"
|
||||
target="_blank"
|
||||
>
|
||||
<lnbits-qrcode
|
||||
:value="props.row.extra.fiat_payment_request"
|
||||
></lnbits-qrcode>
|
||||
</a>
|
||||
:show-buttons="false"
|
||||
></lnbits-qrcode>
|
||||
</div>
|
||||
<div v-else>
|
||||
<a :href="'lightning:' + props.row.bolt11">
|
||||
<lnbits-qrcode
|
||||
:value="'lightning:' + props.row.bolt11.toUpperCase()"
|
||||
></lnbits-qrcode>
|
||||
</a>
|
||||
<lnbits-qrcode
|
||||
:value="'lightning:' + props.row.bolt11.toUpperCase()"
|
||||
:href="'lightning:' + props.row.bolt11"
|
||||
></lnbits-qrcode>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<div class="row q-gutter-x-sm">
|
||||
<q-btn
|
||||
v-if="
|
||||
props.row.isIn && props.row.isPending && props.row.bolt11
|
||||
"
|
||||
outline
|
||||
color="grey"
|
||||
@click="
|
||||
copyText(
|
||||
props.row.extra.fiat_payment_request || props.row.bolt11
|
||||
)
|
||||
"
|
||||
:label="$t('copy_invoice')"
|
||||
></q-btn>
|
||||
<div class="row q-mt-md">
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
@@ -1196,9 +1219,13 @@
|
||||
:endpoint="endpoint"
|
||||
>
|
||||
<template v-slot:actions>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||
>Close</q-btn
|
||||
>
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
:label="$t('close')"
|
||||
></q-btn>
|
||||
</template>
|
||||
</lnbits-extension-settings-form>
|
||||
</q-card>
|
||||
@@ -1282,10 +1309,10 @@
|
||||
<q-dialog v-model="showQRDialog">
|
||||
<q-card class="q-pa-md">
|
||||
<q-card-section>
|
||||
<lnbits-qrcode :value="qrValue" :size="200"></lnbits-qrcode>
|
||||
<lnbits-qrcode :value="qrValue"></lnbits-qrcode>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat label="Close" v-close-popup />
|
||||
<q-btn flat :label="$t('close')" v-close-popup />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from base64 import b64decode, b64encode, urlsafe_b64decode, urlsafe_b64encode
|
||||
from hashlib import md5, pbkdf2_hmac, sha256
|
||||
from typing import Union
|
||||
|
||||
from Cryptodome import Random
|
||||
from Cryptodome.Cipher import AES
|
||||
@@ -40,7 +39,7 @@ class AESCipher:
|
||||
AES.decrypt(encrypted, password).toString(Utf8);
|
||||
"""
|
||||
|
||||
def __init__(self, key: Union[bytes, str], block_size: int = 16):
|
||||
def __init__(self, key: bytes | str, block_size: int = 16):
|
||||
self.block_size = block_size
|
||||
if isinstance(key, bytes):
|
||||
self.key = key
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import asyncio
|
||||
import statistics
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
import jsonpath_ng.ext as jpx
|
||||
@@ -250,7 +249,7 @@ async def btc_rates(currency: str) -> list[tuple[str, float]]:
|
||||
|
||||
async def fetch_price(
|
||||
provider: ExchangeRateProvider,
|
||||
) -> Optional[tuple[str, float]]:
|
||||
) -> tuple[str, float] | None:
|
||||
if currency.lower() in provider.exclude_to:
|
||||
logger.warning(f"Provider {provider.name} does not support {currency}.")
|
||||
return None
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
from hashlib import sha256
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
from loguru import logger
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import base64
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
from typing import Union
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import secp256k1
|
||||
@@ -149,7 +148,7 @@ def sign_event(
|
||||
return event
|
||||
|
||||
|
||||
def json_dumps(data: Union[dict, list]) -> str:
|
||||
def json_dumps(data: dict | list) -> str:
|
||||
"""
|
||||
Converts a Python dictionary to a JSON string with compact encoding.
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
@@ -71,9 +70,9 @@ class AlbyWallet(Wallet):
|
||||
async def create_invoice(
|
||||
self,
|
||||
amount: int,
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
memo: str | None = None,
|
||||
description_hash: bytes | None = None,
|
||||
unhashed_description: bytes | None = None,
|
||||
**_,
|
||||
) -> InvoiceResponse:
|
||||
# https://api.getalby.com/invoices
|
||||
|
||||
@@ -2,7 +2,6 @@ import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
@@ -102,9 +101,9 @@ class BlinkWallet(Wallet):
|
||||
async def create_invoice(
|
||||
self,
|
||||
amount: int,
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
memo: str | None = None,
|
||||
description_hash: bytes | None = None,
|
||||
unhashed_description: bytes | None = None,
|
||||
**kwargs,
|
||||
) -> InvoiceResponse:
|
||||
# https://dev.blink.sv/api/btc-ln-receive
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import asyncio
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Optional
|
||||
|
||||
from bolt11.decode import decode
|
||||
from grpc.aio import AioRpcError
|
||||
@@ -107,9 +106,9 @@ class BoltzWallet(Wallet):
|
||||
async def create_invoice(
|
||||
self,
|
||||
amount: int,
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
memo: str | None = None,
|
||||
description_hash: bytes | None = None,
|
||||
unhashed_description: bytes | None = None,
|
||||
**_,
|
||||
) -> InvoiceResponse:
|
||||
pair = boltzrpc_pb2.Pair(to=boltzrpc_pb2.LBTC)
|
||||
@@ -254,7 +253,7 @@ class BoltzWallet(Wallet):
|
||||
)
|
||||
await asyncio.sleep(5)
|
||||
|
||||
async def _fetch_wallet(self, wallet_name: str) -> Optional[str]:
|
||||
async def _fetch_wallet(self, wallet_name: str) -> str | None:
|
||||
try:
|
||||
request = boltzrpc_pb2.GetWalletRequest(name=wallet_name)
|
||||
response = await self.rpc.GetWallet(request, metadata=self.metadata)
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
|
||||
wget https://raw.githubusercontent.com/BoltzExchange/boltz-client/refs/heads/master/pkg/boltzrpc/boltzrpc.proto -O boltz_grpc_files/boltzrpc.proto
|
||||
poetry run python -m grpc_tools.protoc -I . --python_out=. --grpc_python_out=. --pyi_out=. boltz_grpc_files/boltzrpc.proto
|
||||
uv run python -m grpc_tools.protoc -I . --python_out=. --grpc_python_out=. --pyi_out=. boltz_grpc_files/boltzrpc.proto
|
||||
|
||||
@@ -9,14 +9,13 @@ if not find_spec("breez_sdk"):
|
||||
def __init__(self):
|
||||
raise RuntimeError(
|
||||
"Breez SDK is not installed. "
|
||||
"Ask admin to run `poetry install -E breez` to install it."
|
||||
"Ask admin to run `uv sync --extra breez` to install it."
|
||||
)
|
||||
|
||||
else:
|
||||
import asyncio
|
||||
from collections.abc import AsyncGenerator
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from bolt11 import Bolt11Exception
|
||||
from bolt11 import decode as bolt11_decode
|
||||
@@ -66,7 +65,7 @@ else:
|
||||
):
|
||||
breez_incoming_queue.put_nowait(e.details)
|
||||
|
||||
def load_bytes(source: str, extension: str) -> Optional[bytes]:
|
||||
def load_bytes(source: str, extension: str) -> bytes | None:
|
||||
# first check if it can be read from a file
|
||||
if source.split(".")[-1] == extension:
|
||||
with open(source, "rb") as f:
|
||||
@@ -85,7 +84,7 @@ else:
|
||||
logger.debug(exc)
|
||||
return None
|
||||
|
||||
def load_greenlight_credentials() -> Optional[GreenlightCredentials]:
|
||||
def load_greenlight_credentials() -> GreenlightCredentials | None:
|
||||
if (
|
||||
settings.breez_greenlight_device_key
|
||||
and settings.breez_greenlight_device_cert
|
||||
@@ -168,9 +167,9 @@ else:
|
||||
async def create_invoice(
|
||||
self,
|
||||
amount: int,
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
memo: str | None = None,
|
||||
description_hash: bytes | None = None,
|
||||
unhashed_description: bytes | None = None,
|
||||
**kwargs,
|
||||
) -> InvoiceResponse:
|
||||
# if description_hash or unhashed_description:
|
||||
|
||||
@@ -8,7 +8,7 @@ if not find_spec("breez_sdk_liquid"):
|
||||
def __init__(self):
|
||||
raise RuntimeError(
|
||||
"Breez Liquid SDK is not installed. "
|
||||
"Ask admin to run `poetry add -E breez` to install it."
|
||||
"Ask admin to run `uv sync --extra breez` to install it."
|
||||
)
|
||||
|
||||
else:
|
||||
@@ -16,7 +16,6 @@ else:
|
||||
from asyncio import Queue
|
||||
from collections.abc import AsyncGenerator
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from bolt11 import decode as bolt11_decode
|
||||
from breez_sdk_liquid import (
|
||||
@@ -128,9 +127,9 @@ else:
|
||||
async def create_invoice(
|
||||
self,
|
||||
amount: int,
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
memo: str | None = None,
|
||||
description_hash: bytes | None = None,
|
||||
unhashed_description: bytes | None = None,
|
||||
**_,
|
||||
) -> InvoiceResponse:
|
||||
try:
|
||||
|
||||
@@ -2,7 +2,6 @@ import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Optional
|
||||
|
||||
from loguru import logger
|
||||
from websocket import create_connection
|
||||
@@ -53,9 +52,9 @@ class ClicheWallet(Wallet):
|
||||
async def create_invoice(
|
||||
self,
|
||||
amount: int,
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
memo: str | None = None,
|
||||
description_hash: bytes | None = None,
|
||||
unhashed_description: bytes | None = None,
|
||||
**_,
|
||||
) -> InvoiceResponse:
|
||||
if unhashed_description or description_hash:
|
||||
|
||||
@@ -5,7 +5,6 @@ import os
|
||||
import ssl
|
||||
import uuid
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import httpx
|
||||
@@ -174,9 +173,9 @@ class CLNRestWallet(Wallet):
|
||||
async def create_invoice(
|
||||
self,
|
||||
amount: int,
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
memo: str | None = None,
|
||||
description_hash: bytes | None = None,
|
||||
unhashed_description: bytes | None = None,
|
||||
**kwargs,
|
||||
) -> InvoiceResponse:
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import asyncio
|
||||
from collections.abc import AsyncGenerator
|
||||
from secrets import token_urlsafe
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
|
||||
from bolt11.decode import decode as bolt11_decode
|
||||
from bolt11.exceptions import Bolt11Exception
|
||||
@@ -92,9 +92,9 @@ class CoreLightningWallet(Wallet):
|
||||
async def create_invoice(
|
||||
self,
|
||||
amount: int,
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
memo: str | None = None,
|
||||
description_hash: bytes | None = None,
|
||||
unhashed_description: bytes | None = None,
|
||||
**kwargs,
|
||||
) -> InvoiceResponse:
|
||||
label = kwargs.get("label", f"lbl{token_urlsafe(16)}")
|
||||
|
||||
@@ -2,7 +2,6 @@ import asyncio
|
||||
import json
|
||||
from collections.abc import AsyncGenerator
|
||||
from secrets import token_urlsafe
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
from bolt11 import Bolt11Exception
|
||||
@@ -106,9 +105,9 @@ class CoreLightningRestWallet(Wallet):
|
||||
async def create_invoice(
|
||||
self,
|
||||
amount: int,
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
memo: str | None = None,
|
||||
description_hash: bytes | None = None,
|
||||
unhashed_description: bytes | None = None,
|
||||
**kwargs,
|
||||
) -> InvoiceResponse:
|
||||
label = kwargs.get("label", f"lbl{token_urlsafe(16)}")
|
||||
|
||||
@@ -5,7 +5,7 @@ import json
|
||||
import urllib.parse
|
||||
from collections.abc import AsyncGenerator
|
||||
from decimal import Decimal
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
@@ -84,9 +84,9 @@ class EclairWallet(Wallet):
|
||||
async def create_invoice(
|
||||
self,
|
||||
amount: int,
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
memo: str | None = None,
|
||||
description_hash: bytes | None = None,
|
||||
unhashed_description: bytes | None = None,
|
||||
**kwargs,
|
||||
) -> InvoiceResponse:
|
||||
data: dict[str, Any] = {
|
||||
|
||||
@@ -3,7 +3,6 @@ from collections.abc import AsyncGenerator
|
||||
from datetime import datetime
|
||||
from hashlib import sha256
|
||||
from os import urandom
|
||||
from typing import Optional
|
||||
|
||||
from bolt11 import (
|
||||
Bolt11,
|
||||
@@ -53,11 +52,11 @@ class FakeWallet(Wallet):
|
||||
async def create_invoice(
|
||||
self,
|
||||
amount: int,
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
expiry: Optional[int] = None,
|
||||
payment_secret: Optional[bytes] = None,
|
||||
memo: str | None = None,
|
||||
description_hash: bytes | None = None,
|
||||
unhashed_description: bytes | None = None,
|
||||
expiry: int | None = None,
|
||||
payment_secret: bytes | None = None,
|
||||
**_,
|
||||
) -> InvoiceResponse:
|
||||
tags = Tags()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import asyncio
|
||||
import json
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
@@ -71,9 +70,9 @@ class LNbitsWallet(Wallet):
|
||||
async def create_invoice(
|
||||
self,
|
||||
amount: int,
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
memo: str | None = None,
|
||||
description_hash: bytes | None = None,
|
||||
unhashed_description: bytes | None = None,
|
||||
**kwargs,
|
||||
) -> InvoiceResponse:
|
||||
data: dict = {"out": False, "amount": amount, "memo": memo or ""}
|
||||
|
||||
@@ -3,7 +3,6 @@ import base64
|
||||
from collections.abc import AsyncGenerator
|
||||
from hashlib import sha256
|
||||
from os import environ
|
||||
from typing import Optional
|
||||
|
||||
import grpc
|
||||
from loguru import logger
|
||||
@@ -123,9 +122,9 @@ class LndWallet(Wallet):
|
||||
async def create_invoice(
|
||||
self,
|
||||
amount: int,
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
memo: str | None = None,
|
||||
description_hash: bytes | None = None,
|
||||
unhashed_description: bytes | None = None,
|
||||
**kwargs,
|
||||
) -> InvoiceResponse:
|
||||
data: dict = {
|
||||
@@ -312,9 +311,9 @@ class LndWallet(Wallet):
|
||||
self,
|
||||
amount: int,
|
||||
payment_hash: str,
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
memo: str | None = None,
|
||||
description_hash: bytes | None = None,
|
||||
unhashed_description: bytes | None = None,
|
||||
**kwargs,
|
||||
) -> InvoiceResponse:
|
||||
data: dict = {
|
||||
|
||||
@@ -3,7 +3,7 @@ import base64
|
||||
import hashlib
|
||||
import json
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
@@ -103,9 +103,9 @@ class LndRestWallet(Wallet):
|
||||
async def create_invoice(
|
||||
self,
|
||||
amount: int,
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
memo: str | None = None,
|
||||
description_hash: bytes | None = None,
|
||||
unhashed_description: bytes | None = None,
|
||||
**kwargs,
|
||||
) -> InvoiceResponse:
|
||||
_data: dict = {
|
||||
@@ -327,9 +327,9 @@ class LndRestWallet(Wallet):
|
||||
self,
|
||||
amount: int,
|
||||
payment_hash: str,
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
memo: str | None = None,
|
||||
description_hash: bytes | None = None,
|
||||
unhashed_description: bytes | None = None,
|
||||
**_,
|
||||
) -> InvoiceResponse:
|
||||
data: dict = {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import asyncio
|
||||
import hashlib
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
@@ -75,9 +74,9 @@ class LNPayWallet(Wallet):
|
||||
async def create_invoice(
|
||||
self,
|
||||
amount: int,
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
memo: str | None = None,
|
||||
description_hash: bytes | None = None,
|
||||
unhashed_description: bytes | None = None,
|
||||
**_,
|
||||
) -> InvoiceResponse:
|
||||
data: dict = {"num_satoshis": f"{amount}"}
|
||||
|
||||
@@ -3,7 +3,6 @@ import hashlib
|
||||
import json
|
||||
import time
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
@@ -69,9 +68,9 @@ class LnTipsWallet(Wallet):
|
||||
async def create_invoice(
|
||||
self,
|
||||
amount: int,
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
memo: str | None = None,
|
||||
description_hash: bytes | None = None,
|
||||
unhashed_description: bytes | None = None,
|
||||
**_,
|
||||
) -> InvoiceResponse:
|
||||
data: dict = {"amount": amount, "description_hash": "", "memo": memo or ""}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import base64
|
||||
from getpass import getpass
|
||||
from typing import Optional
|
||||
|
||||
from loguru import logger
|
||||
|
||||
@@ -8,8 +7,8 @@ from lnbits.utils.crypto import AESCipher
|
||||
|
||||
|
||||
def load_macaroon(
|
||||
macaroon: Optional[str] = None,
|
||||
encrypted_macaroon: Optional[str] = None,
|
||||
macaroon: str | None = None,
|
||||
encrypted_macaroon: str | None = None,
|
||||
) -> str:
|
||||
"""Returns hex version of a macaroon encoded in base64 or the file path."""
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import json
|
||||
import random
|
||||
import time
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Optional, Union, cast
|
||||
from typing import cast
|
||||
from urllib.parse import parse_qs, unquote, urlparse
|
||||
|
||||
import secp256k1
|
||||
@@ -130,9 +130,9 @@ class NWCWallet(Wallet):
|
||||
async def create_invoice(
|
||||
self,
|
||||
amount: int,
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
memo: str | None = None,
|
||||
description_hash: bytes | None = None,
|
||||
unhashed_description: bytes | None = None,
|
||||
**_,
|
||||
) -> InvoiceResponse:
|
||||
desc = ""
|
||||
@@ -360,7 +360,7 @@ class NWCConnection:
|
||||
"""
|
||||
return self.shutdown or not settings.lnbits_running
|
||||
|
||||
async def _send(self, data: list[Union[str, dict]]):
|
||||
async def _send(self, data: list[str | dict]):
|
||||
"""
|
||||
Sends data to the NWC relay.
|
||||
|
||||
@@ -396,7 +396,7 @@ class NWCConnection:
|
||||
|
||||
async def _close_subscription_by_subid(
|
||||
self, sub_id: str, send_event: bool = True
|
||||
) -> Optional[dict]:
|
||||
) -> dict | None:
|
||||
"""
|
||||
Closes a subscription by its sub_id.
|
||||
|
||||
@@ -427,7 +427,7 @@ class NWCConnection:
|
||||
|
||||
async def _close_subscription_by_eventid(
|
||||
self, event_id, send_event=True
|
||||
) -> Optional[dict]:
|
||||
) -> dict | None:
|
||||
"""
|
||||
Closes a subscription associated to an event_id.
|
||||
|
||||
@@ -514,7 +514,7 @@ class NWCConnection:
|
||||
if subscription: # Check if the subscription exists first
|
||||
subscription["future"].set_exception(Exception(info))
|
||||
|
||||
async def _on_event_message(self, msg: list[Union[str, dict]]): # noqa: C901
|
||||
async def _on_event_message(self, msg: list[str | dict]): # noqa: C901
|
||||
"""
|
||||
Handles EVENT messages from the relay.
|
||||
"""
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import asyncio
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
@@ -71,9 +70,9 @@ class OpenNodeWallet(Wallet):
|
||||
async def create_invoice(
|
||||
self,
|
||||
amount: int,
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
memo: str | None = None,
|
||||
description_hash: bytes | None = None,
|
||||
unhashed_description: bytes | None = None,
|
||||
**_,
|
||||
) -> InvoiceResponse:
|
||||
if description_hash or unhashed_description:
|
||||
|
||||
@@ -4,7 +4,7 @@ import hashlib
|
||||
import json
|
||||
import urllib.parse
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from httpx import RequestError, TimeoutException
|
||||
@@ -96,9 +96,9 @@ class PhoenixdWallet(Wallet):
|
||||
async def create_invoice(
|
||||
self,
|
||||
amount: int,
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
memo: str | None = None,
|
||||
description_hash: bytes | None = None,
|
||||
unhashed_description: bytes | None = None,
|
||||
**kwargs,
|
||||
) -> InvoiceResponse:
|
||||
|
||||
@@ -226,9 +226,7 @@ class PhoenixdWallet(Wallet):
|
||||
)
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
r = await self.client.get(
|
||||
f"/payments/incoming/{checking_id}?all=true&limit=1000"
|
||||
)
|
||||
r = await self.client.get(f"/payments/incoming/{checking_id}")
|
||||
if r.is_error:
|
||||
if r.status_code == 404:
|
||||
# invoice does not exist in phoenixd, so it was never paid
|
||||
@@ -260,9 +258,7 @@ class PhoenixdWallet(Wallet):
|
||||
return PaymentPendingStatus()
|
||||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
r = await self.client.get(
|
||||
f"/payments/outgoing/{checking_id}?all=true&limit=1000"
|
||||
)
|
||||
r = await self.client.get(f"/payments/outgoingbyhash/{checking_id}")
|
||||
if r.is_error:
|
||||
if r.status_code == 404:
|
||||
# payment does not exist in phoenixd, so it was never paid
|
||||
|
||||
@@ -3,7 +3,6 @@ import hashlib
|
||||
import json
|
||||
from collections.abc import AsyncGenerator
|
||||
from secrets import token_urlsafe
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
@@ -111,9 +110,9 @@ class SparkWallet(Wallet):
|
||||
async def create_invoice(
|
||||
self,
|
||||
amount: int,
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
memo: str | None = None,
|
||||
description_hash: bytes | None = None,
|
||||
unhashed_description: bytes | None = None,
|
||||
**kwargs,
|
||||
) -> InvoiceResponse:
|
||||
label = f"lbs{token_urlsafe(16)}"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user