Compare commits
33
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55484b9cb4 | ||
|
|
4db7d20e60 | ||
|
|
02fec90a29 | ||
|
|
4efe1a9fca | ||
|
|
cba8c02d68 | ||
|
|
5c9d375713 | ||
|
|
264d1fa07c | ||
|
|
fc3e2b2b4d | ||
|
|
64edeebaaf | ||
|
|
c2739188d0 | ||
|
|
3f4d778c95 | ||
|
|
c1788c7219 | ||
|
|
07c62285c3 | ||
|
|
841c8ce3a9 | ||
|
|
cc6430221c | ||
|
|
4a34548864 | ||
|
|
4a3a3a0425 | ||
|
|
bf8b33a7b2 | ||
|
|
7e08211887 | ||
|
|
0bbb0aca2d | ||
|
|
9b1418b1f4 | ||
|
|
5f0a8d78e0 | ||
|
|
2fe8ae1cb1 | ||
|
|
1a3d23bbee | ||
|
|
ae45c1b958 | ||
|
|
8337351d6a | ||
|
|
f4dbd369be | ||
|
|
974ee26224 | ||
|
|
e4fb4453c3 | ||
|
|
aa7129a351 | ||
|
|
aba6e4cd87 | ||
|
|
f4e7d0c70c | ||
|
|
4247b9d9a3 |
@@ -21,16 +21,29 @@ runs:
|
|||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ inputs.python-version }}
|
python-version: ${{ inputs.python-version }}
|
||||||
|
# cache poetry install via pip
|
||||||
|
cache: "pip"
|
||||||
|
|
||||||
- name: Install uv
|
- name: Set up Poetry
|
||||||
uses: astral-sh/setup-uv@v6
|
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
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
path: ./.venv
|
||||||
python-version: ${{ inputs.python-version }}
|
key: venv-${{ hashFiles('poetry.lock') }}
|
||||||
|
|
||||||
- name: Install the project dependencies
|
- name: Install the project dependencies
|
||||||
shell: bash
|
shell: bash
|
||||||
run: uv sync --locked --all-extras --dev
|
run: |
|
||||||
|
poetry env use python${{ inputs.python-version }}
|
||||||
|
poetry install --all-extras
|
||||||
|
|
||||||
- name: Use Node.js ${{ inputs.node-version }}
|
- name: Use Node.js ${{ inputs.node-version }}
|
||||||
if: ${{ (inputs.npm == 'true') }}
|
if: ${{ (inputs.npm == 'true') }}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
db-url: ["", "postgres://lnbits:lnbits@0.0.0.0:5432/lnbits"]
|
db-url: ["", "postgres://lnbits:lnbits@0.0.0.0:5432/lnbits"]
|
||||||
uses: ./.github/workflows/tests.yml
|
uses: ./.github/workflows/tests.yml
|
||||||
with:
|
with:
|
||||||
custom-pytest: "uv run pytest tests/api"
|
custom-pytest: "poetry run pytest tests/api"
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
db-url: ${{ matrix.db-url }}
|
db-url: ${{ matrix.db-url }}
|
||||||
secrets:
|
secrets:
|
||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
db-url: ["", "postgres://lnbits:lnbits@0.0.0.0:5432/lnbits"]
|
db-url: ["", "postgres://lnbits:lnbits@0.0.0.0:5432/lnbits"]
|
||||||
uses: ./.github/workflows/tests.yml
|
uses: ./.github/workflows/tests.yml
|
||||||
with:
|
with:
|
||||||
custom-pytest: "uv run pytest tests/wallets"
|
custom-pytest: "poetry run pytest tests/wallets"
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
db-url: ${{ matrix.db-url }}
|
db-url: ${{ matrix.db-url }}
|
||||||
secrets:
|
secrets:
|
||||||
@@ -48,7 +48,7 @@ jobs:
|
|||||||
db-url: ["", "postgres://lnbits:lnbits@0.0.0.0:5432/lnbits"]
|
db-url: ["", "postgres://lnbits:lnbits@0.0.0.0:5432/lnbits"]
|
||||||
uses: ./.github/workflows/tests.yml
|
uses: ./.github/workflows/tests.yml
|
||||||
with:
|
with:
|
||||||
custom-pytest: "uv run pytest tests/unit"
|
custom-pytest: "poetry run pytest tests/unit"
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
db-url: ${{ matrix.db-url }}
|
db-url: ${{ matrix.db-url }}
|
||||||
secrets:
|
secrets:
|
||||||
@@ -77,7 +77,7 @@ jobs:
|
|||||||
python-version: ["3.10"]
|
python-version: ["3.10"]
|
||||||
backend-wallet-class: ["LndRestWallet", "LndWallet", "CoreLightningWallet", "CoreLightningRestWallet", "LNbitsWallet", "EclairWallet"]
|
backend-wallet-class: ["LndRestWallet", "LndWallet", "CoreLightningWallet", "CoreLightningRestWallet", "LNbitsWallet", "EclairWallet"]
|
||||||
with:
|
with:
|
||||||
custom-pytest: "uv run pytest tests/regtest"
|
custom-pytest: "poetry run pytest tests/regtest"
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
backend-wallet-class: ${{ matrix.backend-wallet-class }}
|
backend-wallet-class: ${{ matrix.backend-wallet-class }}
|
||||||
secrets:
|
secrets:
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ jobs:
|
|||||||
LNBITS_EXTENSIONS_DEFAULT_INSTALL: "watchonly, satspay, tipjar, tpos, lnurlp, withdraw"
|
LNBITS_EXTENSIONS_DEFAULT_INSTALL: "watchonly, satspay, tipjar, tpos, lnurlp, withdraw"
|
||||||
LNBITS_BACKEND_WALLET_CLASS: FakeWallet
|
LNBITS_BACKEND_WALLET_CLASS: FakeWallet
|
||||||
run: |
|
run: |
|
||||||
uv run lnbits &
|
poetry run lnbits &
|
||||||
sleep 10
|
sleep 10
|
||||||
|
|
||||||
- name: setup java version
|
- name: setup java version
|
||||||
|
|||||||
@@ -53,6 +53,3 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
make: checkbundle
|
make: checkbundle
|
||||||
npm: true
|
npm: true
|
||||||
|
|
||||||
poetry:
|
|
||||||
uses: ./.github/workflows/poetry.yml
|
|
||||||
|
|||||||
@@ -14,17 +14,18 @@ on:
|
|||||||
- 'flake.nix'
|
- 'flake.nix'
|
||||||
- 'flake.lock'
|
- 'flake.lock'
|
||||||
- 'pyproject.toml'
|
- 'pyproject.toml'
|
||||||
- 'uv.lock'
|
- 'poetry.lock'
|
||||||
- '.github/workflows/nix.yml'
|
- '.github/workflows/nix.yml'
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- 'flake.nix'
|
- 'flake.nix'
|
||||||
- 'flake.lock'
|
- 'flake.lock'
|
||||||
- 'pyproject.toml'
|
- 'pyproject.toml'
|
||||||
- 'uv.lock'
|
- 'poetry.lock'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
nix:
|
nix:
|
||||||
|
if: false # temporarly disable nix support until the `poetry2nix` issue is resolved
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Build LNbits AppImage
|
name: Build LNbits AppImage DMG
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
@@ -11,89 +11,56 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# Step 1: Checkout the repository
|
# Step 1: Checkout the repository
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
# Step 2: Set up Python (uv will still use this toolchain)
|
# Step 2: Install Dependencies
|
||||||
- name: Set up Python
|
- name: Install Dependencies
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: "3.12"
|
|
||||||
|
|
||||||
# Step 3: Install system deps (fuse) + uv
|
|
||||||
- name: Install system deps and uv
|
|
||||||
run: |
|
run: |
|
||||||
|
curl -sSL https://install.python-poetry.org | python3 -
|
||||||
|
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libfuse2
|
sudo apt-get install -y libfuse2
|
||||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
||||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
# Optional: Cache uv + venv to speed up CI
|
# Step 3: Clone LNbits Repository
|
||||||
- name: Cache uv and venv
|
- name: Clone LNbits
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cache/uv
|
|
||||||
.venv
|
|
||||||
key: ${{ runner.os }}-uv-${{ hashFiles('uv.lock', 'pyproject.toml') }}
|
|
||||||
|
|
||||||
# Step 4: Prepare packaging tree and clone LNbits
|
|
||||||
- name: Prepare packaging & clone LNbits
|
|
||||||
run: |
|
run: |
|
||||||
mv .github/packaging packaging
|
mv .github/packaging packaging
|
||||||
mkdir -p packaging/linux/AppDir/usr
|
mkdir -p packaging/linux/AppDir/usr
|
||||||
git clone https://github.com/lnbits/lnbits.git packaging/linux/AppDir/usr/lnbits
|
git clone https://github.com/lnbits/lnbits.git packaging/linux/AppDir/usr/lnbits
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
# Step 5: Build the LNbits binary with uv + PyInstaller
|
# Step 4: Make the AppImage Asset
|
||||||
- name: Build LNbits binary (uv + PyInstaller)
|
- name: Make Asset
|
||||||
run: |
|
run: |
|
||||||
cd packaging/linux/AppDir/usr/lnbits
|
cd packaging/linux/AppDir/usr/lnbits
|
||||||
|
poetry install
|
||||||
# Install project deps into .venv using uv
|
poetry run pip install pyinstaller
|
||||||
uv sync
|
|
||||||
|
|
||||||
# Install PyInstaller into the same environment
|
|
||||||
uv pip install pyinstaller
|
|
||||||
|
|
||||||
# Build the LNbits binary
|
# Build the LNbits binary
|
||||||
uv run pyinstaller \
|
poetry run pyinstaller --onefile --name lnbits --hidden-import=embit --collect-all embit --collect-all lnbits --collect-all sqlalchemy --collect-all aiosqlite --hidden-import=passlib.handlers.bcrypt $(poetry run which lnbits)
|
||||||
--onefile \
|
|
||||||
--name lnbits \
|
|
||||||
--hidden-import=embit \
|
|
||||||
--collect-all embit \
|
|
||||||
--collect-all lnbits \
|
|
||||||
--collect-all sqlalchemy \
|
|
||||||
--collect-all aiosqlite \
|
|
||||||
--hidden-import=passlib.handlers.bcrypt \
|
|
||||||
"$(uv run which lnbits)"
|
|
||||||
|
|
||||||
cd ../../../../..
|
cd ../../../../..
|
||||||
chmod +x packaging/linux/AppDir/AppRun
|
chmod +x packaging/linux/AppDir/AppRun
|
||||||
chmod +x packaging/linux/AppDir/lnbits.desktop
|
chmod +x packaging/linux/AppDir/lnbits.desktop
|
||||||
chmod +x packaging/linux/AppDir/usr/lnbits/dist/lnbits
|
chmod +x packaging/linux/AppDir/usr/lnbits/dist/lnbits
|
||||||
|
|
||||||
# Clean out non-dist content from the app dir to keep AppImage slim
|
|
||||||
find packaging/linux/AppDir/usr/lnbits -mindepth 1 -maxdepth 1 \
|
find packaging/linux/AppDir/usr/lnbits -mindepth 1 -maxdepth 1 \
|
||||||
! -name 'dist' \
|
! -name 'dist' \
|
||||||
! -name 'lnbits' \
|
! -name 'lnbits' \
|
||||||
-exec rm -rf {} +
|
-exec rm -rf {} +
|
||||||
|
|
||||||
# Build AppImage
|
|
||||||
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
|
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
|
||||||
chmod +x appimagetool-x86_64.AppImage
|
chmod +x appimagetool-x86_64.AppImage
|
||||||
TAG_NAME=${{ github.event.release.tag_name }}
|
TAG_NAME=${{ github.event.release.tag_name }}
|
||||||
APPIMAGE_NAME="LNbits-${TAG_NAME}.AppImage"
|
APPIMAGE_NAME="LNbits-${TAG_NAME}.AppImage"
|
||||||
./appimagetool-x86_64.AppImage \
|
./appimagetool-x86_64.AppImage --updateinformation "gh-releases-zsync|lnbits|lnbits|latest|*.AppImage.zsync" packaging/linux/AppDir "$APPIMAGE_NAME"
|
||||||
--updateinformation "gh-releases-zsync|lnbits|lnbits|latest|*.AppImage.zsync" \
|
|
||||||
packaging/linux/AppDir "$APPIMAGE_NAME"
|
|
||||||
chmod +x "$APPIMAGE_NAME"
|
chmod +x "$APPIMAGE_NAME"
|
||||||
echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV
|
echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
# Step 6: Upload Linux Release Asset
|
# Step 5: Upload Linux Release Asset
|
||||||
- name: Upload Linux Release Asset
|
- name: Upload Linux Release Asset
|
||||||
uses: actions/upload-release-asset@v1
|
uses: actions/upload-release-asset@v1
|
||||||
with:
|
with:
|
||||||
@@ -102,4 +69,4 @@ jobs:
|
|||||||
asset_name: ${{ env.APPIMAGE_NAME }}
|
asset_name: ${{ env.APPIMAGE_NAME }}
|
||||||
asset_content_type: application/octet-stream
|
asset_content_type: application/octet-stream
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
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,10 +32,6 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
docker build -t lnbits/lnbits .
|
docker build -t lnbits/lnbits .
|
||||||
|
|
||||||
- uses: ./.github/actions/prepare
|
|
||||||
with:
|
|
||||||
python-version: ${{ inputs.python-version }}
|
|
||||||
|
|
||||||
- name: Setup Regtest
|
- name: Setup Regtest
|
||||||
run: |
|
run: |
|
||||||
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
|
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
|
||||||
@@ -44,6 +40,10 @@ jobs:
|
|||||||
./tests
|
./tests
|
||||||
sudo chmod -R a+rwx .
|
sudo chmod -R a+rwx .
|
||||||
|
|
||||||
|
- uses: ./.github/actions/prepare
|
||||||
|
with:
|
||||||
|
python-version: ${{ inputs.python-version }}
|
||||||
|
|
||||||
- name: Run pytest
|
- name: Run pytest
|
||||||
uses: pavelzw/pytest-action@v2
|
uses: pavelzw/pytest-action@v2
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -61,4 +61,3 @@ dist
|
|||||||
|
|
||||||
# jetbrains
|
# jetbrains
|
||||||
.idea
|
.idea
|
||||||
.venv
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ exclude: '^lnbits/static/bundle.*|^docs/.*|^lnbits/static/vendor/.*|^lnbits/exte
|
|||||||
|
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v6.0.0
|
rev: v4.3.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
@@ -14,16 +14,16 @@ repos:
|
|||||||
- id: mixed-line-ending
|
- id: mixed-line-ending
|
||||||
- id: check-case-conflict
|
- id: check-case-conflict
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 25.1.0
|
rev: 24.2.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.12.10
|
rev: v0.3.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args: [ --fix, --exit-non-zero-on-fix ]
|
args: [ --fix, --exit-non-zero-on-fix ]
|
||||||
- repo: https://github.com/rbubley/mirrors-prettier
|
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||||
rev: v3.6.2
|
rev: "v4.0.0-alpha.8"
|
||||||
hooks:
|
hooks:
|
||||||
- id: prettier
|
- id: prettier
|
||||||
types_or: [css, javascript, html, json]
|
types_or: [css, javascript, html, json]
|
||||||
|
|||||||
+20
-7
@@ -2,20 +2,26 @@ FROM python:3.12-slim-bookworm AS builder
|
|||||||
|
|
||||||
RUN apt-get clean
|
RUN apt-get clean
|
||||||
RUN apt-get update
|
RUN apt-get update
|
||||||
RUN apt-get install -y curl pkg-config build-essential libnss-myhostname automake
|
RUN apt-get install -y curl pkg-config build-essential libnss-myhostname
|
||||||
|
|
||||||
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
|
RUN curl -sSL https://install.python-poetry.org | python3 -
|
||||||
ENV PATH="/root/.local/bin:$PATH"
|
ENV PATH="/root/.local/bin:$PATH"
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Only copy the files required to install the dependencies
|
# Only copy the files required to install the dependencies
|
||||||
COPY pyproject.toml uv.lock ./
|
COPY pyproject.toml poetry.lock ./
|
||||||
RUN touch README.md
|
RUN touch README.md
|
||||||
|
|
||||||
RUN mkdir data
|
RUN mkdir data
|
||||||
|
|
||||||
RUN uv sync --all-extras
|
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}
|
||||||
|
|
||||||
FROM python:3.12-slim-bookworm
|
FROM python:3.12-slim-bookworm
|
||||||
|
|
||||||
@@ -28,19 +34,26 @@ RUN apt-get update && apt-get -y upgrade && \
|
|||||||
apt-get -y install postgresql-client-14 postgresql-client-common && \
|
apt-get -y install postgresql-client-14 postgresql-client-common && \
|
||||||
apt-get clean all && rm -rf /var/lib/apt/lists/*
|
apt-get clean all && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
|
RUN curl -sSL https://install.python-poetry.org | python3 -
|
||||||
ENV PATH="/root/.local/bin:$PATH"
|
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
|
WORKDIR /app
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
COPY --from=builder /app/.venv .venv
|
COPY --from=builder /app/.venv .venv
|
||||||
|
|
||||||
RUN uv sync --all-extras
|
ARG POETRY_INSTALL_ARGS="--only main"
|
||||||
|
RUN poetry install ${POETRY_INSTALL_ARGS}
|
||||||
|
|
||||||
ENV LNBITS_PORT="5000"
|
ENV LNBITS_PORT="5000"
|
||||||
ENV LNBITS_HOST="0.0.0.0"
|
ENV LNBITS_HOST="0.0.0.0"
|
||||||
|
|
||||||
EXPOSE 5000
|
EXPOSE 5000
|
||||||
|
|
||||||
CMD ["sh", "-c", "uv run lnbits --port $LNBITS_PORT --host $LNBITS_HOST --forwarded-allow-ips='*'"]
|
CMD ["sh", "-c", "poetry run lnbits --port $LNBITS_PORT --host $LNBITS_HOST --forwarded-allow-ips='*'"]
|
||||||
|
|||||||
+2
-4
@@ -7,11 +7,9 @@ RUN ls -l /usr/local/bin/boltzd
|
|||||||
RUN apt-get update && apt-get -y upgrade && \
|
RUN apt-get update && apt-get -y upgrade && \
|
||||||
apt-get install -y netcat-openbsd
|
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)
|
# Reinstall dependencies just in case (needed for CMD usage)
|
||||||
RUN uv sync --all-extras
|
ARG POETRY_INSTALL_ARGS="--only main"
|
||||||
|
RUN poetry install ${POETRY_INSTALL_ARGS}
|
||||||
|
|
||||||
# LNbits + boltzd configuration
|
# LNbits + boltzd configuration
|
||||||
ENV LNBITS_PORT="5000"
|
ENV LNBITS_PORT="5000"
|
||||||
|
|||||||
@@ -9,34 +9,34 @@ check: mypy pyright checkblack checkruff checkprettier checkbundle
|
|||||||
test: test-unit test-wallets test-api test-regtest
|
test: test-unit test-wallets test-api test-regtest
|
||||||
|
|
||||||
prettier:
|
prettier:
|
||||||
uv run ./node_modules/.bin/prettier --write .
|
poetry run ./node_modules/.bin/prettier --write .
|
||||||
|
|
||||||
pyright:
|
pyright:
|
||||||
uv run ./node_modules/.bin/pyright
|
poetry run ./node_modules/.bin/pyright
|
||||||
|
|
||||||
mypy:
|
mypy:
|
||||||
uv run mypy
|
poetry run mypy
|
||||||
|
|
||||||
black:
|
black:
|
||||||
uv run black .
|
poetry run black .
|
||||||
|
|
||||||
ruff:
|
ruff:
|
||||||
uv run ruff check . --fix
|
poetry run ruff check . --fix
|
||||||
|
|
||||||
checkruff:
|
checkruff:
|
||||||
uv run ruff check .
|
poetry run ruff check .
|
||||||
|
|
||||||
checkprettier:
|
checkprettier:
|
||||||
uv run ./node_modules/.bin/prettier --check .
|
poetry run ./node_modules/.bin/prettier --check .
|
||||||
|
|
||||||
checkblack:
|
checkblack:
|
||||||
uv run black --check .
|
poetry run black --check .
|
||||||
|
|
||||||
checkeditorconfig:
|
checkeditorconfig:
|
||||||
editorconfig-checker
|
editorconfig-checker
|
||||||
|
|
||||||
dev:
|
dev:
|
||||||
uv run lnbits --reload
|
poetry run lnbits --reload
|
||||||
|
|
||||||
docker:
|
docker:
|
||||||
docker build -t lnbits/lnbits .
|
docker build -t lnbits/lnbits .
|
||||||
@@ -46,27 +46,27 @@ test-wallets:
|
|||||||
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
|
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
|
||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
DEBUG=true \
|
DEBUG=true \
|
||||||
uv run pytest tests/wallets
|
poetry run pytest tests/wallets
|
||||||
|
|
||||||
test-unit:
|
test-unit:
|
||||||
LNBITS_DATA_FOLDER="./tests/data" \
|
LNBITS_DATA_FOLDER="./tests/data" \
|
||||||
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
|
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
|
||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
DEBUG=true \
|
DEBUG=true \
|
||||||
uv run pytest tests/unit
|
poetry run pytest tests/unit
|
||||||
|
|
||||||
test-api:
|
test-api:
|
||||||
LNBITS_DATA_FOLDER="./tests/data" \
|
LNBITS_DATA_FOLDER="./tests/data" \
|
||||||
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
|
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
|
||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
DEBUG=true \
|
DEBUG=true \
|
||||||
uv run pytest tests/api
|
poetry run pytest tests/api
|
||||||
|
|
||||||
test-regtest:
|
test-regtest:
|
||||||
LNBITS_DATA_FOLDER="./tests/data" \
|
LNBITS_DATA_FOLDER="./tests/data" \
|
||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
DEBUG=true \
|
DEBUG=true \
|
||||||
uv run pytest tests/regtest
|
poetry run pytest tests/regtest
|
||||||
|
|
||||||
test-migration:
|
test-migration:
|
||||||
LNBITS_ADMIN_UI=True \
|
LNBITS_ADMIN_UI=True \
|
||||||
@@ -74,18 +74,18 @@ test-migration:
|
|||||||
HOST=0.0.0.0 \
|
HOST=0.0.0.0 \
|
||||||
PORT=5002 \
|
PORT=5002 \
|
||||||
LNBITS_DATA_FOLDER="./tests/data" \
|
LNBITS_DATA_FOLDER="./tests/data" \
|
||||||
timeout 5s uv run lnbits --host 0.0.0.0 --port 5002 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi
|
timeout 5s poetry 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 \
|
HOST=0.0.0.0 \
|
||||||
PORT=5002 \
|
PORT=5002 \
|
||||||
LNBITS_DATABASE_URL="postgres://lnbits:lnbits@localhost:5432/migration" \
|
LNBITS_DATABASE_URL="postgres://lnbits:lnbits@localhost:5432/migration" \
|
||||||
LNBITS_ADMIN_UI=False \
|
LNBITS_ADMIN_UI=False \
|
||||||
timeout 5s uv run lnbits --host 0.0.0.0 --port 5002 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi
|
timeout 5s poetry 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_DATA_FOLDER="./tests/data" \
|
||||||
LNBITS_DATABASE_URL="postgres://lnbits:lnbits@localhost:5432/migration" \
|
LNBITS_DATABASE_URL="postgres://lnbits:lnbits@localhost:5432/migration" \
|
||||||
uv run python tools/conv.py
|
poetry run python tools/conv.py
|
||||||
|
|
||||||
migration:
|
migration:
|
||||||
uv run python tools/conv.py
|
poetry run python tools/conv.py
|
||||||
|
|
||||||
openapi:
|
openapi:
|
||||||
LNBITS_ADMIN_UI=False \
|
LNBITS_ADMIN_UI=False \
|
||||||
@@ -94,9 +94,9 @@ openapi:
|
|||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
HOST=0.0.0.0 \
|
HOST=0.0.0.0 \
|
||||||
PORT=5003 \
|
PORT=5003 \
|
||||||
uv run lnbits &
|
poetry run lnbits &
|
||||||
sleep 15
|
sleep 15
|
||||||
curl -s http://0.0.0.0:5003/openapi.json | uv run openapi-spec-validator --errors=all -
|
curl -s http://0.0.0.0:5003/openapi.json | poetry run openapi-spec-validator --errors=all -
|
||||||
# kill -9 %1
|
# kill -9 %1
|
||||||
|
|
||||||
bak:
|
bak:
|
||||||
@@ -109,7 +109,7 @@ sass:
|
|||||||
bundle:
|
bundle:
|
||||||
npm install
|
npm install
|
||||||
npm run bundle
|
npm run bundle
|
||||||
uv run ./node_modules/.bin/prettier -w ./lnbits/static/vendor.json
|
poetry run ./node_modules/.bin/prettier -w ./lnbits/static/vendor.json
|
||||||
|
|
||||||
checkbundle:
|
checkbundle:
|
||||||
cp lnbits/static/bundle.min.js lnbits/static/bundle.min.js.old
|
cp lnbits/static/bundle.min.js lnbits/static/bundle.min.js.old
|
||||||
@@ -126,8 +126,8 @@ checkbundle:
|
|||||||
|
|
||||||
install-pre-commit-hook:
|
install-pre-commit-hook:
|
||||||
@echo "Installing pre-commit hook to git"
|
@echo "Installing pre-commit hook to git"
|
||||||
@echo "Uninstall the hook with uv run pre-commit uninstall"
|
@echo "Uninstall the hook with poetry run pre-commit uninstall"
|
||||||
uv run pre-commit install
|
poetry run pre-commit install
|
||||||
|
|
||||||
pre-commit:
|
pre-commit:
|
||||||
uv run pre-commit run --all-files
|
poetry run pre-commit run --all-files
|
||||||
|
|||||||
+1
-1
@@ -23,4 +23,4 @@ if ! nc -z localhost 9002; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Starting LNbits on $LNBITS_HOST:$LNBITS_PORT..."
|
echo "Starting LNbits on $LNBITS_HOST:$LNBITS_PORT..."
|
||||||
exec uv run lnbits --port "$LNBITS_PORT" --host "$LNBITS_HOST" --forwarded-allow-ips='*'
|
exec poetry run lnbits --port "$LNBITS_PORT" --host "$LNBITS_HOST" --forwarded-allow-ips='*'
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ Thanks for contributing :)
|
|||||||
|
|
||||||
# Run
|
# Run
|
||||||
|
|
||||||
Follow the [Option 2 (recommended): UV](https://docs.lnbits.org/guide/installation.html)
|
Follow the [Basic installation: Option 1 (recommended): poetry](https://docs.lnbits.org/guide/installation.html#option-1-recommended-poetry)
|
||||||
guide to install uv and other dependencies.
|
guide to install poetry and other dependencies.
|
||||||
|
|
||||||
Then you can start LNbits uvicorn server with:
|
Then you can start LNbits uvicorn server with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv run lnbits
|
poetry run lnbits
|
||||||
```
|
```
|
||||||
|
|
||||||
Or you can use the following to start uvicorn with hot reloading enabled:
|
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
|
```bash
|
||||||
make dev
|
make dev
|
||||||
# or
|
# or
|
||||||
uv run lnbits --reload
|
poetry run lnbits --reload
|
||||||
```
|
```
|
||||||
|
|
||||||
You might need the following extra dependencies on clean installation of Debian:
|
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:
|
This project has unit tests that help prevent regressions. Before you can run the tests, you must install a few dependencies:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv sync --all-extras --dev
|
poetry install
|
||||||
npm i
|
npm i
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ make format
|
|||||||
Run mypy checks:
|
Run mypy checks:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make mypy
|
poetry run mypy
|
||||||
```
|
```
|
||||||
|
|
||||||
Run everything:
|
Run everything:
|
||||||
|
|||||||
@@ -42,10 +42,14 @@ 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.
|
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` or `uv`:
|
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>
|
||||||
|
```
|
||||||
|
|
||||||
**But we need an extra step to make sure LNbits doesn't break in production.**
|
**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 `uv` and `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 `poetry` compatibility can be tested with `nix build .#checks.x86_64-linux.vmTest`.
|
||||||
|
|
||||||
## SQLite to PostgreSQL migration
|
## SQLite to PostgreSQL migration
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ $ sudo nano .env
|
|||||||
Now start LNbits once in the terminal window
|
Now start LNbits once in the terminal window
|
||||||
|
|
||||||
```
|
```
|
||||||
$ uv run lnbits
|
$ poetry run lnbits
|
||||||
```
|
```
|
||||||
|
|
||||||
You can now `cat` the Super User ID:
|
You can now `cat` the Super User ID:
|
||||||
|
|||||||
+22
-40
@@ -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.
|
LNbits will create a folder for db and extension files in the folder the AppImage runs from.
|
||||||
|
|
||||||
## Option 2: UV (recommended for developers)
|
## Option 2: Poetry (recommended for developers)
|
||||||
|
|
||||||
It is recommended to use the latest version of UV. Make sure you have Python version `3.12` installed.
|
It is recommended to use the latest version of Poetry. Make sure you have Python version `3.12` installed.
|
||||||
|
|
||||||
### Install Python 3.12
|
### Install Python 3.12
|
||||||
|
|
||||||
## Option 2 (recommended): UV
|
## Option 2 (recommended): Poetry
|
||||||
|
|
||||||
It is recommended to use the latest version of UV. Make sure you have Python version 3.10 or higher installed.
|
It is recommended to use the latest version of Poetry. Make sure you have Python version 3.9 or higher installed.
|
||||||
|
|
||||||
### Verify Python version
|
### Verify Python version
|
||||||
|
|
||||||
@@ -39,19 +39,11 @@ It is recommended to use the latest version of UV. Make sure you have Python ver
|
|||||||
python3 --version
|
python3 --version
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install UV
|
### Install Poetry
|
||||||
|
|
||||||
```sh
|
|
||||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
|
||||||
```
|
|
||||||
|
|
||||||
### (old) Install Poetry
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# If path 'export PATH="$HOME/.local/bin:$PATH"' fails, use the path echoed by the install
|
# If path 'export PATH="$HOME/.local/bin:$PATH"' fails, use the path echoed by the install
|
||||||
curl -sSL https://install.python-poetry.org | python3 -
|
curl -sSL https://install.python-poetry.org | python3 - && export PATH="$HOME/.local/bin:$PATH"
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### install LNbits
|
### install LNbits
|
||||||
@@ -59,13 +51,9 @@ export PATH="$HOME/.local/bin:$PATH"
|
|||||||
```sh
|
```sh
|
||||||
git clone https://github.com/lnbits/lnbits.git
|
git clone https://github.com/lnbits/lnbits.git
|
||||||
cd lnbits
|
cd lnbits
|
||||||
|
poetry env use 3.12
|
||||||
git checkout main
|
git checkout main
|
||||||
uv sync --all-extras
|
poetry install --only main
|
||||||
|
|
||||||
# or poetry
|
|
||||||
# poetry env use 3.12
|
|
||||||
# poetry install --only main
|
|
||||||
|
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
# Optional: to set funding source amongst other options via the env `nano .env`
|
# Optional: to set funding source amongst other options via the env `nano .env`
|
||||||
```
|
```
|
||||||
@@ -73,11 +61,8 @@ cp .env.example .env
|
|||||||
#### Running the server
|
#### Running the server
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
uv run lnbits
|
poetry run lnbits
|
||||||
# To change port/host pass 'uv run lnbits --port 9000 --host 0.0.0.0'
|
# To change port/host pass 'poetry 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
|
# 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.
|
# Note that you have to add the line DEBUG=true in your .env file, too.
|
||||||
```
|
```
|
||||||
@@ -86,7 +71,7 @@ uv run lnbits
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
# A very useful terminal client for getting the supersuer ID, updating extensions, etc
|
# A very useful terminal client for getting the supersuer ID, updating extensions, etc
|
||||||
uv run lnbits-cli --help
|
poetry run lnbits-cli --help
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Updating the server
|
#### Updating the server
|
||||||
@@ -100,18 +85,15 @@ cd lnbits
|
|||||||
git pull --rebase
|
git pull --rebase
|
||||||
|
|
||||||
# Check your poetry version with
|
# Check your poetry version with
|
||||||
# poetry env list
|
poetry env list
|
||||||
# If version is less 3.12, update it by running
|
# If version is less 3.12, update it by running
|
||||||
# poetry env use python3.12
|
poetry env use python3.12
|
||||||
# poetry env remove python3.9
|
poetry env remove python3.9
|
||||||
# poetry env list
|
poetry env list
|
||||||
|
|
||||||
# Run install and start LNbits with
|
# Run install and start LNbits with
|
||||||
# poetry install --only main
|
poetry install --only main
|
||||||
# poetry run lnbits
|
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
|
# use LNbits admin UI Extensions page function "Update All" do get extensions onto proper level
|
||||||
```
|
```
|
||||||
@@ -126,13 +108,13 @@ chmod +x lnbits.sh &&
|
|||||||
|
|
||||||
Now visit `0.0.0.0:5000` to make a super-user account.
|
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 `uv run lnbits` (see previous option).
|
`./lnbits.sh` can be used to run, but for more control `cd lnbits` and use `poetry run lnbits` (see previous option).
|
||||||
|
|
||||||
## Option 3: Nix
|
## Option 3: Nix
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Install nix. If you have installed via another manager, remove and use this install (from https://nixos.org/download)
|
# Install nix. If you have installed via another manager, remove and use this install (from https://nixos.org/download)
|
||||||
sh <(c&url -L https://nixos.org/nix/install) --daemon
|
sh <(curl -L https://nixos.org/nix/install) --daemon
|
||||||
|
|
||||||
# Enable nix-command and flakes experimental features for nix:
|
# Enable nix-command and flakes experimental features for nix:
|
||||||
echo 'experimental-features = nix-command flakes' >> /etc/nix/nix.conf
|
echo 'experimental-features = nix-command flakes' >> /etc/nix/nix.conf
|
||||||
@@ -350,7 +332,7 @@ LNBITS_DATABASE_URL="postgres://postgres:postgres@localhost/lnbits"
|
|||||||
|
|
||||||
# START LNbits
|
# START LNbits
|
||||||
# STOP LNbits
|
# STOP LNbits
|
||||||
uv run python tools/conv.py
|
poetry run python tools/conv.py
|
||||||
# or
|
# or
|
||||||
make migration
|
make migration
|
||||||
```
|
```
|
||||||
@@ -375,8 +357,8 @@ Description=LNbits
|
|||||||
[Service]
|
[Service]
|
||||||
# replace with the absolute path of your lnbits installation
|
# replace with the absolute path of your lnbits installation
|
||||||
WorkingDirectory=/home/lnbits/lnbits
|
WorkingDirectory=/home/lnbits/lnbits
|
||||||
# same here. run `which uv` if you can't find the poetry binary
|
# same here. run `which poetry` if you can't find the poetry binary
|
||||||
ExecStart=/home/lnbits/.local/bin/uv run lnbits
|
ExecStart=/home/lnbits/.local/bin/poetry run lnbits
|
||||||
# replace with the user that you're running lnbits on
|
# replace with the user that you're running lnbits on
|
||||||
User=lnbits
|
User=lnbits
|
||||||
Restart=always
|
Restart=always
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ You can also use an AES-encrypted macaroon (more info) instead by using
|
|||||||
|
|
||||||
- `LND_GRPC_MACAROON_ENCRYPTED`: eNcRyPtEdMaCaRoOn
|
- `LND_GRPC_MACAROON_ENCRYPTED`: eNcRyPtEdMaCaRoOn
|
||||||
|
|
||||||
To encrypt your macaroon, run `uv run lnbits-cli encrypt macaroon`.
|
To encrypt your macaroon, run `poetry run lnbits-cli encrypt macaroon`.
|
||||||
|
|
||||||
### LNbits
|
### LNbits
|
||||||
|
|
||||||
|
|||||||
Generated
+61
-76
@@ -1,39 +1,15 @@
|
|||||||
{
|
{
|
||||||
"nodes": {
|
"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": {
|
"flake-utils": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"systems": "systems"
|
"systems": "systems"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1731533236,
|
"lastModified": 1710146030,
|
||||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "flake-utils",
|
"repo": "flake-utils",
|
||||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -42,49 +18,71 @@
|
|||||||
"type": "github"
|
"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": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1751274312,
|
"lastModified": 1735563628,
|
||||||
"narHash": "sha256-/bVBlRpECLVzjV19t5KMdMFWSwKLtb5RyXdjz3LJT+g=",
|
"narHash": "sha256-OnSAY7XDSx7CtDoqNh8jwVwh4xNL/2HaJxGjryLWzX8=",
|
||||||
"owner": "NixOS",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "50ab793786d9de88ee30ec4e4c24fb4236fc2674",
|
"rev": "b134951a4c9f3c995fd7be05f3243f8ecd65d798",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "NixOS",
|
"owner": "nixos",
|
||||||
"ref": "nixos-24.11",
|
"ref": "nixos-24.05",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pyproject-nix": {
|
"poetry2nix": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nix-github-actions": "nix-github-actions",
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
"nixpkgs"
|
"nixpkgs"
|
||||||
]
|
],
|
||||||
|
"systems": "systems_2",
|
||||||
|
"treefmt-nix": "treefmt-nix"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1754923840,
|
"lastModified": 1724134185,
|
||||||
"narHash": "sha256-QSKpYg+Ts9HYF155ltlj40iBex39c05cpOF8gjoE2EM=",
|
"narHash": "sha256-nDqpGjz7cq3ThdC98BPe1ANCNlsJds/LLZ3/MdIXjA0=",
|
||||||
"owner": "pyproject-nix",
|
"owner": "nix-community",
|
||||||
"repo": "pyproject.nix",
|
"repo": "poetry2nix",
|
||||||
"rev": "023cd4be230eacae52635be09eef100c37ef78da",
|
"rev": "5ee730a8752264e463c0eaf06cc060fd07f6dae9",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "pyproject-nix",
|
"owner": "nix-community",
|
||||||
"repo": "pyproject.nix",
|
"repo": "poetry2nix",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"build-system-pkgs": "build-system-pkgs",
|
|
||||||
"flake-utils": "flake-utils",
|
|
||||||
"nixpkgs": "nixpkgs",
|
"nixpkgs": "nixpkgs",
|
||||||
"pyproject-nix": "pyproject-nix",
|
"poetry2nix": "poetry2nix"
|
||||||
"uv2nix": "uv2nix_2"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"systems": {
|
"systems": {
|
||||||
@@ -102,51 +100,38 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"uv2nix": {
|
"systems_2": {
|
||||||
"inputs": {
|
|
||||||
"nixpkgs": [
|
|
||||||
"build-system-pkgs",
|
|
||||||
"nixpkgs"
|
|
||||||
],
|
|
||||||
"pyproject-nix": [
|
|
||||||
"build-system-pkgs",
|
|
||||||
"pyproject-nix"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1755210905,
|
"lastModified": 1681028828,
|
||||||
"narHash": "sha256-WnoFEk79ysjL85TNP7bvImzhxvQw9B6uNtnLd4oJntw=",
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
"owner": "pyproject-nix",
|
"owner": "nix-systems",
|
||||||
"repo": "uv2nix",
|
"repo": "default",
|
||||||
"rev": "87bcba013ef304bbfd67c8e8a257aee634ed5a4c",
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "pyproject-nix",
|
"id": "systems",
|
||||||
"repo": "uv2nix",
|
"type": "indirect"
|
||||||
"type": "github"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"uv2nix_2": {
|
"treefmt-nix": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
|
"poetry2nix",
|
||||||
"nixpkgs"
|
"nixpkgs"
|
||||||
],
|
|
||||||
"pyproject-nix": [
|
|
||||||
"pyproject-nix"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1755485731,
|
"lastModified": 1719749022,
|
||||||
"narHash": "sha256-k8kxwVs8Oze6q/jAaRa3RvZbb50I/K0b5uptlsh0HXI=",
|
"narHash": "sha256-ddPKHcqaKCIFSFc/cvxS14goUhCOAwsM1PbMr0ZtHMg=",
|
||||||
"owner": "pyproject-nix",
|
"owner": "numtide",
|
||||||
"repo": "uv2nix",
|
"repo": "treefmt-nix",
|
||||||
"rev": "bebbd80bf56110fcd20b425589814af28f1939eb",
|
"rev": "8df5ff62195d4e67e2264df0b7f5e8c9995fd0bd",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "pyproject-nix",
|
"owner": "numtide",
|
||||||
"repo": "uv2nix",
|
"repo": "treefmt-nix",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,149 +1,70 @@
|
|||||||
{
|
{
|
||||||
description = "LNbits, free and open-source Lightning wallet and accounts system (uv2nix)";
|
description = "LNbits, free and open-source Lightning wallet and accounts system";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-24.05";
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
poetry2nix = {
|
||||||
|
url = "github:nix-community/poetry2nix";
|
||||||
pyproject-nix.url = "github:pyproject-nix/pyproject.nix";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
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:
|
||||||
outputs = { self, nixpkgs, flake-utils, uv2nix, pyproject-nix, build-system-pkgs, ... }:
|
let
|
||||||
flake-utils.lib.eachSystem [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]
|
supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
|
||||||
(system:
|
forSystems = systems: f:
|
||||||
let
|
nixpkgs.lib.genAttrs systems
|
||||||
pkgs = import nixpkgs { inherit system; };
|
(system: f system (import nixpkgs { inherit system; overlays = [ poetry2nix.overlays.default self.overlays.default ]; }));
|
||||||
lib = pkgs.lib;
|
forAllSystems = forSystems supportedSystems;
|
||||||
|
projectName = "lnbits";
|
||||||
python = pkgs.python312;
|
in
|
||||||
|
{
|
||||||
# Read uv.lock / pyproject via uv2nix
|
overlays = {
|
||||||
workspace = uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./.; };
|
default = final: prev: {
|
||||||
|
${projectName} = self.packages.${prev.stdenv.hostPlatform.system}.${projectName};
|
||||||
# Prefer wheels when available
|
};
|
||||||
uvLockedOverlay = workspace.mkPyprojectOverlay { sourcePreference = "wheel"; };
|
};
|
||||||
|
packages = forAllSystems (system: pkgs: {
|
||||||
# Helper for extending lists safely (works if a is null)
|
default = self.packages.${system}.${projectName};
|
||||||
plus = a: b: lib.unique (((if a == null then [] else a)) ++ b);
|
${projectName} = pkgs.poetry2nix.mkPoetryApplication {
|
||||||
|
projectDir = ./.;
|
||||||
# Extra build inputs for troublesome sdists
|
meta.rev = self.dirtyRev or self.rev;
|
||||||
myOverrides = (final: prev: {
|
meta.mainProgram = projectName;
|
||||||
# embit needs setuptools at build time
|
overrides = pkgs.poetry2nix.overrides.withDefaults (final: prev: {
|
||||||
embit = prev.embit.overrideAttrs (old: {
|
coincurve = prev.coincurve.override { preferWheel = true; };
|
||||||
nativeBuildInputs = plus (old.nativeBuildInputs or []) [ prev.setuptools ];
|
protobuf = prev.protobuf.override { preferWheel = true; };
|
||||||
});
|
ruff = prev.ruff.override { preferWheel = true; };
|
||||||
|
wallycore = prev.wallycore.override { preferWheel = true; };
|
||||||
# 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: {
|
tlv8 = prev.tlv8.overrideAttrs (old: {
|
||||||
nativeBuildInputs = plus (old.nativeBuildInputs or []) [ prev.setuptools ];
|
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [
|
||||||
});
|
|
||||||
|
|
||||||
# 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
|
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: {
|
pynostr = prev.pynostr.overrideAttrs (old: {
|
||||||
nativeBuildInputs = plus (old.nativeBuildInputs or []) [ prev.setuptools-scm ];
|
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [
|
||||||
|
prev.setuptools-scm
|
||||||
|
];
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
};
|
||||||
# Compose Python package set honoring uv.lock
|
});
|
||||||
pythonSet =
|
nixosModules = {
|
||||||
(pkgs.callPackage pyproject-nix.build.packages { inherit python; })
|
default = { pkgs, lib, config, ... }: {
|
||||||
.overrideScope (lib.composeManyExtensions [
|
imports = [ "${./nix/modules/${projectName}-service.nix}" ];
|
||||||
build-system-pkgs.overlays.default
|
nixpkgs.overlays = [ self.overlays.default ];
|
||||||
uvLockedOverlay
|
};
|
||||||
myOverrides
|
};
|
||||||
]);
|
checks = forAllSystems (system: pkgs:
|
||||||
|
let
|
||||||
projectName = "lnbits";
|
vmTests = import ./nix/tests {
|
||||||
|
makeTest = (import (nixpkgs + "/nixos/lib/testing-python.nix") { inherit system; }).makeTest;
|
||||||
# Build a venv from the locked spec (this installs the resolved wheels)
|
inherit inputs pkgs;
|
||||||
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
|
in
|
||||||
|
pkgs.lib.optionalAttrs pkgs.stdenv.isLinux vmTests # vmTests can only be ran on Linux, so append them only if on Linux.
|
||||||
|
//
|
||||||
{
|
{
|
||||||
# nix build → produces the venv in ./result
|
# Other checks here...
|
||||||
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,9 +13,10 @@ if [ ! -d lnbits/data ]; then
|
|||||||
# Install Python 3.10 and distutils non-interactively
|
# Install Python 3.10 and distutils non-interactively
|
||||||
sudo apt install -y python3.10 python3.10-distutils
|
sudo apt install -y python3.10 python3.10-distutils
|
||||||
|
|
||||||
# Install UV
|
# Install Poetry
|
||||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
curl -sSL https://install.python-poetry.org | python3.10 -
|
||||||
|
|
||||||
|
# Add Poetry to PATH for the current session
|
||||||
export PATH="/home/$USER/.local/bin:$PATH"
|
export PATH="/home/$USER/.local/bin:$PATH"
|
||||||
|
|
||||||
if [ ! -d lnbits/wallets ]; then
|
if [ ! -d lnbits/wallets ]; then
|
||||||
@@ -41,13 +42,13 @@ elif [ ! -d lnbits/wallets ]; then
|
|||||||
cd lnbits || { echo "Failed to cd into lnbits ... FAIL"; exit 1; }
|
cd lnbits || { echo "Failed to cd into lnbits ... FAIL"; exit 1; }
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Install the dependencies using UV
|
# Install the dependencies using Poetry
|
||||||
uv sync --all-extras
|
poetry env use python3.9
|
||||||
|
poetry install --only main
|
||||||
|
|
||||||
# Set environment variables for LNbits
|
# Set environment variables for LNbits
|
||||||
export LNBITS_ADMIN_UI=true
|
export LNBITS_ADMIN_UI=true
|
||||||
export HOST=0.0.0.0
|
export HOST=0.0.0.0
|
||||||
|
|
||||||
# Run LNbits
|
# Run LNbits
|
||||||
uv run lnbits
|
poetry run lnbits
|
||||||
|
|||||||
+6
-11
@@ -4,10 +4,9 @@ import importlib
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
import time
|
|
||||||
from collections.abc import Callable
|
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Callable, Optional
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
@@ -27,7 +26,7 @@ from lnbits.core.crud.extensions import create_installed_extension
|
|||||||
from lnbits.core.helpers import migrate_extension_database
|
from lnbits.core.helpers import migrate_extension_database
|
||||||
from lnbits.core.models.notifications import NotificationType
|
from lnbits.core.models.notifications import NotificationType
|
||||||
from lnbits.core.services.extensions import deactivate_extension, get_valid_extensions
|
from lnbits.core.services.extensions import deactivate_extension, get_valid_extensions
|
||||||
from lnbits.core.services.notifications import enqueue_admin_notification
|
from lnbits.core.services.notifications import enqueue_notification
|
||||||
from lnbits.core.services.payments import check_pending_payments
|
from lnbits.core.services.payments import check_pending_payments
|
||||||
from lnbits.core.tasks import (
|
from lnbits.core.tasks import (
|
||||||
audit_queue,
|
audit_queue,
|
||||||
@@ -71,9 +70,8 @@ from .tasks import internal_invoice_listener, invoice_listener, run_interval
|
|||||||
|
|
||||||
|
|
||||||
async def startup(app: FastAPI):
|
async def startup(app: FastAPI):
|
||||||
logger.info(f"Starting LNbits Version: {settings.version}")
|
|
||||||
start = time.perf_counter()
|
|
||||||
settings.lnbits_running = True
|
settings.lnbits_running = True
|
||||||
|
|
||||||
# wait till migration is done
|
# wait till migration is done
|
||||||
await migrate_databases()
|
await migrate_databases()
|
||||||
|
|
||||||
@@ -104,7 +102,7 @@ async def startup(app: FastAPI):
|
|||||||
# initialize tasks
|
# initialize tasks
|
||||||
register_async_tasks()
|
register_async_tasks()
|
||||||
|
|
||||||
enqueue_admin_notification(
|
enqueue_notification(
|
||||||
NotificationType.server_start_stop,
|
NotificationType.server_start_stop,
|
||||||
{
|
{
|
||||||
"message": "LNbits server started.",
|
"message": "LNbits server started.",
|
||||||
@@ -112,13 +110,10 @@ async def startup(app: FastAPI):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
end = time.perf_counter()
|
|
||||||
logger.success(f"LNbits started in {end - start:.2f} seconds.")
|
|
||||||
|
|
||||||
|
|
||||||
async def shutdown():
|
async def shutdown():
|
||||||
logger.warning("LNbits shutting down...")
|
logger.warning("LNbits shutting down...")
|
||||||
enqueue_admin_notification(
|
enqueue_notification(
|
||||||
NotificationType.server_start_stop,
|
NotificationType.server_start_stop,
|
||||||
{
|
{
|
||||||
"message": "LNbits server shutting down...",
|
"message": "LNbits server shutting down...",
|
||||||
@@ -280,7 +275,7 @@ async def check_installed_extensions(app: FastAPI):
|
|||||||
|
|
||||||
|
|
||||||
async def build_all_installed_extensions_list( # noqa: C901
|
async def build_all_installed_extensions_list( # noqa: C901
|
||||||
include_deactivated: bool | None = True,
|
include_deactivated: Optional[bool] = True,
|
||||||
) -> list[InstallableExtension]:
|
) -> list[InstallableExtension]:
|
||||||
"""
|
"""
|
||||||
Returns a list of all the installed extensions plus the extensions that
|
Returns a list of all the installed extensions plus the extensions that
|
||||||
|
|||||||
+33
-32
@@ -5,6 +5,7 @@ import time
|
|||||||
from functools import wraps
|
from functools import wraps
|
||||||
from getpass import getpass
|
from getpass import getpass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import click
|
import click
|
||||||
@@ -95,7 +96,7 @@ def decrypt():
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def get_super_user() -> str | None:
|
def get_super_user() -> Optional[str]:
|
||||||
"""Get the superuser"""
|
"""Get the superuser"""
|
||||||
superuser_file = Path(settings.lnbits_data_folder, ".super_user")
|
superuser_file = Path(settings.lnbits_data_folder, ".super_user")
|
||||||
if not superuser_file.exists() or not superuser_file.is_file():
|
if not superuser_file.exists() or not superuser_file.is_file():
|
||||||
@@ -154,7 +155,7 @@ async def db_versions():
|
|||||||
@db.command("cleanup-wallets")
|
@db.command("cleanup-wallets")
|
||||||
@click.argument("days", type=int, required=False)
|
@click.argument("days", type=int, required=False)
|
||||||
@coro
|
@coro
|
||||||
async def database_cleanup_wallets(days: int | None = None):
|
async def database_cleanup_wallets(days: Optional[int] = None):
|
||||||
"""Delete all wallets that never had any transaction"""
|
"""Delete all wallets that never had any transaction"""
|
||||||
async with core_db.connect() as conn:
|
async with core_db.connect() as conn:
|
||||||
delta = days or settings.cleanup_wallets_days
|
delta = days or settings.cleanup_wallets_days
|
||||||
@@ -211,10 +212,10 @@ async def database_revert_payment(checking_id: str):
|
|||||||
@click.option("-v", "--verbose", is_flag=True, help="Detailed log.")
|
@click.option("-v", "--verbose", is_flag=True, help="Detailed log.")
|
||||||
@coro
|
@coro
|
||||||
async def check_invalid_payments(
|
async def check_invalid_payments(
|
||||||
days: int | None = None,
|
days: Optional[int] = None,
|
||||||
limit: int | None = None,
|
limit: Optional[int] = None,
|
||||||
wallet: str | None = None,
|
wallet: Optional[str] = None,
|
||||||
verbose: bool | None = False,
|
verbose: Optional[bool] = False,
|
||||||
):
|
):
|
||||||
"""Check payments that are settled in the DB but pending on the Funding Source"""
|
"""Check payments that are settled in the DB but pending on the Funding Source"""
|
||||||
await check_admin_settings()
|
await check_admin_settings()
|
||||||
@@ -302,7 +303,7 @@ async def create_user(username: str, password: str):
|
|||||||
@users.command("cleanup-accounts")
|
@users.command("cleanup-accounts")
|
||||||
@click.argument("days", type=int, required=False)
|
@click.argument("days", type=int, required=False)
|
||||||
@coro
|
@coro
|
||||||
async def database_cleanup_accounts(days: int | None = None):
|
async def database_cleanup_accounts(days: Optional[int] = None):
|
||||||
"""Delete all accounts that have no wallets"""
|
"""Delete all accounts that have no wallets"""
|
||||||
async with core_db.connect() as conn:
|
async with core_db.connect() as conn:
|
||||||
delta = days or settings.cleanup_wallets_days
|
delta = days or settings.cleanup_wallets_days
|
||||||
@@ -352,12 +353,12 @@ async def extensions_list():
|
|||||||
)
|
)
|
||||||
@coro
|
@coro
|
||||||
async def extensions_update( # noqa: C901
|
async def extensions_update( # noqa: C901
|
||||||
extension: str | None = None,
|
extension: Optional[str] = None,
|
||||||
all_extensions: bool | None = False,
|
all_extensions: Optional[bool] = False,
|
||||||
repo_index: str | None = None,
|
repo_index: Optional[str] = None,
|
||||||
source_repo: str | None = None,
|
source_repo: Optional[str] = None,
|
||||||
url: str | None = None,
|
url: Optional[str] = None,
|
||||||
admin_user: str | None = None,
|
admin_user: Optional[str] = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Update extension to the latest version.
|
Update extension to the latest version.
|
||||||
@@ -442,10 +443,10 @@ async def extensions_update( # noqa: C901
|
|||||||
@coro
|
@coro
|
||||||
async def extensions_install(
|
async def extensions_install(
|
||||||
extension: str,
|
extension: str,
|
||||||
repo_index: str | None = None,
|
repo_index: Optional[str] = None,
|
||||||
source_repo: str | None = None,
|
source_repo: Optional[str] = None,
|
||||||
url: str | None = None,
|
url: Optional[str] = None,
|
||||||
admin_user: str | None = None,
|
admin_user: Optional[str] = None,
|
||||||
):
|
):
|
||||||
"""Install a extension"""
|
"""Install a extension"""
|
||||||
click.echo(f"Installing {extension}... {repo_index}")
|
click.echo(f"Installing {extension}... {repo_index}")
|
||||||
@@ -472,7 +473,7 @@ async def extensions_install(
|
|||||||
)
|
)
|
||||||
@coro
|
@coro
|
||||||
async def extensions_uninstall(
|
async def extensions_uninstall(
|
||||||
extension: str, url: str | None = None, admin_user: str | None = None
|
extension: str, url: Optional[str] = None, admin_user: Optional[str] = None
|
||||||
):
|
):
|
||||||
"""Uninstall a extension"""
|
"""Uninstall a extension"""
|
||||||
click.echo(f"Uninstalling '{extension}'...")
|
click.echo(f"Uninstalling '{extension}'...")
|
||||||
@@ -561,10 +562,10 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
async def install_extension(
|
async def install_extension(
|
||||||
extension: str,
|
extension: str,
|
||||||
repo_index: str | None = None,
|
repo_index: Optional[str] = None,
|
||||||
source_repo: str | None = None,
|
source_repo: Optional[str] = None,
|
||||||
url: str | None = None,
|
url: Optional[str] = None,
|
||||||
admin_user: str | None = None,
|
admin_user: Optional[str] = None,
|
||||||
) -> tuple[bool, str]:
|
) -> tuple[bool, str]:
|
||||||
try:
|
try:
|
||||||
release = await _select_release(extension, repo_index, source_repo)
|
release = await _select_release(extension, repo_index, source_repo)
|
||||||
@@ -590,10 +591,10 @@ async def install_extension(
|
|||||||
|
|
||||||
async def update_extension(
|
async def update_extension(
|
||||||
extension: str,
|
extension: str,
|
||||||
repo_index: str | None = None,
|
repo_index: Optional[str] = None,
|
||||||
source_repo: str | None = None,
|
source_repo: Optional[str] = None,
|
||||||
url: str | None = None,
|
url: Optional[str] = None,
|
||||||
admin_user: str | None = None,
|
admin_user: Optional[str] = None,
|
||||||
) -> tuple[bool, str]:
|
) -> tuple[bool, str]:
|
||||||
try:
|
try:
|
||||||
click.echo(f"Updating '{extension}' extension.")
|
click.echo(f"Updating '{extension}' extension.")
|
||||||
@@ -643,9 +644,9 @@ async def update_extension(
|
|||||||
|
|
||||||
async def _select_release(
|
async def _select_release(
|
||||||
extension: str,
|
extension: str,
|
||||||
repo_index: str | None = None,
|
repo_index: Optional[str] = None,
|
||||||
source_repo: str | None = None,
|
source_repo: Optional[str] = None,
|
||||||
) -> ExtensionRelease | None:
|
) -> Optional[ExtensionRelease]:
|
||||||
all_releases = await InstallableExtension.get_extension_releases(extension)
|
all_releases = await InstallableExtension.get_extension_releases(extension)
|
||||||
if len(all_releases) == 0:
|
if len(all_releases) == 0:
|
||||||
click.echo(f"No repository found for extension '{extension}'.")
|
click.echo(f"No repository found for extension '{extension}'.")
|
||||||
@@ -705,7 +706,7 @@ def _get_latest_release_per_repo(all_releases):
|
|||||||
|
|
||||||
|
|
||||||
async def _call_install_extension(
|
async def _call_install_extension(
|
||||||
data: CreateExtension, url: str | None, user_id: str | None = None
|
data: CreateExtension, url: Optional[str], user_id: Optional[str] = None
|
||||||
):
|
):
|
||||||
if url:
|
if url:
|
||||||
user_id = user_id or get_super_user()
|
user_id = user_id or get_super_user()
|
||||||
@@ -719,7 +720,7 @@ async def _call_install_extension(
|
|||||||
|
|
||||||
|
|
||||||
async def _call_uninstall_extension(
|
async def _call_uninstall_extension(
|
||||||
extension: str, url: str | None, user_id: str | None = None
|
extension: str, url: Optional[str], user_id: Optional[str] = None
|
||||||
):
|
):
|
||||||
if url:
|
if url:
|
||||||
user_id = user_id or get_super_user()
|
user_id = user_id or get_super_user()
|
||||||
@@ -755,7 +756,7 @@ async def _can_run_operation(url) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def _is_lnbits_started(url: str | None):
|
async def _is_lnbits_started(url: Optional[str]):
|
||||||
try:
|
try:
|
||||||
url = url or f"http://{settings.host}:{settings.port}/api/v1/health"
|
url = url or f"http://{settings.host}:{settings.port}/api/v1/health"
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ from .views.fiat_api import fiat_router
|
|||||||
|
|
||||||
# this compat is needed for usermanager extension
|
# this compat is needed for usermanager extension
|
||||||
from .views.generic import generic_router
|
from .views.generic import generic_router
|
||||||
from .views.lnurl_api import lnurl_router
|
|
||||||
from .views.node_api import node_router, public_node_router, super_node_router
|
from .views.node_api import node_router, public_node_router, super_node_router
|
||||||
from .views.payment_api import payment_router
|
from .views.payment_api import payment_router
|
||||||
from .views.tinyurl_api import tinyurl_router
|
from .views.tinyurl_api import tinyurl_router
|
||||||
@@ -43,7 +42,6 @@ def init_core_routers(app: FastAPI):
|
|||||||
app.include_router(users_router)
|
app.include_router(users_router)
|
||||||
app.include_router(audit_router)
|
app.include_router(audit_router)
|
||||||
app.include_router(fiat_router)
|
app.include_router(fiat_router)
|
||||||
app.include_router(lnurl_router)
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["core_app", "core_app_extra", "db"]
|
__all__ = ["core_app", "core_app_extra", "db"]
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
from lnbits.core.db import db
|
from lnbits.core.db import db
|
||||||
from lnbits.core.models import AuditEntry, AuditFilters
|
from lnbits.core.models import AuditEntry, AuditFilters
|
||||||
from lnbits.core.models.audit import AuditCountStat
|
from lnbits.core.models.audit import AuditCountStat
|
||||||
@@ -6,14 +8,14 @@ from lnbits.db import Connection, Filters, Page
|
|||||||
|
|
||||||
async def create_audit_entry(
|
async def create_audit_entry(
|
||||||
entry: AuditEntry,
|
entry: AuditEntry,
|
||||||
conn: Connection | None = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
await (conn or db).insert("audit", entry)
|
await (conn or db).insert("audit", entry)
|
||||||
|
|
||||||
|
|
||||||
async def get_audit_entries(
|
async def get_audit_entries(
|
||||||
filters: Filters[AuditFilters] | None = None,
|
filters: Optional[Filters[AuditFilters]] = None,
|
||||||
conn: Connection | None = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> Page[AuditEntry]:
|
) -> Page[AuditEntry]:
|
||||||
return await (conn or db).fetch_page(
|
return await (conn or db).fetch_page(
|
||||||
"SELECT * from audit",
|
"SELECT * from audit",
|
||||||
@@ -25,7 +27,7 @@ async def get_audit_entries(
|
|||||||
|
|
||||||
|
|
||||||
async def delete_expired_audit_entries(
|
async def delete_expired_audit_entries(
|
||||||
conn: Connection | None = None,
|
conn: Optional[Connection] = None,
|
||||||
):
|
):
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
# Timestamp placeholder is safe from SQL injection (not user input)
|
# Timestamp placeholder is safe from SQL injection (not user input)
|
||||||
@@ -38,8 +40,8 @@ async def delete_expired_audit_entries(
|
|||||||
|
|
||||||
async def get_count_stats(
|
async def get_count_stats(
|
||||||
field: str,
|
field: str,
|
||||||
filters: Filters[AuditFilters] | None = None,
|
filters: Optional[Filters[AuditFilters]] = None,
|
||||||
conn: Connection | None = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> list[AuditCountStat]:
|
) -> list[AuditCountStat]:
|
||||||
if field not in ["request_method", "component", "response_code"]:
|
if field not in ["request_method", "component", "response_code"]:
|
||||||
return []
|
return []
|
||||||
@@ -65,8 +67,8 @@ async def get_count_stats(
|
|||||||
|
|
||||||
|
|
||||||
async def get_long_duration_stats(
|
async def get_long_duration_stats(
|
||||||
filters: Filters[AuditFilters] | None = None,
|
filters: Optional[Filters[AuditFilters]] = None,
|
||||||
conn: Connection | None = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> list[AuditCountStat]:
|
) -> list[AuditCountStat]:
|
||||||
if not filters:
|
if not filters:
|
||||||
filters = Filters()
|
filters = Filters()
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
from lnbits.core.db import db
|
from lnbits.core.db import db
|
||||||
from lnbits.db import Connection
|
from lnbits.db import Connection
|
||||||
|
|
||||||
@@ -5,8 +7,8 @@ from ..models import DbVersion
|
|||||||
|
|
||||||
|
|
||||||
async def get_db_version(
|
async def get_db_version(
|
||||||
ext_id: str, conn: Connection | None = None
|
ext_id: str, conn: Optional[Connection] = None
|
||||||
) -> DbVersion | None:
|
) -> Optional[DbVersion]:
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"SELECT * FROM dbversions WHERE db = :ext_id",
|
"SELECT * FROM dbversions WHERE db = :ext_id",
|
||||||
{"ext_id": ext_id},
|
{"ext_id": ext_id},
|
||||||
@@ -14,7 +16,7 @@ async def get_db_version(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_db_versions(conn: Connection | None = None) -> list[DbVersion]:
|
async def get_db_versions(conn: Optional[Connection] = None) -> list[DbVersion]:
|
||||||
return await (conn or db).fetchall("SELECT * FROM dbversions", model=DbVersion)
|
return await (conn or db).fetchall("SELECT * FROM dbversions", model=DbVersion)
|
||||||
|
|
||||||
|
|
||||||
@@ -28,7 +30,7 @@ async def update_migration_version(conn, db_name, version):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def delete_dbversion(*, ext_id: str, conn: Connection | None = None) -> None:
|
async def delete_dbversion(*, ext_id: str, conn: Optional[Connection] = None) -> None:
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
"""
|
"""
|
||||||
DELETE FROM dbversions WHERE db = :ext
|
DELETE FROM dbversions WHERE db = :ext
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
from lnbits.core.db import db
|
from lnbits.core.db import db
|
||||||
from lnbits.core.models.extensions import (
|
from lnbits.core.models.extensions import (
|
||||||
InstallableExtension,
|
InstallableExtension,
|
||||||
@@ -8,20 +10,20 @@ from lnbits.db import Connection, Database
|
|||||||
|
|
||||||
async def create_installed_extension(
|
async def create_installed_extension(
|
||||||
ext: InstallableExtension,
|
ext: InstallableExtension,
|
||||||
conn: Connection | None = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
await (conn or db).insert("installed_extensions", ext)
|
await (conn or db).insert("installed_extensions", ext)
|
||||||
|
|
||||||
|
|
||||||
async def update_installed_extension(
|
async def update_installed_extension(
|
||||||
ext: InstallableExtension,
|
ext: InstallableExtension,
|
||||||
conn: Connection | None = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
await (conn or db).update("installed_extensions", ext)
|
await (conn or db).update("installed_extensions", ext)
|
||||||
|
|
||||||
|
|
||||||
async def update_installed_extension_state(
|
async def update_installed_extension_state(
|
||||||
*, ext_id: str, active: bool, conn: Connection | None = None
|
*, ext_id: str, active: bool, conn: Optional[Connection] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
"""
|
"""
|
||||||
@@ -32,7 +34,7 @@ async def update_installed_extension_state(
|
|||||||
|
|
||||||
|
|
||||||
async def delete_installed_extension(
|
async def delete_installed_extension(
|
||||||
*, ext_id: str, conn: Connection | None = None
|
*, ext_id: str, conn: Optional[Connection] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
"""
|
"""
|
||||||
@@ -42,7 +44,7 @@ async def delete_installed_extension(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def drop_extension_db(ext_id: str, conn: Connection | None = None) -> None:
|
async def drop_extension_db(ext_id: str, conn: Optional[Connection] = None) -> None:
|
||||||
row: dict = await (conn or db).fetchone(
|
row: dict = await (conn or db).fetchone(
|
||||||
"SELECT * FROM dbversions WHERE db = :id",
|
"SELECT * FROM dbversions WHERE db = :id",
|
||||||
{"id": ext_id},
|
{"id": ext_id},
|
||||||
@@ -63,8 +65,8 @@ async def drop_extension_db(ext_id: str, conn: Connection | None = None) -> None
|
|||||||
|
|
||||||
|
|
||||||
async def get_installed_extension(
|
async def get_installed_extension(
|
||||||
ext_id: str, conn: Connection | None = None
|
ext_id: str, conn: Optional[Connection] = None
|
||||||
) -> InstallableExtension | None:
|
) -> Optional[InstallableExtension]:
|
||||||
extension = await (conn or db).fetchone(
|
extension = await (conn or db).fetchone(
|
||||||
"SELECT * FROM installed_extensions WHERE id = :id",
|
"SELECT * FROM installed_extensions WHERE id = :id",
|
||||||
{"id": ext_id},
|
{"id": ext_id},
|
||||||
@@ -74,8 +76,8 @@ async def get_installed_extension(
|
|||||||
|
|
||||||
|
|
||||||
async def get_installed_extensions(
|
async def get_installed_extensions(
|
||||||
active: bool | None = None,
|
active: Optional[bool] = None,
|
||||||
conn: Connection | None = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> list[InstallableExtension]:
|
) -> list[InstallableExtension]:
|
||||||
query = "SELECT * FROM installed_extensions"
|
query = "SELECT * FROM installed_extensions"
|
||||||
if active is not None:
|
if active is not None:
|
||||||
@@ -91,8 +93,8 @@ async def get_installed_extensions(
|
|||||||
|
|
||||||
|
|
||||||
async def get_user_extension(
|
async def get_user_extension(
|
||||||
user_id: str, extension: str, conn: Connection | None = None
|
user_id: str, extension: str, conn: Optional[Connection] = None
|
||||||
) -> UserExtension | None:
|
) -> Optional[UserExtension]:
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM extensions
|
SELECT * FROM extensions
|
||||||
@@ -104,7 +106,7 @@ async def get_user_extension(
|
|||||||
|
|
||||||
|
|
||||||
async def get_user_extensions(
|
async def get_user_extensions(
|
||||||
user_id: str, conn: Connection | None = None
|
user_id: str, conn: Optional[Connection] = None
|
||||||
) -> list[UserExtension]:
|
) -> list[UserExtension]:
|
||||||
return await (conn or db).fetchall(
|
return await (conn or db).fetchall(
|
||||||
"""SELECT * FROM extensions WHERE "user" = :user""",
|
"""SELECT * FROM extensions WHERE "user" = :user""",
|
||||||
@@ -114,20 +116,20 @@ async def get_user_extensions(
|
|||||||
|
|
||||||
|
|
||||||
async def create_user_extension(
|
async def create_user_extension(
|
||||||
user_extension: UserExtension, conn: Connection | None = None
|
user_extension: UserExtension, conn: Optional[Connection] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
await (conn or db).insert("extensions", user_extension)
|
await (conn or db).insert("extensions", user_extension)
|
||||||
|
|
||||||
|
|
||||||
async def update_user_extension(
|
async def update_user_extension(
|
||||||
user_extension: UserExtension, conn: Connection | None = None
|
user_extension: UserExtension, conn: Optional[Connection] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
where = """WHERE extension = :extension AND "user" = :user"""
|
where = """WHERE extension = :extension AND "user" = :user"""
|
||||||
await (conn or db).update("extensions", user_extension, where)
|
await (conn or db).update("extensions", user_extension, where)
|
||||||
|
|
||||||
|
|
||||||
async def get_user_active_extensions_ids(
|
async def get_user_active_extensions_ids(
|
||||||
user_id: str, conn: Connection | None = None
|
user_id: str, conn: Optional[Connection] = None
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
exts = await (conn or db).fetchall(
|
exts = await (conn or db).fetchall(
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from time import time
|
from time import time
|
||||||
from typing import Any
|
from typing import Any, Optional
|
||||||
|
|
||||||
from lnbits.core.crud.wallets import get_total_balance, get_wallet, get_wallets_ids
|
from lnbits.core.crud.wallets import get_total_balance, get_wallet, get_wallets_ids
|
||||||
from lnbits.core.db import db
|
from lnbits.core.db import db
|
||||||
@@ -19,11 +19,16 @@ from ..models import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def update_payment_extra():
|
async def update_payment_extra(
|
||||||
pass
|
payment: Payment, conn: Optional[Connection] = None
|
||||||
|
) -> Payment:
|
||||||
|
return await (conn or db).execute(
|
||||||
|
"UPDATE apipayments SET extra = :extra WHERE checking_id = :checking_id",
|
||||||
|
{"extra": payment.extra, "checking_id": payment.checking_id},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_payment(checking_id: str, conn: Connection | None = None) -> Payment:
|
async def get_payment(checking_id: str, conn: Optional[Connection] = None) -> Payment:
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"SELECT * FROM apipayments WHERE checking_id = :checking_id",
|
"SELECT * FROM apipayments WHERE checking_id = :checking_id",
|
||||||
{"checking_id": checking_id},
|
{"checking_id": checking_id},
|
||||||
@@ -33,10 +38,10 @@ async def get_payment(checking_id: str, conn: Connection | None = None) -> Payme
|
|||||||
|
|
||||||
async def get_standalone_payment(
|
async def get_standalone_payment(
|
||||||
checking_id_or_hash: str,
|
checking_id_or_hash: str,
|
||||||
conn: Connection | None = None,
|
conn: Optional[Connection] = None,
|
||||||
incoming: bool | None = False,
|
incoming: Optional[bool] = False,
|
||||||
wallet_id: str | None = None,
|
wallet_id: Optional[str] = None,
|
||||||
) -> Payment | None:
|
) -> Optional[Payment]:
|
||||||
clause: str = "checking_id = :checking_id OR payment_hash = :hash"
|
clause: str = "checking_id = :checking_id OR payment_hash = :hash"
|
||||||
values = {
|
values = {
|
||||||
"wallet_id": wallet_id,
|
"wallet_id": wallet_id,
|
||||||
@@ -64,8 +69,8 @@ async def get_standalone_payment(
|
|||||||
|
|
||||||
|
|
||||||
async def get_wallet_payment(
|
async def get_wallet_payment(
|
||||||
wallet_id: str, payment_hash: str, conn: Connection | None = None
|
wallet_id: str, payment_hash: str, conn: Optional[Connection] = None
|
||||||
) -> Payment | None:
|
) -> Optional[Payment]:
|
||||||
payment = await (conn or db).fetchone(
|
payment = await (conn or db).fetchone(
|
||||||
"""
|
"""
|
||||||
SELECT *
|
SELECT *
|
||||||
@@ -102,17 +107,17 @@ async def get_latest_payments_by_extension(
|
|||||||
|
|
||||||
async def get_payments_paginated( # noqa: C901
|
async def get_payments_paginated( # noqa: C901
|
||||||
*,
|
*,
|
||||||
wallet_id: str | None = None,
|
wallet_id: Optional[str] = None,
|
||||||
user_id: str | None = None,
|
user_id: Optional[str] = None,
|
||||||
complete: bool = False,
|
complete: bool = False,
|
||||||
pending: bool = False,
|
pending: bool = False,
|
||||||
failed: bool = False,
|
failed: bool = False,
|
||||||
outgoing: bool = False,
|
outgoing: bool = False,
|
||||||
incoming: bool = False,
|
incoming: bool = False,
|
||||||
since: int | None = None,
|
since: Optional[int] = None,
|
||||||
exclude_uncheckable: bool = False,
|
exclude_uncheckable: bool = False,
|
||||||
filters: Filters[PaymentFilters] | None = None,
|
filters: Optional[Filters[PaymentFilters]] = None,
|
||||||
conn: Connection | None = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> Page[Payment]:
|
) -> Page[Payment]:
|
||||||
"""
|
"""
|
||||||
Filters payments to be returned by:
|
Filters payments to be returned by:
|
||||||
@@ -176,17 +181,17 @@ async def get_payments_paginated( # noqa: C901
|
|||||||
|
|
||||||
async def get_payments(
|
async def get_payments(
|
||||||
*,
|
*,
|
||||||
wallet_id: str | None = None,
|
wallet_id: Optional[str] = None,
|
||||||
complete: bool = False,
|
complete: bool = False,
|
||||||
pending: bool = False,
|
pending: bool = False,
|
||||||
outgoing: bool = False,
|
outgoing: bool = False,
|
||||||
incoming: bool = False,
|
incoming: bool = False,
|
||||||
since: int | None = None,
|
since: Optional[int] = None,
|
||||||
exclude_uncheckable: bool = False,
|
exclude_uncheckable: bool = False,
|
||||||
filters: Filters[PaymentFilters] | None = None,
|
filters: Optional[Filters[PaymentFilters]] = None,
|
||||||
conn: Connection | None = None,
|
conn: Optional[Connection] = None,
|
||||||
limit: int | None = None,
|
limit: Optional[int] = None,
|
||||||
offset: int | None = None,
|
offset: Optional[int] = None,
|
||||||
) -> list[Payment]:
|
) -> list[Payment]:
|
||||||
"""
|
"""
|
||||||
Filters payments to be returned by complete | pending | outgoing | incoming.
|
Filters payments to be returned by complete | pending | outgoing | incoming.
|
||||||
@@ -230,7 +235,7 @@ async def get_payments_status_count() -> PaymentsStatusCount:
|
|||||||
|
|
||||||
|
|
||||||
async def delete_expired_invoices(
|
async def delete_expired_invoices(
|
||||||
conn: Connection | None = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
# first we delete all invoices older than one month
|
# first we delete all invoices older than one month
|
||||||
|
|
||||||
@@ -259,7 +264,7 @@ async def create_payment(
|
|||||||
checking_id: str,
|
checking_id: str,
|
||||||
data: CreatePayment,
|
data: CreatePayment,
|
||||||
status: PaymentState = PaymentState.PENDING,
|
status: PaymentState = PaymentState.PENDING,
|
||||||
conn: Connection | None = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> Payment:
|
) -> Payment:
|
||||||
# we don't allow the creation of the same invoice twice
|
# we don't allow the creation of the same invoice twice
|
||||||
# note: this can be removed if the db uniqueness constraints are set appropriately
|
# note: this can be removed if the db uniqueness constraints are set appropriately
|
||||||
@@ -290,7 +295,7 @@ async def create_payment(
|
|||||||
|
|
||||||
|
|
||||||
async def update_payment_checking_id(
|
async def update_payment_checking_id(
|
||||||
checking_id: str, new_checking_id: str, conn: Connection | None = None
|
checking_id: str, new_checking_id: str, conn: Optional[Connection] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
"UPDATE apipayments SET checking_id = :new_id WHERE checking_id = :old_id",
|
"UPDATE apipayments SET checking_id = :new_id WHERE checking_id = :old_id",
|
||||||
@@ -300,8 +305,8 @@ async def update_payment_checking_id(
|
|||||||
|
|
||||||
async def update_payment(
|
async def update_payment(
|
||||||
payment: Payment,
|
payment: Payment,
|
||||||
new_checking_id: str | None = None,
|
new_checking_id: Optional[str] = None,
|
||||||
conn: Connection | None = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
await (conn or db).update(
|
await (conn or db).update(
|
||||||
"apipayments", payment, "WHERE checking_id = :checking_id"
|
"apipayments", payment, "WHERE checking_id = :checking_id"
|
||||||
@@ -311,9 +316,9 @@ async def update_payment(
|
|||||||
|
|
||||||
|
|
||||||
async def get_payments_history(
|
async def get_payments_history(
|
||||||
wallet_id: str | None = None,
|
wallet_id: Optional[str] = None,
|
||||||
group: DateTrunc = "day",
|
group: DateTrunc = "day",
|
||||||
filters: Filters | None = None,
|
filters: Optional[Filters] = None,
|
||||||
) -> list[PaymentHistoryPoint]:
|
) -> list[PaymentHistoryPoint]:
|
||||||
if not filters:
|
if not filters:
|
||||||
filters = Filters()
|
filters = Filters()
|
||||||
@@ -376,11 +381,10 @@ async def get_payments_history(
|
|||||||
|
|
||||||
async def get_payment_count_stats(
|
async def get_payment_count_stats(
|
||||||
field: PaymentCountField,
|
field: PaymentCountField,
|
||||||
filters: Filters[PaymentFilters] | None = None,
|
filters: Optional[Filters[PaymentFilters]] = None,
|
||||||
user_id: str | None = None,
|
user_id: Optional[str] = None,
|
||||||
conn: Connection | None = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> list[PaymentCountStat]:
|
) -> list[PaymentCountStat]:
|
||||||
|
|
||||||
if not filters:
|
if not filters:
|
||||||
filters = Filters()
|
filters = Filters()
|
||||||
extra_stmts = []
|
extra_stmts = []
|
||||||
@@ -409,11 +413,10 @@ async def get_payment_count_stats(
|
|||||||
|
|
||||||
|
|
||||||
async def get_daily_stats(
|
async def get_daily_stats(
|
||||||
filters: Filters[PaymentFilters] | None = None,
|
filters: Optional[Filters[PaymentFilters]] = None,
|
||||||
user_id: str | None = None,
|
user_id: Optional[str] = None,
|
||||||
conn: Connection | None = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> tuple[list[PaymentDailyStats], list[PaymentDailyStats]]:
|
) -> tuple[list[PaymentDailyStats], list[PaymentDailyStats]]:
|
||||||
|
|
||||||
if not filters:
|
if not filters:
|
||||||
filters = Filters()
|
filters = Filters()
|
||||||
|
|
||||||
@@ -459,11 +462,10 @@ async def get_daily_stats(
|
|||||||
|
|
||||||
|
|
||||||
async def get_wallets_stats(
|
async def get_wallets_stats(
|
||||||
filters: Filters[PaymentFilters] | None = None,
|
filters: Optional[Filters[PaymentFilters]] = None,
|
||||||
user_id: str | None = None,
|
user_id: Optional[str] = None,
|
||||||
conn: Connection | None = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> list[PaymentWalletStats]:
|
) -> list[PaymentWalletStats]:
|
||||||
|
|
||||||
if not filters:
|
if not filters:
|
||||||
filters = Filters()
|
filters = Filters()
|
||||||
|
|
||||||
@@ -508,7 +510,7 @@ async def get_wallets_stats(
|
|||||||
|
|
||||||
|
|
||||||
async def delete_wallet_payment(
|
async def delete_wallet_payment(
|
||||||
checking_id: str, wallet_id: str, conn: Connection | None = None
|
checking_id: str, wallet_id: str, conn: Optional[Connection] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
"DELETE FROM apipayments WHERE checking_id = :checking_id AND wallet = :wallet",
|
"DELETE FROM apipayments WHERE checking_id = :checking_id AND wallet = :wallet",
|
||||||
@@ -517,8 +519,8 @@ async def delete_wallet_payment(
|
|||||||
|
|
||||||
|
|
||||||
async def check_internal(
|
async def check_internal(
|
||||||
payment_hash: str, conn: Connection | None = None
|
payment_hash: str, conn: Optional[Connection] = None
|
||||||
) -> Payment | None:
|
) -> Optional[Payment]:
|
||||||
"""
|
"""
|
||||||
Returns the checking_id of the internal payment if it exists,
|
Returns the checking_id of the internal payment if it exists,
|
||||||
otherwise None
|
otherwise None
|
||||||
@@ -534,7 +536,7 @@ async def check_internal(
|
|||||||
|
|
||||||
|
|
||||||
async def is_internal_status_success(
|
async def is_internal_status_success(
|
||||||
payment_hash: str, conn: Connection | None = None
|
payment_hash: str, conn: Optional[Connection] = None
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Returns True if the internal payment was found and is successful,
|
Returns True if the internal payment was found and is successful,
|
||||||
@@ -563,7 +565,7 @@ async def mark_webhook_sent(payment_hash: str, status: str) -> None:
|
|||||||
|
|
||||||
|
|
||||||
async def _only_user_wallets_statement(
|
async def _only_user_wallets_statement(
|
||||||
user_id: str, conn: Connection | None = None
|
user_id: str, conn: Optional[Connection] = None
|
||||||
) -> str:
|
) -> str:
|
||||||
wallet_ids = await get_wallets_ids(user_id=user_id, conn=conn) or [
|
wallet_ids = await get_wallets_ids(user_id=user_id, conn=conn) or [
|
||||||
"no-wallets-for-user"
|
"no-wallets-for-user"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import json
|
import json
|
||||||
from typing import Any
|
from typing import Any, Optional
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ from lnbits.settings import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_super_settings() -> SuperSettings | None:
|
async def get_super_settings() -> Optional[SuperSettings]:
|
||||||
data = await get_settings_by_tag("core")
|
data = await get_settings_by_tag("core")
|
||||||
if data:
|
if data:
|
||||||
super_user = await get_settings_field("super_user")
|
super_user = await get_settings_field("super_user")
|
||||||
@@ -24,7 +24,7 @@ async def get_super_settings() -> SuperSettings | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def get_admin_settings(is_super_user: bool = False) -> AdminSettings | None:
|
async def get_admin_settings(is_super_user: bool = False) -> Optional[AdminSettings]:
|
||||||
sets = await get_super_settings()
|
sets = await get_super_settings()
|
||||||
if not sets:
|
if not sets:
|
||||||
return None
|
return None
|
||||||
@@ -41,7 +41,7 @@ async def get_admin_settings(is_super_user: bool = False) -> AdminSettings | Non
|
|||||||
|
|
||||||
|
|
||||||
async def update_admin_settings(
|
async def update_admin_settings(
|
||||||
data: EditableSettings, tag: str | None = "core"
|
data: EditableSettings, tag: Optional[str] = "core"
|
||||||
) -> None:
|
) -> None:
|
||||||
editable_settings = await get_settings_by_tag("core") or {}
|
editable_settings = await get_settings_by_tag("core") or {}
|
||||||
editable_settings.update(data.dict(exclude_unset=True))
|
editable_settings.update(data.dict(exclude_unset=True))
|
||||||
@@ -61,7 +61,7 @@ async def update_super_user(super_user: str) -> SuperSettings:
|
|||||||
return settings
|
return settings
|
||||||
|
|
||||||
|
|
||||||
async def delete_admin_settings(tag: str | None = "core") -> None:
|
async def delete_admin_settings(tag: Optional[str] = "core") -> None:
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"DELETE FROM system_settings WHERE tag = :tag",
|
"DELETE FROM system_settings WHERE tag = :tag",
|
||||||
{"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(
|
async def get_settings_field(
|
||||||
id_: str, tag: str | None = "core"
|
id_: str, tag: Optional[str] = "core"
|
||||||
) -> SettingsField | None:
|
) -> Optional[SettingsField]:
|
||||||
|
|
||||||
row: dict = await db.fetchone(
|
row: dict = await db.fetchone(
|
||||||
"""
|
"""
|
||||||
@@ -108,7 +108,9 @@ async def get_settings_field(
|
|||||||
return SettingsField(id=row["id"], value=json.loads(row["value"]), tag=row["tag"])
|
return SettingsField(id=row["id"], value=json.loads(row["value"]), tag=row["tag"])
|
||||||
|
|
||||||
|
|
||||||
async def set_settings_field(id_: str, value: Any | None, tag: str | None = "core"):
|
async def set_settings_field(
|
||||||
|
id_: str, value: Optional[Any], tag: Optional[str] = "core"
|
||||||
|
):
|
||||||
value = json.dumps(value) if value is not None else None
|
value = json.dumps(value) if value is not None else None
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
@@ -120,7 +122,7 @@ async def set_settings_field(id_: str, value: Any | None, tag: str | None = "cor
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_settings_by_tag(tag: str) -> dict[str, Any] | None:
|
async def get_settings_by_tag(tag: str) -> Optional[dict[str, Any]]:
|
||||||
rows: list[dict] = await db.fetchall(
|
rows: list[dict] = await db.fetchall(
|
||||||
"SELECT * FROM system_settings WHERE tag = :tag", {"tag": tag}
|
"SELECT * FROM system_settings WHERE tag = :tag", {"tag": tag}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
import shortuuid
|
import shortuuid
|
||||||
|
|
||||||
from lnbits.core.db import db
|
from lnbits.core.db import db
|
||||||
@@ -17,7 +19,7 @@ async def create_tinyurl(domain: str, endless: bool, wallet: str):
|
|||||||
return await get_tinyurl(tinyurl_id)
|
return await get_tinyurl(tinyurl_id)
|
||||||
|
|
||||||
|
|
||||||
async def get_tinyurl(tinyurl_id: str) -> TinyURL | None:
|
async def get_tinyurl(tinyurl_id: str) -> Optional[TinyURL]:
|
||||||
return await db.fetchone(
|
return await db.fetchone(
|
||||||
"SELECT * FROM tiny_url WHERE id = :tinyurl",
|
"SELECT * FROM tiny_url WHERE id = :tinyurl",
|
||||||
{"tinyurl": tinyurl_id},
|
{"tinyurl": tinyurl_id},
|
||||||
|
|||||||
+22
-20
@@ -1,6 +1,6 @@
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from time import time
|
from time import time
|
||||||
from typing import Any
|
from typing import Any, Optional
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from lnbits.core.crud.extensions import get_user_active_extensions_ids
|
from lnbits.core.crud.extensions import get_user_active_extensions_ids
|
||||||
@@ -18,8 +18,8 @@ from ..models import (
|
|||||||
|
|
||||||
|
|
||||||
async def create_account(
|
async def create_account(
|
||||||
account: Account | None = None,
|
account: Optional[Account] = None,
|
||||||
conn: Connection | None = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> Account:
|
) -> Account:
|
||||||
if account:
|
if account:
|
||||||
account.validate_fields()
|
account.validate_fields()
|
||||||
@@ -36,7 +36,7 @@ async def update_account(account: Account) -> Account:
|
|||||||
return account
|
return account
|
||||||
|
|
||||||
|
|
||||||
async def delete_account(user_id: str, conn: Connection | None = None) -> None:
|
async def delete_account(user_id: str, conn: Optional[Connection] = None) -> None:
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
"DELETE from accounts WHERE id = :user",
|
"DELETE from accounts WHERE id = :user",
|
||||||
{"user": user_id},
|
{"user": user_id},
|
||||||
@@ -44,8 +44,8 @@ async def delete_account(user_id: str, conn: Connection | None = None) -> None:
|
|||||||
|
|
||||||
|
|
||||||
async def get_accounts(
|
async def get_accounts(
|
||||||
filters: Filters[AccountFilters] | None = None,
|
filters: Optional[Filters[AccountFilters]] = None,
|
||||||
conn: Connection | None = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> Page[AccountOverview]:
|
) -> Page[AccountOverview]:
|
||||||
where_clauses = []
|
where_clauses = []
|
||||||
values: dict[str, Any] = {}
|
values: dict[str, Any] = {}
|
||||||
@@ -92,7 +92,9 @@ async def get_accounts(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_account(user_id: str, conn: Connection | None = None) -> Account | None:
|
async def get_account(
|
||||||
|
user_id: str, conn: Optional[Connection] = None
|
||||||
|
) -> Optional[Account]:
|
||||||
if len(user_id) == 0:
|
if len(user_id) == 0:
|
||||||
return None
|
return None
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
@@ -104,7 +106,7 @@ async def get_account(user_id: str, conn: Connection | None = None) -> Account |
|
|||||||
|
|
||||||
async def delete_accounts_no_wallets(
|
async def delete_accounts_no_wallets(
|
||||||
time_delta: int,
|
time_delta: int,
|
||||||
conn: Connection | None = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
delta = int(time()) - time_delta
|
delta = int(time()) - time_delta
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
@@ -123,8 +125,8 @@ async def delete_accounts_no_wallets(
|
|||||||
|
|
||||||
|
|
||||||
async def get_account_by_username(
|
async def get_account_by_username(
|
||||||
username: str, conn: Connection | None = None
|
username: str, conn: Optional[Connection] = None
|
||||||
) -> Account | None:
|
) -> Optional[Account]:
|
||||||
if len(username) == 0:
|
if len(username) == 0:
|
||||||
return None
|
return None
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
@@ -135,8 +137,8 @@ async def get_account_by_username(
|
|||||||
|
|
||||||
|
|
||||||
async def get_account_by_pubkey(
|
async def get_account_by_pubkey(
|
||||||
pubkey: str, conn: Connection | None = None
|
pubkey: str, conn: Optional[Connection] = None
|
||||||
) -> Account | None:
|
) -> Optional[Account]:
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"SELECT * FROM accounts WHERE LOWER(pubkey) = :pubkey",
|
"SELECT * FROM accounts WHERE LOWER(pubkey) = :pubkey",
|
||||||
{"pubkey": pubkey.lower()},
|
{"pubkey": pubkey.lower()},
|
||||||
@@ -145,8 +147,8 @@ async def get_account_by_pubkey(
|
|||||||
|
|
||||||
|
|
||||||
async def get_account_by_email(
|
async def get_account_by_email(
|
||||||
email: str, conn: Connection | None = None
|
email: str, conn: Optional[Connection] = None
|
||||||
) -> Account | None:
|
) -> Optional[Account]:
|
||||||
if len(email) == 0:
|
if len(email) == 0:
|
||||||
return None
|
return None
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
@@ -157,8 +159,8 @@ async def get_account_by_email(
|
|||||||
|
|
||||||
|
|
||||||
async def get_account_by_username_or_email(
|
async def get_account_by_username_or_email(
|
||||||
username_or_email: str, conn: Connection | None = None
|
username_or_email: str, conn: Optional[Connection] = None
|
||||||
) -> Account | None:
|
) -> Optional[Account]:
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM accounts
|
SELECT * FROM accounts
|
||||||
@@ -169,7 +171,7 @@ async def get_account_by_username_or_email(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_user(user_id: str, conn: Connection | None = None) -> User | None:
|
async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[User]:
|
||||||
account = await get_account(user_id, conn)
|
account = await get_account(user_id, conn)
|
||||||
if not account:
|
if not account:
|
||||||
return None
|
return None
|
||||||
@@ -177,8 +179,8 @@ async def get_user(user_id: str, conn: Connection | None = None) -> User | None:
|
|||||||
|
|
||||||
|
|
||||||
async def get_user_from_account(
|
async def get_user_from_account(
|
||||||
account: Account, conn: Connection | None = None
|
account: Account, conn: Optional[Connection] = None
|
||||||
) -> User | None:
|
) -> Optional[User]:
|
||||||
extensions = await get_user_active_extensions_ids(account.id, conn)
|
extensions = await get_user_active_extensions_ids(account.id, conn)
|
||||||
wallets = await get_wallets(account.id, False, conn=conn)
|
wallets = await get_wallets(account.id, False, conn=conn)
|
||||||
return User(
|
return User(
|
||||||
@@ -205,7 +207,7 @@ async def update_user_access_control_list(user_acls: UserAcls):
|
|||||||
|
|
||||||
|
|
||||||
async def get_user_access_control_lists(
|
async def get_user_access_control_lists(
|
||||||
user_id: str, conn: Connection | None = None
|
user_id: str, conn: Optional[Connection] = None
|
||||||
) -> UserAcls:
|
) -> UserAcls:
|
||||||
user_acls = await (conn or db).fetchone(
|
user_acls = await (conn or db).fetchone(
|
||||||
"SELECT id, access_control_list FROM accounts WHERE id = :id",
|
"SELECT id, access_control_list FROM accounts WHERE id = :id",
|
||||||
|
|||||||
+23
-20
@@ -1,5 +1,6 @@
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from time import time
|
from time import time
|
||||||
|
from typing import Optional
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from lnbits.core.db import db
|
from lnbits.core.db import db
|
||||||
@@ -13,8 +14,8 @@ from ..models import Wallet
|
|||||||
async def create_wallet(
|
async def create_wallet(
|
||||||
*,
|
*,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
wallet_name: str | None = None,
|
wallet_name: Optional[str] = None,
|
||||||
conn: Connection | None = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> Wallet:
|
) -> Wallet:
|
||||||
wallet_id = uuid4().hex
|
wallet_id = uuid4().hex
|
||||||
wallet = Wallet(
|
wallet = Wallet(
|
||||||
@@ -31,8 +32,8 @@ async def create_wallet(
|
|||||||
|
|
||||||
async def update_wallet(
|
async def update_wallet(
|
||||||
wallet: Wallet,
|
wallet: Wallet,
|
||||||
conn: Connection | None = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> Wallet:
|
) -> Optional[Wallet]:
|
||||||
wallet.updated_at = datetime.now(timezone.utc)
|
wallet.updated_at = datetime.now(timezone.utc)
|
||||||
await (conn or db).update("wallets", wallet)
|
await (conn or db).update("wallets", wallet)
|
||||||
return wallet
|
return wallet
|
||||||
@@ -43,7 +44,7 @@ async def delete_wallet(
|
|||||||
user_id: str,
|
user_id: str,
|
||||||
wallet_id: str,
|
wallet_id: str,
|
||||||
deleted: bool = True,
|
deleted: bool = True,
|
||||||
conn: Connection | None = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
now = int(time())
|
now = int(time())
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
@@ -57,7 +58,9 @@ async def delete_wallet(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def force_delete_wallet(wallet_id: str, conn: Connection | None = None) -> None:
|
async def force_delete_wallet(
|
||||||
|
wallet_id: str, conn: Optional[Connection] = None
|
||||||
|
) -> None:
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
"DELETE FROM wallets WHERE id = :wallet",
|
"DELETE FROM wallets WHERE id = :wallet",
|
||||||
{"wallet": wallet_id},
|
{"wallet": wallet_id},
|
||||||
@@ -65,8 +68,8 @@ async def force_delete_wallet(wallet_id: str, conn: Connection | None = None) ->
|
|||||||
|
|
||||||
|
|
||||||
async def delete_wallet_by_id(
|
async def delete_wallet_by_id(
|
||||||
wallet_id: str, conn: Connection | None = None
|
wallet_id: str, conn: Optional[Connection] = None
|
||||||
) -> int | None:
|
) -> Optional[int]:
|
||||||
now = int(time())
|
now = int(time())
|
||||||
result = await (conn or db).execute(
|
result = await (conn or db).execute(
|
||||||
# Timestamp placeholder is safe from SQL injection (not user input)
|
# Timestamp placeholder is safe from SQL injection (not user input)
|
||||||
@@ -80,13 +83,13 @@ async def delete_wallet_by_id(
|
|||||||
return result.rowcount
|
return result.rowcount
|
||||||
|
|
||||||
|
|
||||||
async def remove_deleted_wallets(conn: Connection | None = None) -> None:
|
async def remove_deleted_wallets(conn: Optional[Connection] = None) -> None:
|
||||||
await (conn or db).execute("DELETE FROM wallets WHERE deleted = true")
|
await (conn or db).execute("DELETE FROM wallets WHERE deleted = true")
|
||||||
|
|
||||||
|
|
||||||
async def delete_unused_wallets(
|
async def delete_unused_wallets(
|
||||||
time_delta: int,
|
time_delta: int,
|
||||||
conn: Connection | None = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
delta = int(time()) - time_delta
|
delta = int(time()) - time_delta
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
@@ -104,8 +107,8 @@ async def delete_unused_wallets(
|
|||||||
|
|
||||||
|
|
||||||
async def get_wallet(
|
async def get_wallet(
|
||||||
wallet_id: str, deleted: bool | None = None, conn: Connection | None = None
|
wallet_id: str, deleted: Optional[bool] = None, conn: Optional[Connection] = None
|
||||||
) -> Wallet | None:
|
) -> Optional[Wallet]:
|
||||||
query = """
|
query = """
|
||||||
SELECT *, COALESCE((
|
SELECT *, COALESCE((
|
||||||
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
||||||
@@ -122,7 +125,7 @@ async def get_wallet(
|
|||||||
|
|
||||||
|
|
||||||
async def get_wallets(
|
async def get_wallets(
|
||||||
user_id: str, deleted: bool | None = None, conn: Connection | None = None
|
user_id: str, deleted: Optional[bool] = None, conn: Optional[Connection] = None
|
||||||
) -> list[Wallet]:
|
) -> list[Wallet]:
|
||||||
query = """
|
query = """
|
||||||
SELECT *, COALESCE((
|
SELECT *, COALESCE((
|
||||||
@@ -141,9 +144,9 @@ async def get_wallets(
|
|||||||
|
|
||||||
async def get_wallets_paginated(
|
async def get_wallets_paginated(
|
||||||
user_id: str,
|
user_id: str,
|
||||||
deleted: bool | None = None,
|
deleted: Optional[bool] = None,
|
||||||
filters: Filters[WalletsFilters] | None = None,
|
filters: Optional[Filters[WalletsFilters]] = None,
|
||||||
conn: Connection | None = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> Page[Wallet]:
|
) -> Page[Wallet]:
|
||||||
if deleted is None:
|
if deleted is None:
|
||||||
deleted = False
|
deleted = False
|
||||||
@@ -163,7 +166,7 @@ async def get_wallets_paginated(
|
|||||||
|
|
||||||
|
|
||||||
async def get_wallets_ids(
|
async def get_wallets_ids(
|
||||||
user_id: str, deleted: bool | None = None, conn: Connection | None = None
|
user_id: str, deleted: Optional[bool] = None, conn: Optional[Connection] = None
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
query = """SELECT id FROM wallets WHERE "user" = :user"""
|
query = """SELECT id FROM wallets WHERE "user" = :user"""
|
||||||
if deleted is not None:
|
if deleted is not None:
|
||||||
@@ -183,8 +186,8 @@ async def get_wallets_count():
|
|||||||
|
|
||||||
async def get_wallet_for_key(
|
async def get_wallet_for_key(
|
||||||
key: str,
|
key: str,
|
||||||
conn: Connection | None = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> Wallet | None:
|
) -> Optional[Wallet]:
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"""
|
"""
|
||||||
SELECT *, COALESCE((
|
SELECT *, COALESCE((
|
||||||
@@ -198,7 +201,7 @@ async def get_wallet_for_key(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_total_balance(conn: Connection | None = None):
|
async def get_total_balance(conn: Optional[Connection] = None):
|
||||||
result = await (conn or db).execute("SELECT SUM(balance) as balance FROM balances")
|
result = await (conn or db).execute("SELECT SUM(balance) as balance FROM balances")
|
||||||
row = result.mappings().first()
|
row = result.mappings().first()
|
||||||
return row.get("balance", 0) or 0
|
return row.get("balance", 0) or 0
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
from lnbits.core.db import db
|
from lnbits.core.db import db
|
||||||
|
|
||||||
from ..models import WebPushSubscription
|
from ..models import WebPushSubscription
|
||||||
@@ -5,7 +7,7 @@ from ..models import WebPushSubscription
|
|||||||
|
|
||||||
async def get_webpush_subscription(
|
async def get_webpush_subscription(
|
||||||
endpoint: str, user: str
|
endpoint: str, user: str
|
||||||
) -> WebPushSubscription | None:
|
) -> Optional[WebPushSubscription]:
|
||||||
return await db.fetchone(
|
return await db.fetchone(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM webpush_subscriptions
|
SELECT * FROM webpush_subscriptions
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import importlib
|
import importlib
|
||||||
import re
|
import re
|
||||||
from typing import Any
|
from typing import Any, Optional
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ from lnbits.settings import settings
|
|||||||
|
|
||||||
|
|
||||||
async def migrate_extension_database(
|
async def migrate_extension_database(
|
||||||
ext: InstallableExtension, current_version: DbVersion | None = None
|
ext: InstallableExtension, current_version: Optional[DbVersion] = None
|
||||||
):
|
):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -38,7 +38,7 @@ async def run_migration(
|
|||||||
db: Connection,
|
db: Connection,
|
||||||
migrations_module: Any,
|
migrations_module: Any,
|
||||||
db_name: str,
|
db_name: str,
|
||||||
current_version: DbVersion | None = None,
|
current_version: Optional[DbVersion] = None,
|
||||||
):
|
):
|
||||||
matcher = re.compile(r"^m(\d\d\d)_")
|
matcher = re.compile(r"^m(\d\d\d)_")
|
||||||
|
|
||||||
|
|||||||
@@ -735,11 +735,3 @@ async def m032_add_external_id_to_accounts(db: Connection):
|
|||||||
|
|
||||||
async def m033_update_payment_table(db: Connection):
|
async def m033_update_payment_table(db: Connection):
|
||||||
await db.execute("ALTER TABLE apipayments ADD COLUMN fiat_provider TEXT")
|
await db.execute("ALTER TABLE apipayments ADD COLUMN fiat_provider TEXT")
|
||||||
|
|
||||||
|
|
||||||
async def m034_add_stored_paylinks_to_wallet(db: Connection):
|
|
||||||
await db.execute(
|
|
||||||
"""
|
|
||||||
ALTER TABLE wallets ADD COLUMN stored_paylinks TEXT
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from .audit import AuditEntry, AuditFilters
|
from .audit import AuditEntry, AuditFilters
|
||||||
from .lnurl import CreateLnurlPayment, CreateLnurlWithdraw
|
from .lnurl import CreateLnurl, CreateLnurlAuth, PayLnurlWData
|
||||||
from .misc import (
|
from .misc import (
|
||||||
BalanceDelta,
|
BalanceDelta,
|
||||||
Callback,
|
Callback,
|
||||||
@@ -9,7 +9,6 @@ from .misc import (
|
|||||||
SimpleStatus,
|
SimpleStatus,
|
||||||
)
|
)
|
||||||
from .payments import (
|
from .payments import (
|
||||||
CancelInvoice,
|
|
||||||
CreateInvoice,
|
CreateInvoice,
|
||||||
CreatePayment,
|
CreatePayment,
|
||||||
DecodePayment,
|
DecodePayment,
|
||||||
@@ -24,7 +23,6 @@ from .payments import (
|
|||||||
PaymentsStatusCount,
|
PaymentsStatusCount,
|
||||||
PaymentState,
|
PaymentState,
|
||||||
PaymentWalletStats,
|
PaymentWalletStats,
|
||||||
SettleInvoice,
|
|
||||||
)
|
)
|
||||||
from .tinyurl import TinyURL
|
from .tinyurl import TinyURL
|
||||||
from .users import (
|
from .users import (
|
||||||
@@ -59,12 +57,11 @@ __all__ = [
|
|||||||
"BalanceDelta",
|
"BalanceDelta",
|
||||||
"BaseWallet",
|
"BaseWallet",
|
||||||
"Callback",
|
"Callback",
|
||||||
"CancelInvoice",
|
|
||||||
"ConversionData",
|
"ConversionData",
|
||||||
"CoreAppExtra",
|
"CoreAppExtra",
|
||||||
"CreateInvoice",
|
"CreateInvoice",
|
||||||
"CreateLnurlPayment",
|
"CreateLnurl",
|
||||||
"CreateLnurlWithdraw",
|
"CreateLnurlAuth",
|
||||||
"CreatePayment",
|
"CreatePayment",
|
||||||
"CreateUser",
|
"CreateUser",
|
||||||
"CreateWallet",
|
"CreateWallet",
|
||||||
@@ -75,6 +72,7 @@ __all__ = [
|
|||||||
"LoginUsernamePassword",
|
"LoginUsernamePassword",
|
||||||
"LoginUsr",
|
"LoginUsr",
|
||||||
"PayInvoice",
|
"PayInvoice",
|
||||||
|
"PayLnurlWData",
|
||||||
"Payment",
|
"Payment",
|
||||||
"PaymentCountField",
|
"PaymentCountField",
|
||||||
"PaymentCountStat",
|
"PaymentCountStat",
|
||||||
@@ -87,7 +85,6 @@ __all__ = [
|
|||||||
"PaymentsStatusCount",
|
"PaymentsStatusCount",
|
||||||
"RegisterUser",
|
"RegisterUser",
|
||||||
"ResetUserPassword",
|
"ResetUserPassword",
|
||||||
"SettleInvoice",
|
|
||||||
"SimpleStatus",
|
"SimpleStatus",
|
||||||
"TinyURL",
|
"TinyURL",
|
||||||
"UpdateBalance",
|
"UpdateBalance",
|
||||||
|
|||||||
+13
-23
@@ -1,31 +1,21 @@
|
|||||||
from time import time
|
from typing import Optional
|
||||||
|
|
||||||
from lnurl import LnAddress, Lnurl, LnurlPayResponse
|
from pydantic import BaseModel
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
|
|
||||||
|
|
||||||
class CreateLnurlPayment(BaseModel):
|
class CreateLnurl(BaseModel):
|
||||||
res: LnurlPayResponse | None = None
|
description_hash: str
|
||||||
lnurl: Lnurl | LnAddress | None = None
|
callback: str
|
||||||
amount: int
|
amount: int
|
||||||
comment: str | None = None
|
comment: Optional[str] = None
|
||||||
unit: str | None = None
|
description: Optional[str] = None
|
||||||
internal_memo: str | None = None
|
unit: Optional[str] = None
|
||||||
|
internal_memo: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class CreateLnurlWithdraw(BaseModel):
|
class CreateLnurlAuth(BaseModel):
|
||||||
lnurl_w: Lnurl
|
callback: str
|
||||||
|
|
||||||
|
|
||||||
class LnurlScan(BaseModel):
|
class PayLnurlWData(BaseModel):
|
||||||
lnurl: Lnurl | LnAddress
|
lnurl_w: str
|
||||||
|
|
||||||
|
|
||||||
class StoredPayLink(BaseModel):
|
|
||||||
lnurl: str
|
|
||||||
label: str
|
|
||||||
last_used: int = Field(default_factory=lambda: int(time()))
|
|
||||||
|
|
||||||
|
|
||||||
class StoredPayLinks(BaseModel):
|
|
||||||
links: list[StoredPayLink] = []
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Callable
|
from typing import Callable
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ from enum import Enum
|
|||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from lnbits.core.models.users import UserNotifications
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationType(Enum):
|
class NotificationType(Enum):
|
||||||
server_status = "server_status"
|
server_status = "server_status"
|
||||||
@@ -20,7 +18,6 @@ class NotificationType(Enum):
|
|||||||
class NotificationMessage(BaseModel):
|
class NotificationMessage(BaseModel):
|
||||||
message_type: NotificationType
|
message_type: NotificationType
|
||||||
values: dict
|
values: dict
|
||||||
user_notifications: UserNotifications | None = None
|
|
||||||
|
|
||||||
|
|
||||||
NOTIFICATION_TEMPLATES = {
|
NOTIFICATION_TEMPLATES = {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ from enum import Enum
|
|||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
from fastapi import Query
|
from fastapi import Query
|
||||||
from lnurl import LnurlWithdrawResponse
|
|
||||||
from pydantic import BaseModel, Field, validator
|
from pydantic import BaseModel, Field, validator
|
||||||
|
|
||||||
from lnbits.db import FilterModel
|
from lnbits.db import FilterModel
|
||||||
@@ -253,25 +252,13 @@ class CreateInvoice(BaseModel):
|
|||||||
memo: str | None = Query(None, max_length=640)
|
memo: str | None = Query(None, max_length=640)
|
||||||
description_hash: str | None = None
|
description_hash: str | None = None
|
||||||
unhashed_description: str | None = None
|
unhashed_description: str | None = None
|
||||||
payment_hash: str | None = Query(
|
|
||||||
None,
|
|
||||||
description="The payment hash of the hold invoice.",
|
|
||||||
min_length=64,
|
|
||||||
max_length=64,
|
|
||||||
)
|
|
||||||
expiry: int | None = None
|
expiry: int | None = None
|
||||||
extra: dict | None = None
|
extra: dict | None = None
|
||||||
webhook: str | None = None
|
webhook: str | None = None
|
||||||
bolt11: str | None = None
|
bolt11: str | None = None
|
||||||
lnurl_withdraw: LnurlWithdrawResponse | None = None
|
lnurl_callback: str | None = None
|
||||||
fiat_provider: str | None = None
|
fiat_provider: str | None = None
|
||||||
|
|
||||||
@validator("payment_hash")
|
|
||||||
def check_hex(cls, v):
|
|
||||||
if v:
|
|
||||||
_ = bytes.fromhex(v)
|
|
||||||
return v
|
|
||||||
|
|
||||||
@validator("unit")
|
@validator("unit")
|
||||||
@classmethod
|
@classmethod
|
||||||
def unit_is_from_allowed_currencies(cls, v):
|
def unit_is_from_allowed_currencies(cls, v):
|
||||||
@@ -285,31 +272,3 @@ class PaymentsStatusCount(BaseModel):
|
|||||||
outgoing: int = 0
|
outgoing: int = 0
|
||||||
failed: int = 0
|
failed: int = 0
|
||||||
pending: int = 0
|
pending: int = 0
|
||||||
|
|
||||||
|
|
||||||
class SettleInvoice(BaseModel):
|
|
||||||
preimage: str = Field(
|
|
||||||
...,
|
|
||||||
description="The preimage of the payment hash to settle the invoice.",
|
|
||||||
min_length=64,
|
|
||||||
max_length=64,
|
|
||||||
)
|
|
||||||
|
|
||||||
@validator("preimage")
|
|
||||||
def check_hex(cls, v):
|
|
||||||
_ = bytes.fromhex(v)
|
|
||||||
return v
|
|
||||||
|
|
||||||
|
|
||||||
class CancelInvoice(BaseModel):
|
|
||||||
payment_hash: str = Field(
|
|
||||||
...,
|
|
||||||
description="The payment hash of the invoice to cancel.",
|
|
||||||
min_length=64,
|
|
||||||
max_length=64,
|
|
||||||
)
|
|
||||||
|
|
||||||
@validator("payment_hash")
|
|
||||||
def check_hex(cls, v):
|
|
||||||
_ = bytes.fromhex(v)
|
|
||||||
return v
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ from __future__ import annotations
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from bcrypt import checkpw, gensalt, hashpw
|
|
||||||
from fastapi import Query
|
from fastapi import Query
|
||||||
|
from passlib.context import CryptContext
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from lnbits.core.models.misc import SimpleItem
|
from lnbits.core.models.misc import SimpleItem
|
||||||
@@ -20,15 +20,6 @@ from lnbits.settings import settings
|
|||||||
from .wallets import Wallet
|
from .wallets import Wallet
|
||||||
|
|
||||||
|
|
||||||
class UserNotifications(BaseModel):
|
|
||||||
nostr_identifier: str | None = None
|
|
||||||
telegram_chat_id: str | None = None
|
|
||||||
email_address: str | None = None
|
|
||||||
excluded_wallets: list[str] = []
|
|
||||||
outgoing_payments_sats: int = 0
|
|
||||||
incoming_payments_sats: int = 0
|
|
||||||
|
|
||||||
|
|
||||||
class UserExtra(BaseModel):
|
class UserExtra(BaseModel):
|
||||||
email_verified: bool | None = False
|
email_verified: bool | None = False
|
||||||
first_name: str | None = None
|
first_name: str | None = None
|
||||||
@@ -44,8 +35,6 @@ class UserExtra(BaseModel):
|
|||||||
# how many wallets are shown in the user interface
|
# how many wallets are shown in the user interface
|
||||||
visible_wallet_count: int | None = 10
|
visible_wallet_count: int | None = 10
|
||||||
|
|
||||||
notifications: UserNotifications = UserNotifications()
|
|
||||||
|
|
||||||
|
|
||||||
class EndpointAccess(BaseModel):
|
class EndpointAccess(BaseModel):
|
||||||
path: str
|
path: str
|
||||||
@@ -131,18 +120,16 @@ class Account(BaseModel):
|
|||||||
|
|
||||||
def hash_password(self, password: str) -> str:
|
def hash_password(self, password: str) -> str:
|
||||||
"""sets and returns the hashed password"""
|
"""sets and returns the hashed password"""
|
||||||
salt = gensalt()
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
hashed_pw = hashpw(password.encode(), salt)
|
self.password_hash = pwd_context.hash(password)
|
||||||
if not hashed_pw:
|
|
||||||
raise ValueError("Password hashing failed.")
|
|
||||||
self.password_hash = hashed_pw.decode()
|
|
||||||
return self.password_hash
|
return self.password_hash
|
||||||
|
|
||||||
def verify_password(self, password: str) -> bool:
|
def verify_password(self, password: str) -> bool:
|
||||||
"""returns True if the password matches the hash"""
|
"""returns True if the password matches the hash"""
|
||||||
if not self.password_hash:
|
if not self.password_hash:
|
||||||
return False
|
return False
|
||||||
return checkpw(password.encode(), self.password_hash.encode())
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
|
return pwd_context.verify(password, self.password_hash)
|
||||||
|
|
||||||
def validate_fields(self):
|
def validate_fields(self):
|
||||||
if self.username and not is_valid_username(self.username):
|
if self.username and not is_valid_username(self.username):
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from lnurl import encode as lnurl_encode
|
from ecdsa import SECP256k1, SigningKey
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from lnbits.core.models.lnurl import StoredPayLinks
|
|
||||||
from lnbits.db import FilterModel
|
from lnbits.db import FilterModel
|
||||||
from lnbits.helpers import url_for
|
from lnbits.helpers import url_for
|
||||||
|
from lnbits.lnurl import encode as lnurl_encode
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
|
|
||||||
@@ -39,7 +41,6 @@ class Wallet(BaseModel):
|
|||||||
currency: str | None = None
|
currency: str | None = None
|
||||||
balance_msat: int = Field(default=0, no_database=True)
|
balance_msat: int = Field(default=0, no_database=True)
|
||||||
extra: WalletExtra = WalletExtra()
|
extra: WalletExtra = WalletExtra()
|
||||||
stored_paylinks: StoredPayLinks = StoredPayLinks()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def balance(self) -> int:
|
def balance(self) -> int:
|
||||||
@@ -57,6 +58,14 @@ class Wallet(BaseModel):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
def lnurlauth_key(self, domain: str) -> SigningKey:
|
||||||
|
hashing_key = hashlib.sha256(self.id.encode()).digest()
|
||||||
|
linking_key = hmac.digest(hashing_key, domain.encode(), "sha256")
|
||||||
|
|
||||||
|
return SigningKey.from_string(
|
||||||
|
linking_key, curve=SECP256k1, hashfunc=hashlib.sha256
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CreateWallet(BaseModel):
|
class CreateWallet(BaseModel):
|
||||||
name: str | None = None
|
name: str | None = None
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ from .funding_source import (
|
|||||||
get_balance_delta,
|
get_balance_delta,
|
||||||
switch_to_voidwallet,
|
switch_to_voidwallet,
|
||||||
)
|
)
|
||||||
from .lnurl import fetch_lnurl_pay_request, get_pr_from_lnurl, perform_withdraw
|
from .lnurl import perform_lnurlauth, redeem_lnurl_withdraw
|
||||||
from .notifications import enqueue_admin_notification, send_payment_notification
|
from .notifications import enqueue_notification, send_payment_notification
|
||||||
from .payments import (
|
from .payments import (
|
||||||
calculate_fiat_amounts,
|
calculate_fiat_amounts,
|
||||||
cancel_hold_invoice,
|
|
||||||
check_transaction_status,
|
check_transaction_status,
|
||||||
check_wallet_limits,
|
check_wallet_limits,
|
||||||
create_fiat_invoice,
|
create_fiat_invoice,
|
||||||
@@ -18,7 +17,6 @@ from .payments import (
|
|||||||
get_payments_daily_stats,
|
get_payments_daily_stats,
|
||||||
pay_invoice,
|
pay_invoice,
|
||||||
service_fee,
|
service_fee,
|
||||||
settle_hold_invoice,
|
|
||||||
update_pending_payment,
|
update_pending_payment,
|
||||||
update_pending_payments,
|
update_pending_payments,
|
||||||
update_wallet_balance,
|
update_wallet_balance,
|
||||||
@@ -38,7 +36,6 @@ from .websockets import websocket_manager, websocket_updater
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"calculate_fiat_amounts",
|
"calculate_fiat_amounts",
|
||||||
"cancel_hold_invoice",
|
|
||||||
"check_admin_settings",
|
"check_admin_settings",
|
||||||
"check_transaction_status",
|
"check_transaction_status",
|
||||||
"check_wallet_limits",
|
"check_wallet_limits",
|
||||||
@@ -49,18 +46,16 @@ __all__ = [
|
|||||||
"create_user_account",
|
"create_user_account",
|
||||||
"create_user_account_no_ckeck",
|
"create_user_account_no_ckeck",
|
||||||
"create_wallet_invoice",
|
"create_wallet_invoice",
|
||||||
"enqueue_admin_notification",
|
"enqueue_notification",
|
||||||
"fee_reserve",
|
"fee_reserve",
|
||||||
"fee_reserve_total",
|
"fee_reserve_total",
|
||||||
"fetch_lnurl_pay_request",
|
|
||||||
"get_balance_delta",
|
"get_balance_delta",
|
||||||
"get_payments_daily_stats",
|
"get_payments_daily_stats",
|
||||||
"get_pr_from_lnurl",
|
|
||||||
"pay_invoice",
|
"pay_invoice",
|
||||||
"perform_withdraw",
|
"perform_lnurlauth",
|
||||||
|
"redeem_lnurl_withdraw",
|
||||||
"send_payment_notification",
|
"send_payment_notification",
|
||||||
"service_fee",
|
"service_fee",
|
||||||
"settle_hold_invoice",
|
|
||||||
"switch_to_voidwallet",
|
"switch_to_voidwallet",
|
||||||
"update_cached_settings",
|
"update_cached_settings",
|
||||||
"update_pending_payment",
|
"update_pending_payment",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import importlib
|
import importlib
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
@@ -73,13 +74,11 @@ async def uninstall_extension(ext_id: str):
|
|||||||
async def activate_extension(ext: Extension):
|
async def activate_extension(ext: Extension):
|
||||||
core_app_extra.register_new_ext_routes(ext)
|
core_app_extra.register_new_ext_routes(ext)
|
||||||
await update_installed_extension_state(ext_id=ext.code, active=True)
|
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):
|
async def deactivate_extension(ext_id: str):
|
||||||
settings.deactivate_extension_paths(ext_id)
|
settings.deactivate_extension_paths(ext_id)
|
||||||
await update_installed_extension_state(ext_id=ext_id, active=False)
|
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:
|
async def stop_extension_background_work(ext_id: str) -> bool:
|
||||||
@@ -146,7 +145,7 @@ async def start_extension_background_work(ext_id: str) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
async def get_valid_extensions(
|
async def get_valid_extensions(
|
||||||
include_deactivated: bool | None = True,
|
include_deactivated: Optional[bool] = True,
|
||||||
) -> list[Extension]:
|
) -> list[Extension]:
|
||||||
installed_extensions = await get_installed_extensions()
|
installed_extensions = await get_installed_extensions()
|
||||||
valid_extensions = [Extension.from_installable_ext(e) for e in installed_extensions]
|
valid_extensions = [Extension.from_installable_ext(e) for e in installed_extensions]
|
||||||
@@ -165,8 +164,8 @@ async def get_valid_extensions(
|
|||||||
|
|
||||||
|
|
||||||
async def get_valid_extension(
|
async def get_valid_extension(
|
||||||
ext_id: str, include_deactivated: bool | None = True
|
ext_id: str, include_deactivated: Optional[bool] = True
|
||||||
) -> Extension | None:
|
) -> Optional[Extension]:
|
||||||
ext = await get_installed_extension(ext_id)
|
ext = await get_installed_extension(ext_id)
|
||||||
if not ext:
|
if not ext:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
import time
|
import time
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
@@ -14,7 +15,7 @@ from lnbits.settings import settings
|
|||||||
|
|
||||||
|
|
||||||
async def handle_fiat_payment_confirmation(
|
async def handle_fiat_payment_confirmation(
|
||||||
payment: Payment, conn: Connection | None = None
|
payment: Payment, conn: Optional[Connection] = None
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
await _credit_fiat_service_fee_wallet(payment, conn=conn)
|
await _credit_fiat_service_fee_wallet(payment, conn=conn)
|
||||||
@@ -28,7 +29,7 @@ async def handle_fiat_payment_confirmation(
|
|||||||
|
|
||||||
|
|
||||||
async def _credit_fiat_service_fee_wallet(
|
async def _credit_fiat_service_fee_wallet(
|
||||||
payment: Payment, conn: Connection | None = None
|
payment: Payment, conn: Optional[Connection] = None
|
||||||
):
|
):
|
||||||
fiat_provider_name = payment.fiat_provider
|
fiat_provider_name = payment.fiat_provider
|
||||||
if not fiat_provider_name:
|
if not fiat_provider_name:
|
||||||
@@ -65,7 +66,7 @@ async def _credit_fiat_service_fee_wallet(
|
|||||||
|
|
||||||
|
|
||||||
async def _debit_fiat_service_faucet_wallet(
|
async def _debit_fiat_service_faucet_wallet(
|
||||||
payment: Payment, conn: Connection | None = None
|
payment: Payment, conn: Optional[Connection] = None
|
||||||
):
|
):
|
||||||
fiat_provider_name = payment.fiat_provider
|
fiat_provider_name = payment.fiat_provider
|
||||||
if not fiat_provider_name:
|
if not fiat_provider_name:
|
||||||
@@ -128,8 +129,8 @@ async def handle_stripe_event(event: dict):
|
|||||||
|
|
||||||
def check_stripe_signature(
|
def check_stripe_signature(
|
||||||
payload: bytes,
|
payload: bytes,
|
||||||
sig_header: str | None,
|
sig_header: Optional[str],
|
||||||
secret: str | None,
|
secret: Optional[str],
|
||||||
tolerance_seconds=300,
|
tolerance_seconds=300,
|
||||||
):
|
):
|
||||||
if not sig_header:
|
if not sig_header:
|
||||||
@@ -175,11 +176,6 @@ async def test_connection(provider: str) -> SimpleStatus:
|
|||||||
This function should be called when setting up or testing the Stripe integration.
|
This function should be called when setting up or testing the Stripe integration.
|
||||||
"""
|
"""
|
||||||
fiat_provider = await get_fiat_provider(provider)
|
fiat_provider = await get_fiat_provider(provider)
|
||||||
if not fiat_provider:
|
|
||||||
return SimpleStatus(
|
|
||||||
success=False,
|
|
||||||
message=f"Fiat provider '{provider}' not found.",
|
|
||||||
)
|
|
||||||
status = await fiat_provider.status()
|
status = await fiat_provider.status()
|
||||||
if status.error_message:
|
if status.error_message:
|
||||||
return SimpleStatus(
|
return SimpleStatus(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.core.models.notifications import NotificationType
|
from lnbits.core.models.notifications import NotificationType
|
||||||
from lnbits.core.services.notifications import enqueue_admin_notification
|
from lnbits.core.services.notifications import enqueue_notification
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.wallets import get_funding_source, set_funding_source
|
from lnbits.wallets import get_funding_source, set_funding_source
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ async def check_server_balance_against_node():
|
|||||||
f"Balance delta reached: {status.delta_sats} sats."
|
f"Balance delta reached: {status.delta_sats} sats."
|
||||||
f" Switch to void wallet: {use_voidwallet}."
|
f" Switch to void wallet: {use_voidwallet}."
|
||||||
)
|
)
|
||||||
enqueue_admin_notification(
|
enqueue_notification(
|
||||||
NotificationType.watchdog_check,
|
NotificationType.watchdog_check,
|
||||||
{
|
{
|
||||||
"delta_sats": status.delta_sats,
|
"delta_sats": status.delta_sats,
|
||||||
@@ -71,7 +71,7 @@ async def check_balance_delta_changed():
|
|||||||
settings.latest_balance_delta_sats = status.delta_sats
|
settings.latest_balance_delta_sats = status.delta_sats
|
||||||
return
|
return
|
||||||
if status.delta_sats != settings.latest_balance_delta_sats:
|
if status.delta_sats != settings.latest_balance_delta_sats:
|
||||||
enqueue_admin_notification(
|
enqueue_notification(
|
||||||
NotificationType.balance_delta,
|
NotificationType.balance_delta,
|
||||||
{
|
{
|
||||||
"delta_sats": status.delta_sats,
|
"delta_sats": status.delta_sats,
|
||||||
|
|||||||
+148
-156
@@ -1,167 +1,159 @@
|
|||||||
from time import time
|
import asyncio
|
||||||
|
import json
|
||||||
|
from io import BytesIO
|
||||||
|
from typing import Optional
|
||||||
|
from urllib.parse import parse_qs, urlparse
|
||||||
|
|
||||||
from lnurl import (
|
import httpx
|
||||||
LnAddress,
|
from fastapi import Depends
|
||||||
Lnurl,
|
|
||||||
LnurlErrorResponse,
|
|
||||||
LnurlPayActionResponse,
|
|
||||||
LnurlPayResponse,
|
|
||||||
LnurlResponseException,
|
|
||||||
LnurlSuccessResponse,
|
|
||||||
LnurlWithdrawResponse,
|
|
||||||
execute_pay_request,
|
|
||||||
execute_withdraw,
|
|
||||||
handle,
|
|
||||||
)
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.core.crud import update_wallet
|
from lnbits.db import Connection
|
||||||
from lnbits.core.models import CreateLnurlPayment, Wallet
|
from lnbits.decorators import (
|
||||||
from lnbits.core.models.lnurl import StoredPayLink
|
WalletTypeInfo,
|
||||||
from lnbits.helpers import check_callback_url
|
require_admin_key,
|
||||||
|
)
|
||||||
|
from lnbits.helpers import check_callback_url, url_for
|
||||||
|
from lnbits.lnurl import LnurlErrorResponse
|
||||||
|
from lnbits.lnurl import decode as decode_lnurl
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
|
|
||||||
|
from .payments import create_invoice
|
||||||
|
|
||||||
|
|
||||||
async def perform_withdraw(lnurl: str, payment_request: str) -> None:
|
async def redeem_lnurl_withdraw(
|
||||||
"""
|
wallet_id: str,
|
||||||
Perform an LNURL withdraw to the given LNURL-withdraw link.
|
lnurl_request: str,
|
||||||
:param lnurl: The LNURL-withdraw link. bech32 or lud17 format.
|
memo: Optional[str] = None,
|
||||||
:param payment_request: The BOLT11 payment request to pay.
|
extra: Optional[dict] = None,
|
||||||
:raises LnurlResponseException: If the LNURL-withdraw process fails.
|
wait_seconds: int = 0,
|
||||||
"""
|
conn: Optional[Connection] = None,
|
||||||
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:
|
|
||||||
res = await handle(lnurl, user_agent=settings.user_agent, timeout=10)
|
|
||||||
if isinstance(res, LnurlErrorResponse):
|
|
||||||
raise LnurlResponseException(res.reason)
|
|
||||||
if not isinstance(res, LnurlPayResponse):
|
|
||||||
raise LnurlResponseException(
|
|
||||||
"Invalid LNURL response. Expected LnurlPayResponse."
|
|
||||||
)
|
|
||||||
res2 = await execute_pay_request(
|
|
||||||
res,
|
|
||||||
msat=amount_msat,
|
|
||||||
comment=comment,
|
|
||||||
user_agent=settings.user_agent,
|
|
||||||
timeout=10,
|
|
||||||
)
|
|
||||||
if isinstance(res2, LnurlErrorResponse):
|
|
||||||
raise LnurlResponseException(res2.reason)
|
|
||||||
return res2.pr
|
|
||||||
|
|
||||||
|
|
||||||
async def fetch_lnurl_pay_request(
|
|
||||||
data: CreateLnurlPayment, wallet: Wallet | None = None
|
|
||||||
) -> tuple[LnurlPayResponse, LnurlPayActionResponse]:
|
|
||||||
"""
|
|
||||||
Pay an LNURL payment request.
|
|
||||||
optional `wallet` is used to store the pay link in the wallet's stored links.
|
|
||||||
|
|
||||||
raises `LnurlResponseException` if pay request fails
|
|
||||||
"""
|
|
||||||
if not data.res and data.lnurl:
|
|
||||||
res = await handle(data.lnurl, user_agent=settings.user_agent, timeout=5)
|
|
||||||
if isinstance(res, LnurlErrorResponse):
|
|
||||||
raise LnurlResponseException(res.reason)
|
|
||||||
if not isinstance(res, LnurlPayResponse):
|
|
||||||
raise LnurlResponseException(
|
|
||||||
"Invalid LNURL response. Expected LnurlPayResponse."
|
|
||||||
)
|
|
||||||
data.res = res
|
|
||||||
if not data.res:
|
|
||||||
raise LnurlResponseException("No LNURL pay request provided.")
|
|
||||||
|
|
||||||
if data.unit and data.unit != "sat":
|
|
||||||
# shift to float with 2 decimal places
|
|
||||||
amount = round(data.amount / 1000, 2)
|
|
||||||
amount_msat = await fiat_amount_as_satoshis(amount, data.unit)
|
|
||||||
amount_msat *= 1000
|
|
||||||
else:
|
|
||||||
amount_msat = data.amount
|
|
||||||
|
|
||||||
res2 = await execute_pay_request(
|
|
||||||
data.res,
|
|
||||||
msat=amount_msat,
|
|
||||||
comment=data.comment,
|
|
||||||
user_agent=settings.user_agent,
|
|
||||||
timeout=10,
|
|
||||||
)
|
|
||||||
|
|
||||||
if wallet:
|
|
||||||
await store_paylink(data.res, res2, wallet, data.lnurl)
|
|
||||||
|
|
||||||
return data.res, res2
|
|
||||||
|
|
||||||
|
|
||||||
async def store_paylink(
|
|
||||||
res: LnurlPayResponse,
|
|
||||||
res2: LnurlPayActionResponse,
|
|
||||||
wallet: Wallet,
|
|
||||||
lnurl: LnAddress | Lnurl | None = None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
if not lnurl_request:
|
||||||
|
return None
|
||||||
|
|
||||||
if res2.disposable is not False:
|
res = {}
|
||||||
return # do not store disposable LNURL pay links
|
|
||||||
|
|
||||||
logger.debug(f"storing lnurl pay link for wallet {wallet.id}. ")
|
headers = {"User-Agent": settings.user_agent}
|
||||||
|
async with httpx.AsyncClient(headers=headers) as client:
|
||||||
|
lnurl = decode_lnurl(lnurl_request)
|
||||||
|
check_callback_url(str(lnurl))
|
||||||
|
r = await client.get(str(lnurl))
|
||||||
|
res = r.json()
|
||||||
|
|
||||||
stored_paylink = None
|
try:
|
||||||
# If we have only a LnurlPayResponse, we can use its lnaddress
|
_, payment_request = await create_invoice(
|
||||||
# because the lnurl is not available.
|
wallet_id=wallet_id,
|
||||||
if not lnurl:
|
amount=int(res["maxWithdrawable"] / 1000),
|
||||||
for _data in res.metadata.list():
|
memo=memo or res["defaultDescription"] or "",
|
||||||
if _data[0] == "text/identifier":
|
extra=extra,
|
||||||
stored_paylink = StoredPayLink(
|
conn=conn,
|
||||||
lnurl=LnAddress(_data[1]), label=res.metadata.text
|
|
||||||
)
|
|
||||||
if not stored_paylink:
|
|
||||||
logger.warning(
|
|
||||||
"No lnaddress found in metadata for LNURL pay link. "
|
|
||||||
"Skipping storage."
|
|
||||||
)
|
|
||||||
return # skip if lnaddress not found in metadata
|
|
||||||
else:
|
|
||||||
if isinstance(lnurl, Lnurl):
|
|
||||||
_lnurl = str(lnurl.lud17 or lnurl.bech32)
|
|
||||||
else:
|
|
||||||
_lnurl = str(lnurl)
|
|
||||||
stored_paylink = StoredPayLink(lnurl=_lnurl, label=res.metadata.text)
|
|
||||||
|
|
||||||
# update last_used if its already stored
|
|
||||||
for pl in wallet.stored_paylinks.links:
|
|
||||||
if pl.lnurl == stored_paylink.lnurl:
|
|
||||||
pl.last_used = int(time())
|
|
||||||
await update_wallet(wallet)
|
|
||||||
logger.debug(
|
|
||||||
"Updated last used time for LNURL "
|
|
||||||
f"pay link {stored_paylink.lnurl} in wallet {wallet.id}."
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
# if not already stored, append it
|
|
||||||
if not any(stored_paylink.lnurl == pl.lnurl for pl in wallet.stored_paylinks.links):
|
|
||||||
wallet.stored_paylinks.links.append(stored_paylink)
|
|
||||||
await update_wallet(wallet)
|
|
||||||
logger.debug(
|
|
||||||
f"Stored LNURL pay link {stored_paylink.lnurl} for wallet {wallet.id}."
|
|
||||||
)
|
)
|
||||||
|
except Exception:
|
||||||
|
logger.warning(
|
||||||
|
f"failed to create invoice on redeem_lnurl_withdraw "
|
||||||
|
f"from {lnurl}. params: {res}"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if wait_seconds:
|
||||||
|
await asyncio.sleep(wait_seconds)
|
||||||
|
|
||||||
|
params = {"k1": res["k1"], "pr": payment_request}
|
||||||
|
|
||||||
|
try:
|
||||||
|
params["balanceNotify"] = url_for(
|
||||||
|
f"/withdraw/notify/{urlparse(lnurl_request).netloc}",
|
||||||
|
external=True,
|
||||||
|
wal=wallet_id,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug(exc)
|
||||||
|
|
||||||
|
headers = {"User-Agent": settings.user_agent}
|
||||||
|
async with httpx.AsyncClient(headers=headers) as client:
|
||||||
|
try:
|
||||||
|
check_callback_url(res["callback"])
|
||||||
|
await client.get(res["callback"], params=params)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug(exc)
|
||||||
|
|
||||||
|
|
||||||
|
async def perform_lnurlauth(
|
||||||
|
callback: str,
|
||||||
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
|
) -> Optional[LnurlErrorResponse]:
|
||||||
|
cb = urlparse(callback)
|
||||||
|
|
||||||
|
k1 = bytes.fromhex(parse_qs(cb.query)["k1"][0])
|
||||||
|
|
||||||
|
key = wallet.wallet.lnurlauth_key(cb.netloc)
|
||||||
|
|
||||||
|
def int_to_bytes_suitable_der(x: int) -> bytes:
|
||||||
|
"""for strict DER we need to encode the integer with some quirks"""
|
||||||
|
b = x.to_bytes((x.bit_length() + 7) // 8, "big")
|
||||||
|
|
||||||
|
if len(b) == 0:
|
||||||
|
# ensure there's at least one byte when the int is zero
|
||||||
|
return bytes([0])
|
||||||
|
|
||||||
|
if b[0] & 0x80 != 0:
|
||||||
|
# ensure it doesn't start with a 0x80 and so it isn't
|
||||||
|
# interpreted as a negative number
|
||||||
|
return bytes([0]) + b
|
||||||
|
|
||||||
|
return b
|
||||||
|
|
||||||
|
def encode_strict_der(r: int, s: int, order: int):
|
||||||
|
# if s > order/2 verification will fail sometimes
|
||||||
|
# so we must fix it here see:
|
||||||
|
# https://github.com/indutny/elliptic/blob/e71b2d9359c5fe9437fbf46f1f05096de447de57/lib/elliptic/ec/index.js#L146-L147
|
||||||
|
if s > order // 2:
|
||||||
|
s = order - s
|
||||||
|
|
||||||
|
# now we do the strict DER encoding copied from
|
||||||
|
# https://github.com/KiriKiri/bip66 (without any checks)
|
||||||
|
r_temp = int_to_bytes_suitable_der(r)
|
||||||
|
s_temp = int_to_bytes_suitable_der(s)
|
||||||
|
|
||||||
|
r_len = len(r_temp)
|
||||||
|
s_len = len(s_temp)
|
||||||
|
sign_len = 6 + r_len + s_len
|
||||||
|
|
||||||
|
signature = BytesIO()
|
||||||
|
signature.write(0x30.to_bytes(1, "big", signed=False))
|
||||||
|
signature.write((sign_len - 2).to_bytes(1, "big", signed=False))
|
||||||
|
signature.write(0x02.to_bytes(1, "big", signed=False))
|
||||||
|
signature.write(r_len.to_bytes(1, "big", signed=False))
|
||||||
|
signature.write(r_temp)
|
||||||
|
signature.write(0x02.to_bytes(1, "big", signed=False))
|
||||||
|
signature.write(s_len.to_bytes(1, "big", signed=False))
|
||||||
|
signature.write(s_temp)
|
||||||
|
|
||||||
|
return signature.getvalue()
|
||||||
|
|
||||||
|
sig = key.sign_digest_deterministic(k1, sigencode=encode_strict_der)
|
||||||
|
|
||||||
|
headers = {"User-Agent": settings.user_agent}
|
||||||
|
async with httpx.AsyncClient(headers=headers) as client:
|
||||||
|
if not key.verifying_key:
|
||||||
|
raise ValueError("LNURLauth verifying_key does not exist")
|
||||||
|
check_callback_url(callback)
|
||||||
|
r = await client.get(
|
||||||
|
callback,
|
||||||
|
params={
|
||||||
|
"k1": k1.hex(),
|
||||||
|
"key": key.verifying_key.to_string("compressed").hex(),
|
||||||
|
"sig": sig.hex(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
resp = json.loads(r.text)
|
||||||
|
if resp["status"] == "OK":
|
||||||
|
return None
|
||||||
|
|
||||||
|
return LnurlErrorResponse(reason=resp["reason"])
|
||||||
|
except (KeyError, json.decoder.JSONDecodeError):
|
||||||
|
return LnurlErrorResponse(
|
||||||
|
reason=r.text[:200] + "..." if len(r.text) > 200 else r.text
|
||||||
|
)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import smtplib
|
|||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -15,14 +16,12 @@ from lnbits.core.crud import (
|
|||||||
get_webpush_subscriptions_for_user,
|
get_webpush_subscriptions_for_user,
|
||||||
mark_webhook_sent,
|
mark_webhook_sent,
|
||||||
)
|
)
|
||||||
from lnbits.core.crud.users import get_user
|
|
||||||
from lnbits.core.models import Payment, Wallet
|
from lnbits.core.models import Payment, Wallet
|
||||||
from lnbits.core.models.notifications import (
|
from lnbits.core.models.notifications import (
|
||||||
NOTIFICATION_TEMPLATES,
|
NOTIFICATION_TEMPLATES,
|
||||||
NotificationMessage,
|
NotificationMessage,
|
||||||
NotificationType,
|
NotificationType,
|
||||||
)
|
)
|
||||||
from lnbits.core.models.users import UserNotifications
|
|
||||||
from lnbits.core.services.nostr import fetch_nip5_details, send_nostr_dm
|
from lnbits.core.services.nostr import fetch_nip5_details, send_nostr_dm
|
||||||
from lnbits.core.services.websockets import websocket_manager
|
from lnbits.core.services.websockets import websocket_manager
|
||||||
from lnbits.helpers import check_callback_url, is_valid_email_address
|
from lnbits.helpers import check_callback_url, is_valid_email_address
|
||||||
@@ -32,8 +31,8 @@ from lnbits.utils.nostr import normalize_private_key
|
|||||||
notifications_queue: asyncio.Queue[NotificationMessage] = asyncio.Queue()
|
notifications_queue: asyncio.Queue[NotificationMessage] = asyncio.Queue()
|
||||||
|
|
||||||
|
|
||||||
def enqueue_admin_notification(message_type: NotificationType, values: dict) -> None:
|
def enqueue_notification(message_type: NotificationType, values: dict) -> None:
|
||||||
if not _is_message_type_enabled(message_type):
|
if not is_message_type_enabled(message_type):
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
notifications_queue.put_nowait(
|
notifications_queue.put_nowait(
|
||||||
@@ -43,125 +42,62 @@ def enqueue_admin_notification(message_type: NotificationType, values: dict) ->
|
|||||||
logger.error(f"Error enqueuing notification: {e}")
|
logger.error(f"Error enqueuing notification: {e}")
|
||||||
|
|
||||||
|
|
||||||
def enqueue_user_notification(
|
|
||||||
message_type: NotificationType, values: dict, user_notifications: UserNotifications
|
|
||||||
) -> None:
|
|
||||||
try:
|
|
||||||
notifications_queue.put_nowait(
|
|
||||||
NotificationMessage(
|
|
||||||
message_type=message_type,
|
|
||||||
values=values,
|
|
||||||
user_notifications=user_notifications,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error enqueuing notification: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
async def process_next_notification() -> None:
|
async def process_next_notification() -> None:
|
||||||
notification_message = await notifications_queue.get()
|
notification_message = await notifications_queue.get()
|
||||||
message_type, text = _notification_message_to_text(notification_message)
|
message_type, text = _notification_message_to_text(notification_message)
|
||||||
user_notifications = notification_message.user_notifications
|
await send_notification(text, message_type)
|
||||||
if user_notifications:
|
|
||||||
await send_user_notification(user_notifications, text, message_type)
|
|
||||||
else:
|
|
||||||
await send_admin_notification(text, message_type)
|
|
||||||
|
|
||||||
|
|
||||||
async def send_admin_notification(
|
|
||||||
message: str,
|
|
||||||
message_type: str | None = None,
|
|
||||||
) -> None:
|
|
||||||
return await send_notification(
|
|
||||||
settings.lnbits_telegram_notifications_chat_id,
|
|
||||||
settings.lnbits_nostr_notifications_identifiers,
|
|
||||||
settings.lnbits_email_notifications_to_emails,
|
|
||||||
message,
|
|
||||||
message_type,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def send_user_notification(
|
|
||||||
user_notifications: UserNotifications,
|
|
||||||
message: str,
|
|
||||||
message_type: str | None = None,
|
|
||||||
) -> None:
|
|
||||||
|
|
||||||
email_address = (
|
|
||||||
[user_notifications.email_address] if user_notifications.email_address else []
|
|
||||||
)
|
|
||||||
nostr_identifiers = (
|
|
||||||
[user_notifications.nostr_identifier]
|
|
||||||
if user_notifications.nostr_identifier
|
|
||||||
else []
|
|
||||||
)
|
|
||||||
return await send_notification(
|
|
||||||
user_notifications.telegram_chat_id,
|
|
||||||
nostr_identifiers,
|
|
||||||
email_address,
|
|
||||||
message,
|
|
||||||
message_type,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def send_notification(
|
async def send_notification(
|
||||||
telegram_chat_id: str | None,
|
|
||||||
nostr_identifiers: list[str] | None,
|
|
||||||
email_addresses: list[str] | None,
|
|
||||||
message: str,
|
message: str,
|
||||||
message_type: str | None = None,
|
message_type: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
try:
|
try:
|
||||||
if telegram_chat_id and settings.is_telegram_notifications_configured():
|
if settings.lnbits_telegram_notifications_enabled:
|
||||||
await send_telegram_notification(telegram_chat_id, message)
|
await send_telegram_notification(message)
|
||||||
logger.debug(f"Sent telegram notification: {message_type}")
|
logger.debug(f"Sent telegram notification: {message_type}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error sending telegram notification {message_type}: {e}")
|
logger.error(f"Error sending telegram notification {message_type}: {e}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if nostr_identifiers and settings.is_nostr_notifications_configured():
|
if settings.lnbits_nostr_notifications_enabled:
|
||||||
await send_nostr_notifications(nostr_identifiers, message)
|
await send_nostr_notification(message)
|
||||||
logger.debug(f"Sent nostr notification: {message_type}")
|
logger.debug(f"Sent nostr notification: {message_type}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error sending nostr notification {message_type}: {e}")
|
logger.error(f"Error sending nostr notification {message_type}: {e}")
|
||||||
try:
|
try:
|
||||||
if email_addresses and settings.lnbits_email_notifications_enabled:
|
if settings.lnbits_email_notifications_enabled:
|
||||||
await send_email_notification(email_addresses, message)
|
await send_email_notification(message)
|
||||||
logger.debug(f"Sent email notification: {message_type}")
|
logger.debug(f"Sent email notification: {message_type}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error sending email notification {message_type}: {e}")
|
logger.error(f"Error sending email notification {message_type}: {e}")
|
||||||
|
|
||||||
|
|
||||||
async def send_nostr_notifications(identifiers: list[str], message: str) -> list[str]:
|
async def send_nostr_notification(message: str) -> dict:
|
||||||
success_sent: list[str] = []
|
for i in settings.lnbits_nostr_notifications_identifiers:
|
||||||
for identifier in identifiers:
|
|
||||||
try:
|
try:
|
||||||
await send_nostr_notification(identifier, message)
|
identifier = await fetch_nip5_details(i)
|
||||||
success_sent.append(identifier)
|
user_pubkey = identifier[0]
|
||||||
|
relays = identifier[1]
|
||||||
|
server_private_key = normalize_private_key(
|
||||||
|
settings.lnbits_nostr_notifications_private_key
|
||||||
|
)
|
||||||
|
await send_nostr_dm(
|
||||||
|
server_private_key,
|
||||||
|
user_pubkey,
|
||||||
|
message,
|
||||||
|
relays,
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Error notifying identifier {identifier}: {e}")
|
logger.warning(f"Error notifying identifier {i}: {e}")
|
||||||
return success_sent
|
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
async def send_nostr_notification(identifier: str, message: str):
|
async def send_telegram_notification(message: str) -> dict:
|
||||||
nip5 = await fetch_nip5_details(identifier)
|
|
||||||
user_pubkey = nip5[0]
|
|
||||||
relays = nip5[1]
|
|
||||||
server_private_key = normalize_private_key(
|
|
||||||
settings.lnbits_nostr_notifications_private_key
|
|
||||||
)
|
|
||||||
await send_nostr_dm(
|
|
||||||
server_private_key,
|
|
||||||
user_pubkey,
|
|
||||||
message,
|
|
||||||
relays,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def send_telegram_notification(chat_id: str, message: str) -> dict:
|
|
||||||
return await send_telegram_message(
|
return await send_telegram_message(
|
||||||
settings.lnbits_telegram_notifications_access_token,
|
settings.lnbits_telegram_notifications_access_token,
|
||||||
chat_id,
|
settings.lnbits_telegram_notifications_chat_id,
|
||||||
message,
|
message,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -176,7 +112,7 @@ async def send_telegram_message(token: str, chat_id: str, message: str) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
async def send_email_notification(
|
async def send_email_notification(
|
||||||
to_emails: list[str], message: str, subject: str = "LNbits Notification"
|
message: str, subject: str = "LNbits Notification"
|
||||||
) -> dict:
|
) -> dict:
|
||||||
if not settings.lnbits_email_notifications_enabled:
|
if not settings.lnbits_email_notifications_enabled:
|
||||||
return {"status": "error", "message": "Email notifications are disabled"}
|
return {"status": "error", "message": "Email notifications are disabled"}
|
||||||
@@ -187,7 +123,7 @@ async def send_email_notification(
|
|||||||
settings.lnbits_email_notifications_username,
|
settings.lnbits_email_notifications_username,
|
||||||
settings.lnbits_email_notifications_password,
|
settings.lnbits_email_notifications_password,
|
||||||
settings.lnbits_email_notifications_email,
|
settings.lnbits_email_notifications_email,
|
||||||
to_emails,
|
settings.lnbits_email_notifications_to_emails,
|
||||||
subject,
|
subject,
|
||||||
message,
|
message,
|
||||||
)
|
)
|
||||||
@@ -227,6 +163,41 @@ async def send_email(
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def is_message_type_enabled(message_type: NotificationType) -> bool:
|
||||||
|
if message_type == NotificationType.balance_update:
|
||||||
|
return settings.lnbits_notification_credit_debit
|
||||||
|
if message_type == NotificationType.settings_update:
|
||||||
|
return settings.lnbits_notification_settings_update
|
||||||
|
if message_type == NotificationType.watchdog_check:
|
||||||
|
return settings.lnbits_notification_watchdog
|
||||||
|
if message_type == NotificationType.balance_delta:
|
||||||
|
return settings.notification_balance_delta_changed
|
||||||
|
if message_type == NotificationType.server_start_stop:
|
||||||
|
return settings.lnbits_notification_server_start_stop
|
||||||
|
if message_type == NotificationType.server_status:
|
||||||
|
return settings.lnbits_notification_server_status_hours > 0
|
||||||
|
if message_type == NotificationType.incoming_payment:
|
||||||
|
return settings.lnbits_notification_incoming_payment_amount_sats > 0
|
||||||
|
if message_type == NotificationType.outgoing_payment:
|
||||||
|
return settings.lnbits_notification_outgoing_payment_amount_sats > 0
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _notification_message_to_text(
|
||||||
|
notification_message: NotificationMessage,
|
||||||
|
) -> tuple[str, str]:
|
||||||
|
message_type = notification_message.message_type.value
|
||||||
|
meesage_value = NOTIFICATION_TEMPLATES.get(message_type, message_type)
|
||||||
|
try:
|
||||||
|
text = meesage_value.format(**notification_message.values)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error formatting notification message: {e}")
|
||||||
|
text = meesage_value
|
||||||
|
text = f"""[{settings.lnbits_site_title}]\n{text}"""
|
||||||
|
return message_type, text
|
||||||
|
|
||||||
|
|
||||||
async def dispatch_webhook(payment: Payment):
|
async def dispatch_webhook(payment: Payment):
|
||||||
"""
|
"""
|
||||||
Dispatches the webhook to the webhook url.
|
Dispatches the webhook to the webhook url.
|
||||||
@@ -260,7 +231,7 @@ async def send_payment_notification(wallet: Wallet, payment: Payment):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error sending websocket payment notification {e!s}")
|
logger.error(f"Error sending websocket payment notification {e!s}")
|
||||||
try:
|
try:
|
||||||
await send_chat_payment_notification(wallet, payment)
|
send_chat_payment_notification(wallet, payment)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error sending chat payment notification {e!s}")
|
logger.error(f"Error sending chat payment notification {e!s}")
|
||||||
try:
|
try:
|
||||||
@@ -277,7 +248,7 @@ async def send_payment_notification(wallet: Wallet, payment: Payment):
|
|||||||
|
|
||||||
async def send_ws_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
|
# TODO: websocket message should be a clean payment model
|
||||||
# await websocket_manager.send(wallet.inkey, payment.json())
|
# await websocket_manager.send_data(payment.json(), wallet.inkey)
|
||||||
# TODO: figure out why we send the balance with the payment here.
|
# TODO: figure out why we send the balance with the payment here.
|
||||||
# cleaner would be to have a separate message for the balance
|
# 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
|
# and send it with the id of the wallet so wallets can subscribe to it
|
||||||
@@ -288,15 +259,16 @@ async def send_ws_payment_notification(wallet: Wallet, payment: Payment):
|
|||||||
"payment": json.loads(payment.json()),
|
"payment": json.loads(payment.json()),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
await websocket_manager.send(wallet.inkey, payment_notification)
|
await websocket_manager.send_data(payment_notification, wallet.inkey)
|
||||||
await websocket_manager.send(wallet.adminkey, payment_notification)
|
await websocket_manager.send_data(payment_notification, wallet.adminkey)
|
||||||
await websocket_manager.send(
|
|
||||||
payment.payment_hash,
|
await websocket_manager.send_data(
|
||||||
json.dumps({"pending": payment.pending, "status": payment.status}),
|
json.dumps({"pending": payment.pending, "status": payment.status}),
|
||||||
|
payment.payment_hash,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def send_chat_payment_notification(wallet: Wallet, payment: Payment):
|
def send_chat_payment_notification(wallet: Wallet, payment: Payment):
|
||||||
amount_sats = abs(payment.sat)
|
amount_sats = abs(payment.sat)
|
||||||
values: dict = {
|
values: dict = {
|
||||||
"wallet_id": wallet.id,
|
"wallet_id": wallet.id,
|
||||||
@@ -311,23 +283,10 @@ async def send_chat_payment_notification(wallet: Wallet, payment: Payment):
|
|||||||
|
|
||||||
if payment.is_out:
|
if payment.is_out:
|
||||||
if amount_sats >= settings.lnbits_notification_outgoing_payment_amount_sats:
|
if amount_sats >= settings.lnbits_notification_outgoing_payment_amount_sats:
|
||||||
enqueue_admin_notification(NotificationType.outgoing_payment, values)
|
enqueue_notification(NotificationType.outgoing_payment, values)
|
||||||
elif amount_sats >= settings.lnbits_notification_incoming_payment_amount_sats:
|
else:
|
||||||
enqueue_admin_notification(NotificationType.incoming_payment, values)
|
if amount_sats >= settings.lnbits_notification_incoming_payment_amount_sats:
|
||||||
|
enqueue_notification(NotificationType.incoming_payment, values)
|
||||||
user = await get_user(wallet.user)
|
|
||||||
user_notifications = user.extra.notifications if user else None
|
|
||||||
if user_notifications and wallet.id not in user_notifications.excluded_wallets:
|
|
||||||
out_limit = user_notifications.outgoing_payments_sats
|
|
||||||
in_limit = user_notifications.incoming_payments_sats
|
|
||||||
if payment.is_out and (amount_sats >= out_limit):
|
|
||||||
enqueue_user_notification(
|
|
||||||
NotificationType.outgoing_payment, values, user_notifications
|
|
||||||
)
|
|
||||||
elif amount_sats >= in_limit:
|
|
||||||
enqueue_user_notification(
|
|
||||||
NotificationType.incoming_payment, values, user_notifications
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def send_payment_push_notification(wallet: Wallet, payment: Payment):
|
async def send_payment_push_notification(wallet: Wallet, payment: Payment):
|
||||||
@@ -371,38 +330,3 @@ async def send_push_notification(subscription, title, body, url=""):
|
|||||||
f"failed sending push notification: "
|
f"failed sending push notification: "
|
||||||
f"{e.response.text if e.response else e}"
|
f"{e.response.text if e.response else e}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _is_message_type_enabled(message_type: NotificationType) -> bool:
|
|
||||||
if message_type == NotificationType.balance_update:
|
|
||||||
return settings.lnbits_notification_credit_debit
|
|
||||||
if message_type == NotificationType.settings_update:
|
|
||||||
return settings.lnbits_notification_settings_update
|
|
||||||
if message_type == NotificationType.watchdog_check:
|
|
||||||
return settings.lnbits_notification_watchdog
|
|
||||||
if message_type == NotificationType.balance_delta:
|
|
||||||
return settings.notification_balance_delta_changed
|
|
||||||
if message_type == NotificationType.server_start_stop:
|
|
||||||
return settings.lnbits_notification_server_start_stop
|
|
||||||
if message_type == NotificationType.server_status:
|
|
||||||
return settings.lnbits_notification_server_status_hours > 0
|
|
||||||
if message_type == NotificationType.incoming_payment:
|
|
||||||
return settings.lnbits_notification_incoming_payment_amount_sats > 0
|
|
||||||
if message_type == NotificationType.outgoing_payment:
|
|
||||||
return settings.lnbits_notification_outgoing_payment_amount_sats > 0
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _notification_message_to_text(
|
|
||||||
notification_message: NotificationMessage,
|
|
||||||
) -> tuple[str, str]:
|
|
||||||
message_type = notification_message.message_type.value
|
|
||||||
meesage_value = NOTIFICATION_TEMPLATES.get(message_type, message_type)
|
|
||||||
try:
|
|
||||||
text = meesage_value.format(**notification_message.values)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Error formatting notification message: {e}")
|
|
||||||
text = meesage_value
|
|
||||||
text = f"""[{settings.lnbits_site_title}]\n{text}"""
|
|
||||||
return message_type, text
|
|
||||||
|
|||||||
@@ -1,34 +1,30 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import json
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import httpx
|
||||||
from bolt11 import Bolt11, MilliSatoshi, Tags
|
from bolt11 import Bolt11, MilliSatoshi, Tags
|
||||||
from bolt11 import decode as bolt11_decode
|
from bolt11 import decode as bolt11_decode
|
||||||
from bolt11 import encode as bolt11_encode
|
from bolt11 import encode as bolt11_encode
|
||||||
from lnurl import LnurlErrorResponse, LnurlSuccessResponse
|
|
||||||
from lnurl import execute_withdraw as lnurl_withdraw
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from bolt11.exceptions import Bolt11Bech32InvalidException
|
|
||||||
import base64, json, time
|
|
||||||
from types import SimpleNamespace
|
|
||||||
|
|
||||||
from lnbits.core.crud.payments import get_daily_stats
|
from lnbits.core.crud.payments import get_daily_stats
|
||||||
from lnbits.core.db import db
|
from lnbits.core.db import db
|
||||||
from lnbits.core.models import PaymentDailyStats, PaymentFilters
|
from lnbits.core.models import PaymentDailyStats, PaymentFilters
|
||||||
from lnbits.core.models.payments import CreateInvoice
|
from lnbits.core.models.payments import CreateInvoice
|
||||||
from lnbits.db import Connection, Filters
|
from lnbits.db import Connection, Filters
|
||||||
from lnbits.decorators import check_user_extension_access
|
from lnbits.decorators import check_user_extension_access
|
||||||
from lnbits.exceptions import InvoiceError, PaymentError, UnsupportedError
|
from lnbits.exceptions import InvoiceError, PaymentError
|
||||||
from lnbits.fiat import get_fiat_provider
|
from lnbits.fiat import get_fiat_provider
|
||||||
from lnbits.helpers import check_callback_url
|
from lnbits.helpers import check_callback_url
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.tasks import create_task, internal_invoice_queue_put
|
from lnbits.tasks import create_task, internal_invoice_queue_put
|
||||||
from lnbits.utils.crypto import fake_privkey, random_secret_and_hash, verify_preimage
|
from lnbits.utils.crypto import fake_privkey, random_secret_and_hash
|
||||||
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis, satoshis_amount_as_fiat
|
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis, satoshis_amount_as_fiat
|
||||||
from lnbits.wallets import fake_wallet, get_funding_source
|
from lnbits.wallets import fake_wallet, get_funding_source
|
||||||
from lnbits.wallets.base import (
|
from lnbits.wallets.base import (
|
||||||
InvoiceResponse,
|
|
||||||
PaymentPendingStatus,
|
PaymentPendingStatus,
|
||||||
PaymentResponse,
|
PaymentResponse,
|
||||||
PaymentStatus,
|
PaymentStatus,
|
||||||
@@ -56,72 +52,16 @@ from .notifications import send_payment_notification
|
|||||||
payment_lock = asyncio.Lock()
|
payment_lock = asyncio.Lock()
|
||||||
wallets_payments_lock: dict[str, asyncio.Lock] = {}
|
wallets_payments_lock: dict[str, asyncio.Lock] = {}
|
||||||
|
|
||||||
def _parse_ark_ticket(ticket: str) -> SimpleNamespace:
|
|
||||||
"""
|
|
||||||
Accepts 'ark1' + base64url(JSON) and returns an object that mimics the
|
|
||||||
attributes LNbits reads from a decoded BOLT11 invoice.
|
|
||||||
Required JSON fields we honor:
|
|
||||||
- amt_sat: int
|
|
||||||
- hash: str (payment hash / checking_id)
|
|
||||||
- ts: int (unix timestamp when created)
|
|
||||||
- exp: int (seconds until expiry)
|
|
||||||
- memo: str (optional)
|
|
||||||
"""
|
|
||||||
if not isinstance(ticket, str) or not ticket.startswith("ark1"):
|
|
||||||
raise ValueError("Not an ARK ticket")
|
|
||||||
|
|
||||||
b64u = ticket[4:]
|
|
||||||
b64u += "=" * (-len(b64u) % 4) # restore padding
|
|
||||||
payload = json.loads(base64.urlsafe_b64decode(b64u).decode())
|
|
||||||
|
|
||||||
amt_sat = int(payload.get("amt_sat", 0))
|
|
||||||
ts = int(payload.get("ts", int(time.time())))
|
|
||||||
exp = int(payload.get("exp", 3600))
|
|
||||||
memo = payload.get("memo") or ""
|
|
||||||
p_hash = payload.get("hash") or payload.get("payment_hash")
|
|
||||||
if not isinstance(p_hash, str):
|
|
||||||
raise ValueError("ARK ticket missing 'hash'")
|
|
||||||
|
|
||||||
# Match attributes accessed later in this module:
|
|
||||||
# - amount_msat (int)
|
|
||||||
# - payment_hash (str)
|
|
||||||
# - description (str)
|
|
||||||
# - expiry_date (int unix ts expected by CreatePayment)
|
|
||||||
# - tags.get(...) is sometimes called; return a harmless stub
|
|
||||||
return SimpleNamespace(
|
|
||||||
amount_msat=amt_sat * 1000,
|
|
||||||
payment_hash=p_hash,
|
|
||||||
description=memo,
|
|
||||||
expiry_date=ts + exp, # unix timestamp
|
|
||||||
timestamp=ts, # not strictly required here but nice to have
|
|
||||||
tags=SimpleNamespace(get=lambda *a, **k: None),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Keep a reference to the real decoder, then override the name used below
|
|
||||||
_real_bolt11_decode = bolt11_decode
|
|
||||||
|
|
||||||
def _safe_decode_payment_request(pr: str):
|
|
||||||
try:
|
|
||||||
return _real_bolt11_decode(pr)
|
|
||||||
except Bolt11Bech32InvalidException:
|
|
||||||
# Fallback: accept ARK tickets
|
|
||||||
if isinstance(pr, str) and pr.startswith("ark1"):
|
|
||||||
return _parse_ark_ticket(pr)
|
|
||||||
# Not ARK? re-raise to keep existing behavior
|
|
||||||
raise
|
|
||||||
|
|
||||||
# Override the imported name used throughout this file:
|
|
||||||
bolt11_decode = _safe_decode_payment_request
|
|
||||||
|
|
||||||
async def pay_invoice(
|
async def pay_invoice(
|
||||||
*,
|
*,
|
||||||
wallet_id: str,
|
wallet_id: str,
|
||||||
payment_request: str,
|
payment_request: str,
|
||||||
max_sat: int | None = None,
|
max_sat: Optional[int] = None,
|
||||||
extra: dict | None = None,
|
extra: Optional[dict] = None,
|
||||||
description: str = "",
|
description: str = "",
|
||||||
tag: str = "",
|
tag: str = "",
|
||||||
conn: Connection | None = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> Payment:
|
) -> Payment:
|
||||||
if settings.lnbits_only_allow_incoming_payments:
|
if settings.lnbits_only_allow_incoming_payments:
|
||||||
raise PaymentError("Only incoming payments allowed.", status="failed")
|
raise PaymentError("Only incoming payments allowed.", status="failed")
|
||||||
@@ -150,8 +90,12 @@ async def pay_invoice(
|
|||||||
|
|
||||||
payment = await _pay_invoice(wallet.id, create_payment_model, conn)
|
payment = await _pay_invoice(wallet.id, create_payment_model, conn)
|
||||||
|
|
||||||
|
service_fee_memo = f"""
|
||||||
|
Service fee for payment of {abs(payment.sat)} sats.
|
||||||
|
Wallet: '{wallet.name}' ({wallet.id})."""
|
||||||
|
|
||||||
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
|
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
|
||||||
await _credit_service_fee_wallet(wallet, payment, new_conn)
|
await _credit_service_fee_wallet(payment, service_fee_memo, new_conn)
|
||||||
|
|
||||||
return payment
|
return payment
|
||||||
|
|
||||||
@@ -169,7 +113,7 @@ async def create_payment_request(
|
|||||||
|
|
||||||
|
|
||||||
async def create_fiat_invoice(
|
async def create_fiat_invoice(
|
||||||
wallet_id: str, invoice_data: CreateInvoice, conn: Connection | None = None
|
wallet_id: str, invoice_data: CreateInvoice, conn: Optional[Connection] = None
|
||||||
):
|
):
|
||||||
fiat_provider_name = invoice_data.fiat_provider
|
fiat_provider_name = invoice_data.fiat_provider
|
||||||
if not fiat_provider_name:
|
if not fiat_provider_name:
|
||||||
@@ -191,17 +135,12 @@ async def create_fiat_invoice(
|
|||||||
internal_payment = await create_wallet_invoice(wallet_id, invoice_data)
|
internal_payment = await create_wallet_invoice(wallet_id, invoice_data)
|
||||||
|
|
||||||
fiat_provider = await get_fiat_provider(fiat_provider_name)
|
fiat_provider = await get_fiat_provider(fiat_provider_name)
|
||||||
if not fiat_provider:
|
|
||||||
raise InvoiceError("No fiat provider found.", status="failed")
|
|
||||||
|
|
||||||
fiat_invoice = await fiat_provider.create_invoice(
|
fiat_invoice = await fiat_provider.create_invoice(
|
||||||
amount=invoice_data.amount,
|
amount=invoice_data.amount,
|
||||||
payment_hash=internal_payment.payment_hash,
|
payment_hash=internal_payment.payment_hash,
|
||||||
currency=invoice_data.unit,
|
currency=invoice_data.unit,
|
||||||
memo=invoice_data.memo,
|
memo=invoice_data.memo,
|
||||||
extra=invoice_data.extra or {},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if fiat_invoice.failed:
|
if fiat_invoice.failed:
|
||||||
logger.warning(fiat_invoice.error_message)
|
logger.warning(fiat_invoice.error_message)
|
||||||
internal_payment.status = PaymentState.FAILED
|
internal_payment.status = PaymentState.FAILED
|
||||||
@@ -229,8 +168,8 @@ async def create_fiat_invoice(
|
|||||||
|
|
||||||
|
|
||||||
async def create_wallet_invoice(wallet_id: str, data: CreateInvoice) -> Payment:
|
async def create_wallet_invoice(wallet_id: str, data: CreateInvoice) -> Payment:
|
||||||
description_hash = None
|
description_hash = b""
|
||||||
unhashed_description = None
|
unhashed_description = b""
|
||||||
memo = data.memo or settings.lnbits_site_title
|
memo = data.memo or settings.lnbits_site_title
|
||||||
if data.description_hash or data.unhashed_description:
|
if data.description_hash or data.unhashed_description:
|
||||||
if data.description_hash:
|
if data.description_hash:
|
||||||
@@ -261,29 +200,30 @@ async def create_wallet_invoice(wallet_id: str, data: CreateInvoice) -> Payment:
|
|||||||
extra=data.extra,
|
extra=data.extra,
|
||||||
webhook=data.webhook,
|
webhook=data.webhook,
|
||||||
internal=data.internal,
|
internal=data.internal,
|
||||||
payment_hash=data.payment_hash,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if data.lnurl_withdraw:
|
# lnurl_response is not saved in the database
|
||||||
try:
|
if data.lnurl_callback:
|
||||||
check_callback_url(data.lnurl_withdraw.callback)
|
headers = {"User-Agent": settings.user_agent}
|
||||||
res = await lnurl_withdraw(
|
async with httpx.AsyncClient(headers=headers) as client:
|
||||||
data.lnurl_withdraw,
|
try:
|
||||||
payment.bolt11,
|
check_callback_url(data.lnurl_callback)
|
||||||
user_agent=settings.user_agent,
|
r = await client.get(
|
||||||
timeout=10,
|
data.lnurl_callback,
|
||||||
)
|
params={"pr": payment.bolt11},
|
||||||
if isinstance(res, LnurlErrorResponse):
|
timeout=10,
|
||||||
payment.extra["lnurl_response"] = res.reason
|
)
|
||||||
payment.status = "failed"
|
if r.is_error:
|
||||||
elif isinstance(res, LnurlSuccessResponse):
|
payment.extra["lnurl_response"] = r.text
|
||||||
payment.extra["lnurl_response"] = True
|
else:
|
||||||
payment.status = "success"
|
resp = json.loads(r.text)
|
||||||
except Exception as exc:
|
if resp["status"] != "OK":
|
||||||
payment.extra["lnurl_response"] = str(exc)
|
payment.extra["lnurl_response"] = resp["reason"]
|
||||||
payment.status = "failed"
|
else:
|
||||||
# updating to payment here would run into a race condition
|
payment.extra["lnurl_response"] = True
|
||||||
# with the payment listeners and they will overwrite each other
|
except (httpx.ConnectError, httpx.RequestError) as ex:
|
||||||
|
logger.error(ex)
|
||||||
|
payment.extra["lnurl_response"] = False
|
||||||
|
|
||||||
return payment
|
return payment
|
||||||
|
|
||||||
@@ -292,16 +232,15 @@ async def create_invoice(
|
|||||||
*,
|
*,
|
||||||
wallet_id: str,
|
wallet_id: str,
|
||||||
amount: float,
|
amount: float,
|
||||||
currency: str | None = "sat",
|
currency: Optional[str] = "sat",
|
||||||
memo: str,
|
memo: str,
|
||||||
description_hash: bytes | None = None,
|
description_hash: Optional[bytes] = None,
|
||||||
unhashed_description: bytes | None = None,
|
unhashed_description: Optional[bytes] = None,
|
||||||
expiry: int | None = None,
|
expiry: Optional[int] = None,
|
||||||
extra: dict | None = None,
|
extra: Optional[dict] = None,
|
||||||
webhook: str | None = None,
|
webhook: Optional[str] = None,
|
||||||
internal: bool | None = False,
|
internal: Optional[bool] = False,
|
||||||
payment_hash: str | None = None,
|
conn: Optional[Connection] = None,
|
||||||
conn: Connection | None = None,
|
|
||||||
) -> Payment:
|
) -> Payment:
|
||||||
if not amount > 0:
|
if not amount > 0:
|
||||||
raise InvoiceError("Amountless invoices not supported.", status="failed")
|
raise InvoiceError("Amountless invoices not supported.", status="failed")
|
||||||
@@ -334,54 +273,39 @@ async def create_invoice(
|
|||||||
status="failed",
|
status="failed",
|
||||||
)
|
)
|
||||||
|
|
||||||
if payment_hash:
|
payment_response = await funding_source.create_invoice(
|
||||||
try:
|
amount=amount_sat,
|
||||||
invoice_response = await funding_source.create_hold_invoice(
|
memo=invoice_memo,
|
||||||
amount=amount_sat,
|
description_hash=description_hash,
|
||||||
memo=invoice_memo,
|
unhashed_description=unhashed_description,
|
||||||
payment_hash=payment_hash,
|
expiry=expiry or settings.lightning_invoice_expiry,
|
||||||
description_hash=description_hash,
|
)
|
||||||
)
|
|
||||||
extra["hold_invoice"] = True
|
|
||||||
except UnsupportedError as exc:
|
|
||||||
raise InvoiceError(
|
|
||||||
"Hold invoices are not supported by the funding source.",
|
|
||||||
status="failed",
|
|
||||||
) from exc
|
|
||||||
else:
|
|
||||||
invoice_response = await funding_source.create_invoice(
|
|
||||||
amount=amount_sat,
|
|
||||||
memo=invoice_memo,
|
|
||||||
description_hash=description_hash,
|
|
||||||
unhashed_description=unhashed_description,
|
|
||||||
expiry=expiry or settings.lightning_invoice_expiry,
|
|
||||||
)
|
|
||||||
if (
|
if (
|
||||||
not invoice_response.ok
|
not payment_response.ok
|
||||||
or not invoice_response.payment_request
|
or not payment_response.payment_request
|
||||||
or not invoice_response.checking_id
|
or not payment_response.checking_id
|
||||||
):
|
):
|
||||||
raise InvoiceError(
|
raise InvoiceError(
|
||||||
message=invoice_response.error_message or "unexpected backend error.",
|
message=payment_response.error_message or "unexpected backend error.",
|
||||||
status="pending",
|
status="pending",
|
||||||
)
|
)
|
||||||
invoice = bolt11_decode(invoice_response.payment_request)
|
invoice = bolt11_decode(payment_response.payment_request)
|
||||||
|
|
||||||
create_payment_model = CreatePayment(
|
create_payment_model = CreatePayment(
|
||||||
wallet_id=wallet_id,
|
wallet_id=wallet_id,
|
||||||
bolt11=invoice_response.payment_request,
|
bolt11=payment_response.payment_request,
|
||||||
payment_hash=invoice.payment_hash,
|
payment_hash=invoice.payment_hash,
|
||||||
preimage=invoice_response.preimage,
|
preimage=payment_response.preimage,
|
||||||
amount_msat=amount_sat * 1000,
|
amount_msat=amount_sat * 1000,
|
||||||
expiry=invoice.expiry_date,
|
expiry=invoice.expiry_date,
|
||||||
memo=memo,
|
memo=memo,
|
||||||
extra=extra,
|
extra=extra,
|
||||||
webhook=webhook,
|
webhook=webhook,
|
||||||
fee=invoice_response.fee_msat or 0,
|
fee=payment_response.fee_msat or 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
payment = await create_payment(
|
payment = await create_payment(
|
||||||
checking_id=invoice_response.checking_id,
|
checking_id=payment_response.checking_id,
|
||||||
data=create_payment_model,
|
data=create_payment_model,
|
||||||
conn=conn,
|
conn=conn,
|
||||||
)
|
)
|
||||||
@@ -488,7 +412,7 @@ def service_fee_fiat(amount_msat: int, fiat_provider_name: str) -> int:
|
|||||||
async def update_wallet_balance(
|
async def update_wallet_balance(
|
||||||
wallet: Wallet,
|
wallet: Wallet,
|
||||||
amount: int,
|
amount: int,
|
||||||
conn: Connection | None = None,
|
conn: Optional[Connection] = None,
|
||||||
):
|
):
|
||||||
if amount == 0:
|
if amount == 0:
|
||||||
raise ValueError("Amount cannot be 0.")
|
raise ValueError("Amount cannot be 0.")
|
||||||
@@ -547,14 +471,14 @@ async def update_wallet_balance(
|
|||||||
|
|
||||||
|
|
||||||
async def check_wallet_limits(
|
async def check_wallet_limits(
|
||||||
wallet_id: str, amount_msat: int, conn: Connection | None = None
|
wallet_id: str, amount_msat: int, conn: Optional[Connection] = None
|
||||||
):
|
):
|
||||||
await check_time_limit_between_transactions(wallet_id, conn)
|
await check_time_limit_between_transactions(wallet_id, conn)
|
||||||
await check_wallet_daily_withdraw_limit(wallet_id, amount_msat, conn)
|
await check_wallet_daily_withdraw_limit(wallet_id, amount_msat, conn)
|
||||||
|
|
||||||
|
|
||||||
async def check_time_limit_between_transactions(
|
async def check_time_limit_between_transactions(
|
||||||
wallet_id: str, conn: Connection | None = None
|
wallet_id: str, conn: Optional[Connection] = None
|
||||||
):
|
):
|
||||||
limit = settings.lnbits_wallet_limit_secs_between_trans
|
limit = settings.lnbits_wallet_limit_secs_between_trans
|
||||||
if not limit or limit <= 0:
|
if not limit or limit <= 0:
|
||||||
@@ -574,7 +498,7 @@ async def check_time_limit_between_transactions(
|
|||||||
|
|
||||||
|
|
||||||
async def check_wallet_daily_withdraw_limit(
|
async def check_wallet_daily_withdraw_limit(
|
||||||
wallet_id: str, amount_msat: int, conn: Connection | None = None
|
wallet_id: str, amount_msat: int, conn: Optional[Connection] = None
|
||||||
):
|
):
|
||||||
limit = settings.lnbits_wallet_limit_daily_max_withdraw
|
limit = settings.lnbits_wallet_limit_daily_max_withdraw
|
||||||
if not limit:
|
if not limit:
|
||||||
@@ -607,8 +531,8 @@ async def check_wallet_daily_withdraw_limit(
|
|||||||
async def calculate_fiat_amounts(
|
async def calculate_fiat_amounts(
|
||||||
amount: float,
|
amount: float,
|
||||||
wallet: Wallet,
|
wallet: Wallet,
|
||||||
currency: str | None = None,
|
currency: Optional[str] = None,
|
||||||
extra: dict | None = None,
|
extra: Optional[dict] = None,
|
||||||
) -> tuple[int, dict]:
|
) -> tuple[int, dict]:
|
||||||
wallet_currency = wallet.currency or settings.lnbits_default_accounting_currency
|
wallet_currency = wallet.currency or settings.lnbits_default_accounting_currency
|
||||||
fiat_amounts: dict = extra or {}
|
fiat_amounts: dict = extra or {}
|
||||||
@@ -643,9 +567,9 @@ async def calculate_fiat_amounts(
|
|||||||
|
|
||||||
|
|
||||||
async def check_transaction_status(
|
async def check_transaction_status(
|
||||||
wallet_id: str, payment_hash: str, conn: Connection | None = None
|
wallet_id: str, payment_hash: str, conn: Optional[Connection] = None
|
||||||
) -> PaymentStatus:
|
) -> PaymentStatus:
|
||||||
payment: Payment | None = await get_wallet_payment(
|
payment: Optional[Payment] = await get_wallet_payment(
|
||||||
wallet_id, payment_hash, conn=conn
|
wallet_id, payment_hash, conn=conn
|
||||||
)
|
)
|
||||||
if not payment:
|
if not payment:
|
||||||
@@ -659,7 +583,7 @@ async def check_transaction_status(
|
|||||||
|
|
||||||
async def get_payments_daily_stats(
|
async def get_payments_daily_stats(
|
||||||
filters: Filters[PaymentFilters],
|
filters: Filters[PaymentFilters],
|
||||||
user_id: str | None = None,
|
user_id: Optional[str] = None,
|
||||||
) -> list[PaymentDailyStats]:
|
) -> list[PaymentDailyStats]:
|
||||||
data_in, data_out = await get_daily_stats(filters, user_id=user_id)
|
data_in, data_out = await get_daily_stats(filters, user_id=user_id)
|
||||||
balance_total: float = 0
|
balance_total: float = 0
|
||||||
@@ -708,7 +632,7 @@ async def get_payments_daily_stats(
|
|||||||
async def _pay_invoice(
|
async def _pay_invoice(
|
||||||
wallet_id: str,
|
wallet_id: str,
|
||||||
create_payment_model: CreatePayment,
|
create_payment_model: CreatePayment,
|
||||||
conn: Connection | None = None,
|
conn: Optional[Connection] = None,
|
||||||
):
|
):
|
||||||
async with payment_lock:
|
async with payment_lock:
|
||||||
if wallet_id not in wallets_payments_lock:
|
if wallet_id not in wallets_payments_lock:
|
||||||
@@ -731,8 +655,8 @@ async def _pay_invoice(
|
|||||||
async def _pay_internal_invoice(
|
async def _pay_internal_invoice(
|
||||||
wallet: Wallet,
|
wallet: Wallet,
|
||||||
create_payment_model: CreatePayment,
|
create_payment_model: CreatePayment,
|
||||||
conn: Connection | None = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> Payment | None:
|
) -> Optional[Payment]:
|
||||||
"""
|
"""
|
||||||
Pay an internal payment.
|
Pay an internal payment.
|
||||||
returns None if the payment is not internal.
|
returns None if the payment is not internal.
|
||||||
@@ -799,7 +723,7 @@ async def _pay_internal_invoice(
|
|||||||
async def _pay_external_invoice(
|
async def _pay_external_invoice(
|
||||||
wallet: Wallet,
|
wallet: Wallet,
|
||||||
create_payment_model: CreatePayment,
|
create_payment_model: CreatePayment,
|
||||||
conn: Connection | None = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> Payment:
|
) -> Payment:
|
||||||
checking_id = create_payment_model.payment_hash
|
checking_id = create_payment_model.payment_hash
|
||||||
amount_msat = create_payment_model.amount_msat
|
amount_msat = create_payment_model.amount_msat
|
||||||
@@ -868,7 +792,7 @@ async def _pay_external_invoice(
|
|||||||
async def update_payment_success_status(
|
async def update_payment_success_status(
|
||||||
payment: Payment,
|
payment: Payment,
|
||||||
status: PaymentStatus,
|
status: PaymentStatus,
|
||||||
conn: Connection | None = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> Payment:
|
) -> Payment:
|
||||||
if status.success:
|
if status.success:
|
||||||
service_fee_msat = service_fee(payment.amount, internal=False)
|
service_fee_msat = service_fee(payment.amount, internal=False)
|
||||||
@@ -892,7 +816,7 @@ async def _fundingsource_pay_invoice(
|
|||||||
|
|
||||||
|
|
||||||
async def _verify_external_payment(
|
async def _verify_external_payment(
|
||||||
payment: Payment, conn: Connection | None = None
|
payment: Payment, conn: Optional[Connection] = None
|
||||||
) -> Payment:
|
) -> Payment:
|
||||||
# fail on pending payments
|
# fail on pending payments
|
||||||
if payment.pending:
|
if payment.pending:
|
||||||
@@ -923,7 +847,7 @@ async def _check_wallet_for_payment(
|
|||||||
wallet_id: str,
|
wallet_id: str,
|
||||||
tag: str,
|
tag: str,
|
||||||
amount_msat: int,
|
amount_msat: int,
|
||||||
conn: Connection | None = None,
|
conn: Optional[Connection] = None,
|
||||||
):
|
):
|
||||||
wallet = await get_wallet(wallet_id, conn=conn)
|
wallet = await get_wallet(wallet_id, conn=conn)
|
||||||
if not wallet:
|
if not wallet:
|
||||||
@@ -939,7 +863,7 @@ async def _check_wallet_for_payment(
|
|||||||
|
|
||||||
|
|
||||||
def _validate_payment_request(
|
def _validate_payment_request(
|
||||||
payment_request: str, max_sat: int | None = None
|
payment_request: str, max_sat: Optional[int] = None
|
||||||
) -> Bolt11:
|
) -> Bolt11:
|
||||||
try:
|
try:
|
||||||
invoice = bolt11_decode(payment_request)
|
invoice = bolt11_decode(payment_request)
|
||||||
@@ -962,16 +886,12 @@ def _validate_payment_request(
|
|||||||
|
|
||||||
|
|
||||||
async def _credit_service_fee_wallet(
|
async def _credit_service_fee_wallet(
|
||||||
wallet: Wallet, payment: Payment, conn: Connection | None = None
|
payment: Payment, memo: str, conn: Optional[Connection] = None
|
||||||
):
|
):
|
||||||
service_fee_msat = service_fee(payment.amount, internal=payment.is_internal)
|
service_fee_msat = service_fee(payment.amount, internal=payment.is_internal)
|
||||||
if not settings.lnbits_service_fee_wallet or not service_fee_msat:
|
if not settings.lnbits_service_fee_wallet or not service_fee_msat:
|
||||||
return
|
return
|
||||||
|
|
||||||
memo = f"""
|
|
||||||
Service fee for payment of {abs(payment.sat)} sats.
|
|
||||||
Wallet: '{wallet.name}' ({wallet.id})."""
|
|
||||||
|
|
||||||
create_payment_model = CreatePayment(
|
create_payment_model = CreatePayment(
|
||||||
wallet_id=settings.lnbits_service_fee_wallet,
|
wallet_id=settings.lnbits_service_fee_wallet,
|
||||||
bolt11=payment.bolt11,
|
bolt11=payment.bolt11,
|
||||||
@@ -988,7 +908,7 @@ async def _credit_service_fee_wallet(
|
|||||||
|
|
||||||
|
|
||||||
async def _check_fiat_invoice_limits(
|
async def _check_fiat_invoice_limits(
|
||||||
amount_sat: int, fiat_provider_name: str, conn: Connection | None = None
|
amount_sat: int, fiat_provider_name: str, conn: Optional[Connection] = None
|
||||||
):
|
):
|
||||||
limits = settings.get_fiat_provider_limits(fiat_provider_name)
|
limits = settings.get_fiat_provider_limits(fiat_provider_name)
|
||||||
if not limits:
|
if not limits:
|
||||||
@@ -1029,40 +949,3 @@ async def _check_fiat_invoice_limits(
|
|||||||
f"The amount exceeds the '{fiat_provider_name}'"
|
f"The amount exceeds the '{fiat_provider_name}'"
|
||||||
"faucet wallet balance.",
|
"faucet wallet balance.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def settle_hold_invoice(payment: Payment, preimage: str) -> InvoiceResponse:
|
|
||||||
if verify_preimage(preimage, payment.payment_hash) is False:
|
|
||||||
raise InvoiceError("Invalid preimage.", status="failed")
|
|
||||||
|
|
||||||
funding_source = get_funding_source()
|
|
||||||
response = await funding_source.settle_hold_invoice(preimage=preimage)
|
|
||||||
|
|
||||||
if not response.ok:
|
|
||||||
raise InvoiceError(
|
|
||||||
response.error_message or "Unexpected backend error.", status="failed"
|
|
||||||
)
|
|
||||||
|
|
||||||
payment.preimage = preimage
|
|
||||||
payment.extra["hold_invoice_settled"] = True
|
|
||||||
await update_payment(payment)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
async def cancel_hold_invoice(payment: Payment) -> InvoiceResponse:
|
|
||||||
funding_source = get_funding_source()
|
|
||||||
response = await funding_source.cancel_hold_invoice(
|
|
||||||
payment_hash=payment.payment_hash
|
|
||||||
)
|
|
||||||
|
|
||||||
if not response.ok:
|
|
||||||
raise InvoiceError(
|
|
||||||
response.error_message or "Unexpected backend error.", status="failed"
|
|
||||||
)
|
|
||||||
|
|
||||||
payment.status = PaymentState.FAILED
|
|
||||||
payment.extra["hold_invoice_cancelled"] = True
|
|
||||||
await update_payment(payment)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -36,7 +37,7 @@ from .settings import update_cached_settings
|
|||||||
|
|
||||||
|
|
||||||
async def create_user_account(
|
async def create_user_account(
|
||||||
account: Account | None = None, wallet_name: str | None = None
|
account: Optional[Account] = None, wallet_name: Optional[str] = None
|
||||||
) -> User:
|
) -> User:
|
||||||
if not settings.new_accounts_allowed:
|
if not settings.new_accounts_allowed:
|
||||||
raise ValueError("Account creation is disabled.")
|
raise ValueError("Account creation is disabled.")
|
||||||
@@ -45,9 +46,9 @@ async def create_user_account(
|
|||||||
|
|
||||||
|
|
||||||
async def create_user_account_no_ckeck(
|
async def create_user_account_no_ckeck(
|
||||||
account: Account | None = None,
|
account: Optional[Account] = None,
|
||||||
wallet_name: str | None = None,
|
wallet_name: Optional[str] = None,
|
||||||
default_exts: list[str] | None = None,
|
default_exts: Optional[list[str]] = None,
|
||||||
) -> User:
|
) -> User:
|
||||||
|
|
||||||
if account:
|
if account:
|
||||||
@@ -164,12 +165,12 @@ async def check_admin_settings():
|
|||||||
settings.first_install = True
|
settings.first_install = True
|
||||||
|
|
||||||
logger.success(
|
logger.success(
|
||||||
"✔️ Admin UI is enabled. run `uv run lnbits-cli superuser` "
|
"✔️ Admin UI is enabled. run `poetry run lnbits-cli superuser` "
|
||||||
"to get the superuser."
|
"to get the superuser."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def init_admin_settings(super_user: str | None = None) -> SuperSettings:
|
async def init_admin_settings(super_user: Optional[str] = None) -> SuperSettings:
|
||||||
account = None
|
account = None
|
||||||
if super_user:
|
if super_user:
|
||||||
account = await get_account(super_user)
|
account = await get_account(super_user)
|
||||||
|
|||||||
@@ -1,65 +1,27 @@
|
|||||||
from asyncio import Queue
|
from fastapi import WebSocket
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
from fastapi import WebSocket, WebSocketDisconnect
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.settings import settings
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class WebsocketConnection:
|
|
||||||
item_id: str
|
|
||||||
websocket: WebSocket
|
|
||||||
receive_queue: Queue[str]
|
|
||||||
|
|
||||||
|
|
||||||
class WebsocketConnectionManager:
|
class WebsocketConnectionManager:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.active_connections: list[WebsocketConnection] = []
|
self.active_connections: list[WebSocket] = []
|
||||||
|
|
||||||
async def connect(self, item_id: str, websocket: WebSocket) -> WebsocketConnection:
|
async def connect(self, websocket: WebSocket, item_id: str):
|
||||||
logger.debug(f"Websocket connected to {item_id}")
|
logger.debug(f"Websocket connected to {item_id}")
|
||||||
await websocket.accept()
|
await websocket.accept()
|
||||||
conn = WebsocketConnection(
|
self.active_connections.append(websocket)
|
||||||
item_id=item_id,
|
|
||||||
websocket=websocket,
|
|
||||||
receive_queue=Queue(),
|
|
||||||
)
|
|
||||||
self.active_connections.append(conn)
|
|
||||||
return conn
|
|
||||||
|
|
||||||
async def listen(self, conn: WebsocketConnection) -> None:
|
def disconnect(self, websocket: WebSocket):
|
||||||
while settings.lnbits_running:
|
self.active_connections.remove(websocket)
|
||||||
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
|
|
||||||
|
|
||||||
def get_connections(self, item_id: str) -> list[WebsocketConnection]:
|
async def send_data(self, message: str, item_id: str):
|
||||||
conns = []
|
for connection in self.active_connections:
|
||||||
for conn in self.active_connections:
|
if connection.path_params["item_id"] == item_id:
|
||||||
if conn.item_id == item_id:
|
await connection.send_text(message)
|
||||||
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()
|
websocket_manager = WebsocketConnectionManager()
|
||||||
|
|
||||||
|
|
||||||
# deprecated import and use `websocket_manager.send()` instead
|
async def websocket_updater(item_id: str, data: str):
|
||||||
async def websocket_updater(item_id: str, data: str) -> None:
|
return await websocket_manager.send_data(data, item_id)
|
||||||
return await websocket_manager.send(item_id, data)
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import traceback
|
import traceback
|
||||||
from collections.abc import Callable, Coroutine
|
from collections.abc import Coroutine
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
@@ -21,7 +22,7 @@ from lnbits.core.services.funding_source import (
|
|||||||
get_balance_delta,
|
get_balance_delta,
|
||||||
)
|
)
|
||||||
from lnbits.core.services.notifications import (
|
from lnbits.core.services.notifications import (
|
||||||
enqueue_admin_notification,
|
enqueue_notification,
|
||||||
process_next_notification,
|
process_next_notification,
|
||||||
send_payment_notification,
|
send_payment_notification,
|
||||||
)
|
)
|
||||||
@@ -86,7 +87,7 @@ async def _notify_server_status() -> None:
|
|||||||
"lnbits_balance_sats": status.lnbits_balance_sats,
|
"lnbits_balance_sats": status.lnbits_balance_sats,
|
||||||
"node_balance_sats": status.node_balance_sats,
|
"node_balance_sats": status.node_balance_sats,
|
||||||
}
|
}
|
||||||
enqueue_admin_notification(NotificationType.server_status, values)
|
enqueue_notification(NotificationType.server_status, values)
|
||||||
|
|
||||||
|
|
||||||
async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue) -> None:
|
async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue) -> None:
|
||||||
@@ -122,7 +123,7 @@ async def wait_notification_messages() -> None:
|
|||||||
try:
|
try:
|
||||||
await process_next_notification()
|
await process_next_notification()
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning("Payment notification error", ex)
|
logger.log("error", ex)
|
||||||
await asyncio.sleep(3)
|
await asyncio.sleep(3)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,8 @@
|
|||||||
<div class="q-pa-md">
|
<div class="q-pa-md">
|
||||||
<lnbits-qrcode
|
<lnbits-qrcode
|
||||||
:value="wallet.adminkey"
|
:value="wallet.adminkey"
|
||||||
:show-buttons="false"
|
:options="{ width: 250 }"
|
||||||
|
class="rounded-borders"
|
||||||
></lnbits-qrcode>
|
></lnbits-qrcode>
|
||||||
</div>
|
</div>
|
||||||
</q-popup-proxy>
|
</q-popup-proxy>
|
||||||
@@ -85,7 +86,8 @@
|
|||||||
<div class="q-pa-md">
|
<div class="q-pa-md">
|
||||||
<lnbits-qrcode
|
<lnbits-qrcode
|
||||||
:value="wallet.inkey"
|
:value="wallet.inkey"
|
||||||
:show-buttons="false"
|
:options="{ width: 250 }"
|
||||||
|
class="rounded-borders"
|
||||||
></lnbits-qrcode>
|
></lnbits-qrcode>
|
||||||
</div>
|
</div>
|
||||||
</q-popup-proxy>
|
</q-popup-proxy>
|
||||||
|
|||||||
@@ -7,71 +7,38 @@
|
|||||||
<div class="row q-col-gutter-md q-mb-md">
|
<div class="row q-col-gutter-md q-mb-md">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<q-card>
|
<q-card>
|
||||||
<div class="q-pa-sm">
|
<div>
|
||||||
<div class="row items-center justify-between q-gutter-xs">
|
<div class="q-gutter-y-md">
|
||||||
<div class="col">
|
<q-tabs v-model="tab" align="left">
|
||||||
<q-btn @click="updateAccount" unelevated color="primary">
|
<q-tab
|
||||||
<span v-text="$t('update_account')"></span>
|
name="user"
|
||||||
</q-btn>
|
:label="$t('account_settings')"
|
||||||
</div>
|
@update="val => tab = val.name"
|
||||||
|
></q-tab>
|
||||||
|
<q-tab
|
||||||
|
name="theme"
|
||||||
|
:label="$t('look_and_feel')"
|
||||||
|
@update="val => tab = val.name"
|
||||||
|
></q-tab>
|
||||||
|
<q-tab
|
||||||
|
name="api_acls"
|
||||||
|
:label="$t('access_control_list')"
|
||||||
|
@update="val => tab = val.name"
|
||||||
|
></q-tab>
|
||||||
|
</q-tabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row q-col-gutter-md q-mb-md">
|
<div class="row q-col-gutter-md">
|
||||||
<div class="col-12">
|
<div v-if="user" class="col-md-12 col-lg-6 q-gutter-y-md">
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-splitter>
|
<q-card-section>
|
||||||
<template v-slot:before>
|
<div class="row">
|
||||||
<q-tabs v-model="tab" vertical active-color="primary">
|
<div class="col">
|
||||||
<q-tab
|
<q-tab-panels v-model="tab">
|
||||||
name="user"
|
|
||||||
icon="person"
|
|
||||||
:label="$q.screen.gt.sm ? $t('account_settings') : ''"
|
|
||||||
@update="val => tab = val.name"
|
|
||||||
>
|
|
||||||
<q-tooltip v-if="!$q.screen.gt.sm"
|
|
||||||
><span v-text="$t('account_settings')"></span
|
|
||||||
></q-tooltip>
|
|
||||||
</q-tab>
|
|
||||||
|
|
||||||
<q-tab
|
|
||||||
name="notifications"
|
|
||||||
icon="notifications"
|
|
||||||
:label="$q.screen.gt.sm ? $t('notifications') : ''"
|
|
||||||
@update="val => tab = val.name"
|
|
||||||
>
|
|
||||||
<q-tooltip v-if="!$q.screen.gt.sm"
|
|
||||||
><span v-text="$t('notifications')"></span
|
|
||||||
></q-tooltip>
|
|
||||||
</q-tab>
|
|
||||||
<q-tab
|
|
||||||
name="theme"
|
|
||||||
icon="palette"
|
|
||||||
:label="$q.screen.gt.sm ? $t('look_and_feel') : ''"
|
|
||||||
@update="val => tab = val.name"
|
|
||||||
>
|
|
||||||
<q-tooltip v-if="!$q.screen.gt.sm"
|
|
||||||
><span v-text="$t('look_and_feel')"></span
|
|
||||||
></q-tooltip>
|
|
||||||
</q-tab>
|
|
||||||
<q-tab
|
|
||||||
name="api_acls"
|
|
||||||
icon="lock"
|
|
||||||
:label="$q.screen.gt.sm ? $t('access_control_list') : ''"
|
|
||||||
@update="val => tab = val.name"
|
|
||||||
>
|
|
||||||
<q-tooltip v-if="!$q.screen.gt.sm"
|
|
||||||
><span v-text="$t('access_control_list')"></span
|
|
||||||
></q-tooltip>
|
|
||||||
</q-tab>
|
|
||||||
</q-tabs>
|
|
||||||
</template>
|
|
||||||
<template v-slot:after>
|
|
||||||
<q-scroll-area style="height: 80vh">
|
|
||||||
<q-tab-panels v-if="user" v-model="tab">
|
|
||||||
<q-tab-panel name="user">
|
<q-tab-panel name="user">
|
||||||
<div v-if="credentialsData.show">
|
<div v-if="credentialsData.show">
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
@@ -326,6 +293,9 @@
|
|||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-separator></q-separator>
|
<q-separator></q-separator>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
|
<q-btn @click="updateAccount" unelevated color="primary">
|
||||||
|
<span v-text="$t('update_account')"></span>
|
||||||
|
</q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
@click="showUpdateCredentials()"
|
@click="showUpdateCredentials()"
|
||||||
:label="$t('change_password')"
|
:label="$t('change_password')"
|
||||||
@@ -589,101 +559,12 @@
|
|||||||
</q-select>
|
</q-select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-tab-panel>
|
<q-separator></q-separator>
|
||||||
<q-tab-panel name="notifications">
|
<div class="row q-mb-md q-mt-md">
|
||||||
<q-card-section>
|
<q-btn @click="updateAccount" unelevated color="primary">
|
||||||
<div class="row q-mb-md">
|
<span v-text="$t('update_account')"></span>
|
||||||
<div class="col-4">
|
</q-btn>
|
||||||
<span
|
</div>
|
||||||
v-text="$t('notifications_nostr_identifier')"
|
|
||||||
></span>
|
|
||||||
{%if not nostr_configured%}
|
|
||||||
<br />
|
|
||||||
<q-badge v-text="$t('not_connected')"></q-badge>
|
|
||||||
{%endif%}
|
|
||||||
</div>
|
|
||||||
<div class="col-8">
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model="user.extra.notifications.nostr_identifier"
|
|
||||||
:hint="$t('notifications_nostr_identifier_desc')"
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row q-mb-md">
|
|
||||||
<div class="col-4">
|
|
||||||
<span v-text="$t('notifications_chat_id')"></span>
|
|
||||||
{%if not telegram_configured%}
|
|
||||||
<br />
|
|
||||||
<q-badge v-text="$t('not_connected')"></q-badge>
|
|
||||||
{%endif%}
|
|
||||||
</div>
|
|
||||||
<div class="col-8">
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model="user.extra.notifications.telegram_chat_id"
|
|
||||||
:hint="$t('notifications_chat_id_desc')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<q-separator class="q-mb-md"></q-separator>
|
|
||||||
<div class="row q-mb-md">
|
|
||||||
<div class="col-4">
|
|
||||||
<span v-text="$t('notification_outgoing_payment')"></span>
|
|
||||||
</div>
|
|
||||||
<div class="col-8">
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
step="1"
|
|
||||||
v-model="user.extra.notifications.outgoing_payments_sats"
|
|
||||||
:hint="$t('notification_outgoing_payment_desc')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row q-mb-md">
|
|
||||||
<div class="col-4">
|
|
||||||
<span v-text="$t('notification_incoming_payment')"></span>
|
|
||||||
</div>
|
|
||||||
<div class="col-8">
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
step="1"
|
|
||||||
v-model="user.extra.notifications.incoming_payments_sats"
|
|
||||||
:hint="$t('notification_incoming_payment_desc')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row q-mb-md">
|
|
||||||
<div class="col-4">
|
|
||||||
<span v-text="$t('exclude_wallets')"></span>
|
|
||||||
</div>
|
|
||||||
<div class="col-8">
|
|
||||||
<q-select
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
emit-value
|
|
||||||
map-options
|
|
||||||
multiple
|
|
||||||
v-model="user.extra.notifications.excluded_wallets"
|
|
||||||
:options="g.user.walletOptions"
|
|
||||||
:label="$t('exclude_wallets')"
|
|
||||||
:hint="$t('notifications_excluded_wallets_desc')"
|
|
||||||
class="q-mt-sm"
|
|
||||||
>
|
|
||||||
</q-select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
<q-tab-panel name="api_acls">
|
<q-tab-panel name="api_acls">
|
||||||
<div class="row q-mb-md">
|
<div class="row q-mb-md">
|
||||||
@@ -910,9 +791,16 @@
|
|||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
</q-tab-panels>
|
</q-tab-panels>
|
||||||
</q-scroll-area>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
</q-splitter>
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
<div v-else class="col-12 col-md-6 q-gutter-y-md">
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<h4 class="q-my-none"><span v-text="$t('account')"></span></h4>
|
||||||
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -328,10 +328,13 @@
|
|||||||
<q-card v-if="selectedRelease" class="q-pa-lg lnbits__dialog-card">
|
<q-card v-if="selectedRelease" class="q-pa-lg lnbits__dialog-card">
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div v-if="selectedRelease.paymentRequest">
|
<div v-if="selectedRelease.paymentRequest">
|
||||||
<lnbits-qrcode
|
<a :href="'lightning:' + selectedRelease.paymentRequest">
|
||||||
:value="'lightning:' + selectedRelease.paymentRequest.toUpperCase()"
|
<q-responsive :ratio="1" class="q-mx-xl">
|
||||||
:href="'lightning:' + selectedRelease.paymentRequest"
|
<lnbits-qrcode
|
||||||
></lnbits-qrcode>
|
:value="'lightning:' + selectedRelease.paymentRequest.toUpperCase()"
|
||||||
|
></lnbits-qrcode>
|
||||||
|
</q-responsive>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<q-spinner-bars color="primary" size="2.55em"></q-spinner-bars>
|
<q-spinner-bars color="primary" size="2.55em"></q-spinner-bars>
|
||||||
@@ -699,10 +702,13 @@
|
|||||||
<q-card-section v-if="selectedExtension.payToEnable.showQRCode">
|
<q-card-section v-if="selectedExtension.payToEnable.showQRCode">
|
||||||
<div class="row q-mt-lg">
|
<div class="row q-mt-lg">
|
||||||
<div v-if="selectedExtension.payToEnable.paymentRequest" class="col">
|
<div v-if="selectedExtension.payToEnable.paymentRequest" class="col">
|
||||||
<lnbits-qrcode
|
<a
|
||||||
:value="'lightning:' + selectedExtension.payToEnable.paymentRequest.toUpperCase()"
|
|
||||||
:href="'lightning:' + selectedExtension.payToEnable.paymentRequest"
|
:href="'lightning:' + selectedExtension.payToEnable.paymentRequest"
|
||||||
></lnbits-qrcode>
|
>
|
||||||
|
<lnbits-qrcode
|
||||||
|
:value="'lightning:' + selectedExtension.payToEnable.paymentRequest.toUpperCase()"
|
||||||
|
></lnbits-qrcode>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="col">
|
<div v-else class="col">
|
||||||
<q-spinner-bars color="primary" size="2.55em"></q-spinner-bars>
|
<q-spinner-bars color="primary" size="2.55em"></q-spinner-bars>
|
||||||
|
|||||||
@@ -209,6 +209,16 @@
|
|||||||
></payment-list>
|
></payment-list>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</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>
|
</div>
|
||||||
{% if HIDE_API %}
|
{% if HIDE_API %}
|
||||||
<div class="col-12 col-md-4 q-gutter-y-md">
|
<div class="col-12 col-md-4 q-gutter-y-md">
|
||||||
@@ -267,101 +277,21 @@
|
|||||||
:label="$t('drain_funds')"
|
:label="$t('drain_funds')"
|
||||||
>
|
>
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section class="text-center">
|
||||||
<lnbits-qrcode
|
<a href="lightning:{{wallet.lnurlwithdraw_full}}">
|
||||||
value="lightning:{{wallet.lnurlwithdraw_full}}"
|
<lnbits-qrcode
|
||||||
href="lightning:{{wallet.lnurlwithdraw_full}}"
|
:value="lightning:{{wallet.lnurlwithdraw_full}}"
|
||||||
></lnbits-qrcode>
|
></lnbits-qrcode>
|
||||||
<p
|
</a>
|
||||||
class="text-center"
|
<p v-text="$t('drain_funds_desc')"></p>
|
||||||
v-text="$t('drain_funds_desc')"
|
|
||||||
></p>
|
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-expansion-item>
|
</q-expansion-item>
|
||||||
<q-separator></q-separator>
|
<q-separator></q-separator>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<q-expansion-item
|
<q-expansion-item
|
||||||
group="extras"
|
v-if="'{{ LNBITS_DENOMINATION }}' == 'sats'"
|
||||||
icon="qr_code"
|
|
||||||
v-if="stored_paylinks.length > 0"
|
|
||||||
:label="$t('stored_paylinks')"
|
|
||||||
>
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<div class="row flex" v-for="paylink in stored_paylinks">
|
|
||||||
<q-btn
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
color="primary"
|
|
||||||
icon="send"
|
|
||||||
size="xs"
|
|
||||||
@click="sendToPaylink(paylink.lnurl)"
|
|
||||||
>
|
|
||||||
<q-tooltip>
|
|
||||||
<span v-text="`send to: ${paylink.lnurl}`"></span>
|
|
||||||
</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<q-btn
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
color="secondary"
|
|
||||||
icon="content_copy"
|
|
||||||
size="xs"
|
|
||||||
@click="copyText(paylink.lnurl)"
|
|
||||||
>
|
|
||||||
<q-tooltip>
|
|
||||||
<span v-text="`copy: ${paylink.lnurl}`"></span>
|
|
||||||
</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<span
|
|
||||||
v-text="paylink.label"
|
|
||||||
class="q-mr-xs q-ml-xs"
|
|
||||||
></span>
|
|
||||||
<q-btn dense flat color="primary" icon="edit" size="xs">
|
|
||||||
<q-popup-edit
|
|
||||||
@update:model-value="editPaylink()"
|
|
||||||
v-model="paylink.label"
|
|
||||||
v-slot="scope"
|
|
||||||
>
|
|
||||||
<q-input
|
|
||||||
dark
|
|
||||||
color="white"
|
|
||||||
v-model="scope.value"
|
|
||||||
dense
|
|
||||||
autofocus
|
|
||||||
counter
|
|
||||||
@keyup.enter="scope.set"
|
|
||||||
>
|
|
||||||
<template v-slot:append>
|
|
||||||
<q-icon name="edit" />
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
</q-popup-edit>
|
|
||||||
<q-tooltip>
|
|
||||||
<span v-text="$t('edit')"></span>
|
|
||||||
</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<span style="flex-grow: 1"></span>
|
|
||||||
<q-btn
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
color="red"
|
|
||||||
icon="delete"
|
|
||||||
size="xs"
|
|
||||||
@click="deletePaylink(paylink.lnurl)"
|
|
||||||
>
|
|
||||||
<q-tooltip>
|
|
||||||
<span v-text="$t('delete')"></span>
|
|
||||||
</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<span v-text="dateFromNow(paylink.last_used)"></span>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
<q-separator></q-separator>
|
|
||||||
<q-expansion-item
|
|
||||||
group="extras"
|
group="extras"
|
||||||
icon="phone_android"
|
icon="phone_android"
|
||||||
:label="$t('access_wallet_on_mobile')"
|
:label="$t('access_wallet_on_mobile')"
|
||||||
@@ -403,15 +333,21 @@
|
|||||||
:label="$t('export_to_phone')"
|
:label="$t('export_to_phone')"
|
||||||
>
|
>
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section class="text-center">
|
||||||
<p
|
<p v-text="$t('export_to_phone_desc')"></p>
|
||||||
class="text-center"
|
|
||||||
v-text="$t('export_to_phone_desc')"
|
|
||||||
></p>
|
|
||||||
<lnbits-qrcode
|
<lnbits-qrcode
|
||||||
:value="`${baseUrl}wallet?usr=${g.user.id}&wal=${g.wallet.id}`"
|
:value="`${baseUrl}wallet?usr=${g.user.id}&wal=${g.wallet.id}`"
|
||||||
></lnbits-qrcode>
|
></lnbits-qrcode>
|
||||||
</q-card-section>
|
</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-card>
|
||||||
</q-expansion-item>
|
</q-expansion-item>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
@@ -725,13 +661,7 @@
|
|||||||
:readonly="receive.lnurl && receive.lnurl.fixed"
|
:readonly="receive.lnurl && receive.lnurl.fixed"
|
||||||
></q-input>
|
></q-input>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<q-input
|
|
||||||
v-if="has_holdinvoice"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model="receive.data.payment_hash"
|
|
||||||
:label="$t('hold_invoice_payment_hash')"
|
|
||||||
></q-input>
|
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
@@ -848,19 +778,18 @@
|
|||||||
v-else-if="receive.paymentReq && receive.lnurl == null"
|
v-else-if="receive.paymentReq && receive.lnurl == null"
|
||||||
class="q-pa-lg q-pt-xl lnbits__dialog-card"
|
class="q-pa-lg q-pt-xl lnbits__dialog-card"
|
||||||
>
|
>
|
||||||
<lnbits-qrcode
|
<div class="text-center q-mb-lg">
|
||||||
v-if="receive.fiatPaymentReq"
|
<a
|
||||||
:show-buttons="false"
|
v-if="receive.fiatPaymentReq"
|
||||||
:href="receive.fiatPaymentReq"
|
:href="receive.fiatPaymentReq"
|
||||||
:value="receive.fiatPaymentReq"
|
target="_blank"
|
||||||
>
|
>
|
||||||
</lnbits-qrcode>
|
<div v-html="invoiceQrCode"></div>
|
||||||
<lnbits-qrcode
|
</a>
|
||||||
v-else
|
<a v-else :href="'lightning:' + receive.paymentReq">
|
||||||
:href="'lightning:' + receive.paymentReq"
|
<div v-html="invoiceQrCode"></div>
|
||||||
:value="'lightning:' + receive.paymentReq"
|
</a>
|
||||||
>
|
</div>
|
||||||
</lnbits-qrcode>
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<h3 class="q-my-md">
|
<h3 class="q-my-md">
|
||||||
<span v-text="formattedAmount"></span>
|
<span v-text="formattedAmount"></span>
|
||||||
@@ -885,6 +814,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-mt-lg">
|
<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
|
<q-btn
|
||||||
v-close-popup
|
v-close-popup
|
||||||
flat
|
flat
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from pathlib import Path
|
|||||||
from shutil import make_archive, move
|
from shutil import make_archive, move
|
||||||
from subprocess import Popen
|
from subprocess import Popen
|
||||||
from tempfile import NamedTemporaryFile
|
from tempfile import NamedTemporaryFile
|
||||||
from typing import IO
|
from typing import IO, Optional
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import filetype
|
import filetype
|
||||||
@@ -16,7 +16,7 @@ from lnbits.core.models import User
|
|||||||
from lnbits.core.models.misc import Image, SimpleStatus
|
from lnbits.core.models.misc import Image, SimpleStatus
|
||||||
from lnbits.core.models.notifications import NotificationType
|
from lnbits.core.models.notifications import NotificationType
|
||||||
from lnbits.core.services import (
|
from lnbits.core.services import (
|
||||||
enqueue_admin_notification,
|
enqueue_notification,
|
||||||
get_balance_delta,
|
get_balance_delta,
|
||||||
update_cached_settings,
|
update_cached_settings,
|
||||||
)
|
)
|
||||||
@@ -65,16 +65,14 @@ async def api_monitor():
|
|||||||
)
|
)
|
||||||
async def api_test_email():
|
async def api_test_email():
|
||||||
return await send_email_notification(
|
return await send_email_notification(
|
||||||
settings.lnbits_email_notifications_to_emails,
|
"This is a LNbits test email.", "LNbits Test Email"
|
||||||
"This is a LNbits test email.",
|
|
||||||
"LNbits Test Email",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@admin_router.get("/api/v1/settings")
|
@admin_router.get("/api/v1/settings", response_model=Optional[AdminSettings])
|
||||||
async def api_get_settings(
|
async def api_get_settings(
|
||||||
user: User = Depends(check_admin),
|
user: User = Depends(check_admin),
|
||||||
) -> AdminSettings | None:
|
) -> Optional[AdminSettings]:
|
||||||
admin_settings = await get_admin_settings(user.super_user)
|
admin_settings = await get_admin_settings(user.super_user)
|
||||||
return admin_settings
|
return admin_settings
|
||||||
|
|
||||||
@@ -84,9 +82,7 @@ async def api_get_settings(
|
|||||||
status_code=HTTPStatus.OK,
|
status_code=HTTPStatus.OK,
|
||||||
)
|
)
|
||||||
async def api_update_settings(data: UpdateSettings, user: User = Depends(check_admin)):
|
async def api_update_settings(data: UpdateSettings, user: User = Depends(check_admin)):
|
||||||
enqueue_admin_notification(
|
enqueue_notification(NotificationType.settings_update, {"username": user.username})
|
||||||
NotificationType.settings_update, {"username": user.username}
|
|
||||||
)
|
|
||||||
await update_admin_settings(data)
|
await update_admin_settings(data)
|
||||||
admin_settings = await get_admin_settings(user.super_user)
|
admin_settings = await get_admin_settings(user.super_user)
|
||||||
if not admin_settings:
|
if not admin_settings:
|
||||||
@@ -117,9 +113,7 @@ async def api_reset_settings(field_name: str):
|
|||||||
|
|
||||||
@admin_router.delete("/api/v1/settings", status_code=HTTPStatus.OK)
|
@admin_router.delete("/api/v1/settings", status_code=HTTPStatus.OK)
|
||||||
async def api_delete_settings(user: User = Depends(check_super_user)) -> None:
|
async def api_delete_settings(user: User = Depends(check_super_user)) -> None:
|
||||||
enqueue_admin_notification(
|
enqueue_notification(NotificationType.settings_update, {"username": user.username})
|
||||||
NotificationType.settings_update, {"username": user.username}
|
|
||||||
)
|
|
||||||
await reset_core_settings()
|
await reset_core_settings()
|
||||||
server_restart.set()
|
server_restart.set()
|
||||||
|
|
||||||
@@ -147,7 +141,7 @@ async def api_download_backup() -> FileResponse:
|
|||||||
pg_backup_filename = f"{settings.lnbits_data_folder}/lnbits-database.dmp"
|
pg_backup_filename = f"{settings.lnbits_data_folder}/lnbits-database.dmp"
|
||||||
is_pg = db_url and db_url.startswith("postgres://")
|
is_pg = db_url and db_url.startswith("postgres://")
|
||||||
|
|
||||||
if is_pg and db_url:
|
if is_pg:
|
||||||
p = urlparse(db_url)
|
p = urlparse(db_url)
|
||||||
command = (
|
command = (
|
||||||
f"pg_dump --host={p.hostname} "
|
f"pg_dump --host={p.hostname} "
|
||||||
|
|||||||
+157
-3
@@ -1,20 +1,36 @@
|
|||||||
|
import hashlib
|
||||||
|
import json
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from time import time
|
from time import time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
|
||||||
|
|
||||||
|
import httpx
|
||||||
import pyqrcode
|
import pyqrcode
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import (
|
||||||
|
APIRouter,
|
||||||
|
Depends,
|
||||||
|
)
|
||||||
|
from fastapi.exceptions import HTTPException
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
|
|
||||||
from lnbits.core.models import (
|
from lnbits.core.models import (
|
||||||
BaseWallet,
|
BaseWallet,
|
||||||
ConversionData,
|
ConversionData,
|
||||||
|
CreateLnurlAuth,
|
||||||
CreateWallet,
|
CreateWallet,
|
||||||
User,
|
User,
|
||||||
Wallet,
|
Wallet,
|
||||||
)
|
)
|
||||||
from lnbits.decorators import check_user_exists
|
from lnbits.decorators import (
|
||||||
|
WalletTypeInfo,
|
||||||
|
check_user_exists,
|
||||||
|
require_admin_key,
|
||||||
|
require_invoice_key,
|
||||||
|
)
|
||||||
|
from lnbits.helpers import check_callback_url
|
||||||
|
from lnbits.lnurl import decode as lnurl_decode
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.utils.exchange_rates import (
|
from lnbits.utils.exchange_rates import (
|
||||||
allowed_currencies,
|
allowed_currencies,
|
||||||
@@ -25,7 +41,7 @@ from lnbits.utils.exchange_rates import (
|
|||||||
from lnbits.wallets import get_funding_source
|
from lnbits.wallets import get_funding_source
|
||||||
from lnbits.wallets.base import StatusResponse
|
from lnbits.wallets.base import StatusResponse
|
||||||
|
|
||||||
from ..services import create_user_account
|
from ..services import create_user_account, perform_lnurlauth
|
||||||
|
|
||||||
api_router = APIRouter(tags=["Core"])
|
api_router = APIRouter(tags=["Core"])
|
||||||
|
|
||||||
@@ -76,6 +92,144 @@ async def api_create_account(data: CreateWallet) -> Wallet:
|
|||||||
return user.wallets[0]
|
return user.wallets[0]
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.get("/api/v1/lnurlscan/{code}")
|
||||||
|
async def api_lnurlscan( # noqa: C901
|
||||||
|
code: str, wallet: WalletTypeInfo = Depends(require_invoice_key)
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
url = str(lnurl_decode(code))
|
||||||
|
domain = urlparse(url).netloc
|
||||||
|
except Exception as exc:
|
||||||
|
# parse internet identifier (user@domain.com)
|
||||||
|
name_domain = code.split("@")
|
||||||
|
if len(name_domain) == 2 and len(name_domain[1].split(".")) >= 2:
|
||||||
|
name, domain = name_domain
|
||||||
|
url = (
|
||||||
|
("http://" if domain.endswith(".onion") else "https://")
|
||||||
|
+ domain
|
||||||
|
+ "/.well-known/lnurlp/"
|
||||||
|
+ name
|
||||||
|
)
|
||||||
|
# will proceed with these values
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST, detail="invalid lnurl"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
# params is what will be returned to the client
|
||||||
|
params: dict = {"domain": domain}
|
||||||
|
|
||||||
|
if "tag=login" in url:
|
||||||
|
params.update(kind="auth")
|
||||||
|
params.update(callback=url) # with k1 already in it
|
||||||
|
|
||||||
|
lnurlauth_key = wallet.wallet.lnurlauth_key(domain)
|
||||||
|
if not lnurlauth_key.verifying_key:
|
||||||
|
raise ValueError("LNURL auth key not found for this domain.")
|
||||||
|
params.update(pubkey=lnurlauth_key.verifying_key.to_string("compressed").hex())
|
||||||
|
else:
|
||||||
|
headers = {"User-Agent": settings.user_agent}
|
||||||
|
async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client:
|
||||||
|
check_callback_url(url)
|
||||||
|
try:
|
||||||
|
r = await client.get(url, timeout=5)
|
||||||
|
r.raise_for_status()
|
||||||
|
except httpx.HTTPStatusError as exc:
|
||||||
|
if exc.response.status_code == 404:
|
||||||
|
raise HTTPException(HTTPStatus.NOT_FOUND, "Not found") from exc
|
||||||
|
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
|
||||||
|
detail={
|
||||||
|
"domain": domain,
|
||||||
|
"message": "failed to get parameters",
|
||||||
|
},
|
||||||
|
) from exc
|
||||||
|
try:
|
||||||
|
data = json.loads(r.text)
|
||||||
|
except json.decoder.JSONDecodeError as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
|
||||||
|
detail={
|
||||||
|
"domain": domain,
|
||||||
|
"message": f"got invalid response '{r.text[:200]}'",
|
||||||
|
},
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
try:
|
||||||
|
tag: str = data.get("tag")
|
||||||
|
params.update(**data)
|
||||||
|
if tag == "channelRequest":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
detail={
|
||||||
|
"domain": domain,
|
||||||
|
"kind": "channel",
|
||||||
|
"message": "unsupported",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
elif tag == "withdrawRequest":
|
||||||
|
params.update(kind="withdraw")
|
||||||
|
params.update(fixed=data["minWithdrawable"] == data["maxWithdrawable"])
|
||||||
|
|
||||||
|
# callback with k1 already in it
|
||||||
|
parsed_callback: ParseResult = urlparse(data["callback"])
|
||||||
|
qs: dict = parse_qs(parsed_callback.query)
|
||||||
|
qs["k1"] = data["k1"]
|
||||||
|
|
||||||
|
# balanceCheck/balanceNotify
|
||||||
|
if "balanceCheck" in data:
|
||||||
|
params.update(balanceCheck=data["balanceCheck"])
|
||||||
|
|
||||||
|
# format callback url and send to client
|
||||||
|
parsed_callback = parsed_callback._replace(
|
||||||
|
query=urlencode(qs, doseq=True)
|
||||||
|
)
|
||||||
|
params.update(callback=urlunparse(parsed_callback))
|
||||||
|
elif tag == "payRequest":
|
||||||
|
params.update(kind="pay")
|
||||||
|
params.update(fixed=data["minSendable"] == data["maxSendable"])
|
||||||
|
|
||||||
|
params.update(
|
||||||
|
description_hash=hashlib.sha256(
|
||||||
|
data["metadata"].encode()
|
||||||
|
).hexdigest()
|
||||||
|
)
|
||||||
|
metadata = json.loads(data["metadata"])
|
||||||
|
for [k, v] in metadata:
|
||||||
|
if k == "text/plain":
|
||||||
|
params.update(description=v)
|
||||||
|
if k in ("image/jpeg;base64", "image/png;base64"):
|
||||||
|
data_uri = f"data:{k},{v}"
|
||||||
|
params.update(image=data_uri)
|
||||||
|
if k in ("text/email", "text/identifier"):
|
||||||
|
params.update(targetUser=v)
|
||||||
|
params.update(commentAllowed=data.get("commentAllowed", 0))
|
||||||
|
|
||||||
|
except KeyError as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
|
||||||
|
detail={
|
||||||
|
"domain": domain,
|
||||||
|
"message": f"lnurl JSON response invalid: {exc}",
|
||||||
|
},
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
return params
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.post("/api/v1/lnurlauth")
|
||||||
|
async def api_perform_lnurlauth(
|
||||||
|
data: CreateLnurlAuth, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||||
|
):
|
||||||
|
err = await perform_lnurlauth(data.callback, wallet=wallet)
|
||||||
|
if err:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.SERVICE_UNAVAILABLE, detail=err.reason
|
||||||
|
)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
@api_router.get(
|
@api_router.get(
|
||||||
"/api/v1/rate/history",
|
"/api/v1/rate/history",
|
||||||
dependencies=[Depends(check_user_exists)],
|
dependencies=[Depends(check_user_exists)],
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import base64
|
import base64
|
||||||
import importlib
|
import importlib
|
||||||
import json
|
import json
|
||||||
from collections.abc import Callable
|
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from time import time
|
from time import time
|
||||||
|
from typing import Callable, Optional
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
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")
|
@auth_router.get("/{provider}", description="SSO Provider")
|
||||||
async def login_with_sso_provider(
|
async def login_with_sso_provider(
|
||||||
request: Request, provider: str, user_id: str | None = None
|
request: Request, provider: str, user_id: Optional[str] = None
|
||||||
):
|
):
|
||||||
provider_sso = _new_sso(provider)
|
provider_sso = _new_sso(provider)
|
||||||
if not provider_sso:
|
if not provider_sso:
|
||||||
@@ -319,7 +319,7 @@ async def update_pubkey(
|
|||||||
data: UpdateUserPubkey,
|
data: UpdateUserPubkey,
|
||||||
user: User = Depends(check_user_exists),
|
user: User = Depends(check_user_exists),
|
||||||
payload: AccessTokenPayload = Depends(access_token_payload),
|
payload: AccessTokenPayload = Depends(access_token_payload),
|
||||||
) -> User | None:
|
) -> Optional[User]:
|
||||||
if data.user_id != user.id:
|
if data.user_id != user.id:
|
||||||
raise ValueError("Invalid user ID.")
|
raise ValueError("Invalid user ID.")
|
||||||
|
|
||||||
@@ -345,7 +345,7 @@ async def update_password(
|
|||||||
data: UpdateUserPassword,
|
data: UpdateUserPassword,
|
||||||
user: User = Depends(check_user_exists),
|
user: User = Depends(check_user_exists),
|
||||||
payload: AccessTokenPayload = Depends(access_token_payload),
|
payload: AccessTokenPayload = Depends(access_token_payload),
|
||||||
) -> User | None:
|
) -> Optional[User]:
|
||||||
_validate_auth_timeout(payload.auth_time)
|
_validate_auth_timeout(payload.auth_time)
|
||||||
if data.user_id != user.id:
|
if data.user_id != user.id:
|
||||||
raise ValueError("Invalid user ID.")
|
raise ValueError("Invalid user ID.")
|
||||||
@@ -419,7 +419,7 @@ async def reset_password(data: ResetUserPassword) -> JSONResponse:
|
|||||||
@auth_router.put("/update")
|
@auth_router.put("/update")
|
||||||
async def update(
|
async def update(
|
||||||
data: UpdateUser, user: User = Depends(check_user_exists)
|
data: UpdateUser, user: User = Depends(check_user_exists)
|
||||||
) -> User | None:
|
) -> Optional[User]:
|
||||||
if data.user_id != user.id:
|
if data.user_id != user.id:
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid user ID.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid user ID.")
|
||||||
if data.username and not is_valid_username(data.username):
|
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)
|
return _auth_success_response(account.username, account.id, account.email)
|
||||||
|
|
||||||
|
|
||||||
async def _handle_sso_login(userinfo: OpenID, verified_user_id: str | None = None):
|
async def _handle_sso_login(userinfo: OpenID, verified_user_id: Optional[str] = None):
|
||||||
email = userinfo.email
|
email = userinfo.email
|
||||||
if not email or not is_valid_email_address(email):
|
if not email or not is_valid_email_address(email):
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.")
|
||||||
@@ -490,9 +490,9 @@ async def _handle_sso_login(userinfo: OpenID, verified_user_id: str | None = Non
|
|||||||
|
|
||||||
|
|
||||||
def _auth_success_response(
|
def _auth_success_response(
|
||||||
username: str | None = None,
|
username: Optional[str] = None,
|
||||||
user_id: str | None = None,
|
user_id: Optional[str] = None,
|
||||||
email: str | None = None,
|
email: Optional[str] = None,
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
payload = AccessTokenPayload(
|
payload = AccessTokenPayload(
|
||||||
sub=username or "", usr=user_id, email=email, auth_time=int(time())
|
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
|
return response
|
||||||
|
|
||||||
|
|
||||||
def _new_sso(provider: str) -> SSOBase | None:
|
def _new_sso(provider: str) -> Optional[SSOBase]:
|
||||||
try:
|
try:
|
||||||
if not settings.is_auth_method_allowed(AuthMethods(f"{provider}-auth")):
|
if not settings.is_auth_method_allowed(AuthMethods(f"{provider}-auth")):
|
||||||
return None
|
return None
|
||||||
@@ -610,7 +610,7 @@ def _nostr_nip98_event(request: Request) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def _check_nostr_event_tags(event: dict):
|
def _check_nostr_event_tags(event: dict):
|
||||||
method: str | None = next((v for k, v in event["tags"] if k == "method"), None)
|
method: Optional[str] = next((v for k, v in event["tags"] if k == "method"), None)
|
||||||
if not method:
|
if not method:
|
||||||
raise ValueError("Tag 'method' is missing.")
|
raise ValueError("Tag 'method' is missing.")
|
||||||
if not method.upper() == "POST":
|
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}'.")
|
raise ValueError(f"Invalid value for tag 'u': '{url}'.")
|
||||||
|
|
||||||
|
|
||||||
def _validate_auth_timeout(auth_time: int | None = 0):
|
def _validate_auth_timeout(auth_time: Optional[int] = 0):
|
||||||
if abs(time() - (auth_time or 0)) > settings.auth_credetials_update_threshold:
|
if abs(time() - (auth_time or 0)) > settings.auth_credetials_update_threshold:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.BAD_REQUEST,
|
HTTPStatus.BAD_REQUEST,
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ async def api_disable_extension(
|
|||||||
return SimpleStatus(
|
return SimpleStatus(
|
||||||
success=True, message=f"Extension '{ext_id}' already disabled."
|
success=True, message=f"Extension '{ext_id}' already disabled."
|
||||||
)
|
)
|
||||||
logger.info(f"Disabling extension: {ext_id}.")
|
logger.info(f"Disabeling extension: {ext_id}.")
|
||||||
user_ext.active = False
|
user_ext.active = False
|
||||||
await update_user_extension(user_ext)
|
await update_user_extension(user_ext)
|
||||||
return SimpleStatus(success=True, message=f"Extension '{ext_id}' disabled.")
|
return SimpleStatus(success=True, message=f"Extension '{ext_id}' disabled.")
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
from lnbits.core.models.misc import SimpleStatus
|
from lnbits.core.models.misc import SimpleStatus
|
||||||
from lnbits.core.services.fiat_providers import test_connection
|
from lnbits.core.services.fiat_providers import test_connection
|
||||||
from lnbits.decorators import check_admin
|
from lnbits.decorators import check_admin
|
||||||
from lnbits.fiat import StripeWallet, get_fiat_provider
|
|
||||||
|
|
||||||
fiat_router = APIRouter(tags=["Fiat API"], prefix="/api/v1/fiat")
|
fiat_router = APIRouter(tags=["Fiat API"], prefix="/api/v1/fiat")
|
||||||
|
|
||||||
@@ -17,29 +16,3 @@ fiat_router = APIRouter(tags=["Fiat API"], prefix="/api/v1/fiat")
|
|||||||
)
|
)
|
||||||
async def api_test_fiat_provider(provider: str) -> SimpleStatus:
|
async def api_test_fiat_provider(provider: str) -> SimpleStatus:
|
||||||
return await test_connection(provider)
|
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 http import HTTPStatus
|
||||||
from typing import Annotated
|
from typing import Annotated, Optional, Union
|
||||||
from urllib.parse import urlencode, urlparse
|
from urllib.parse import urlencode, urlparse
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
@@ -7,7 +7,7 @@ from fastapi import Cookie, Depends, Query, Request
|
|||||||
from fastapi.exceptions import HTTPException
|
from fastapi.exceptions import HTTPException
|
||||||
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
|
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
|
||||||
from fastapi.routing import APIRouter
|
from fastapi.routing import APIRouter
|
||||||
from lnurl import url_decode
|
from lnurl import decode as lnurl_decode
|
||||||
from pydantic.types import UUID4
|
from pydantic.types import UUID4
|
||||||
|
|
||||||
from lnbits.core.helpers import to_valid_user_id
|
from lnbits.core.helpers import to_valid_user_id
|
||||||
@@ -161,9 +161,9 @@ async def extensions(request: Request, user: User = Depends(check_user_exists)):
|
|||||||
)
|
)
|
||||||
async def wallet(
|
async def wallet(
|
||||||
request: Request,
|
request: Request,
|
||||||
lnbits_last_active_wallet: Annotated[str | None, Cookie()] = None,
|
lnbits_last_active_wallet: Annotated[Union[str, None], Cookie()] = None,
|
||||||
user: User = Depends(check_user_exists),
|
user: User = Depends(check_user_exists),
|
||||||
wal: UUID4 | None = Query(None),
|
wal: Optional[UUID4] = Query(None),
|
||||||
):
|
):
|
||||||
if wal:
|
if wal:
|
||||||
wallet = await get_wallet(wal.hex)
|
wallet = await get_wallet(wal.hex)
|
||||||
@@ -206,15 +206,11 @@ async def account(
|
|||||||
request: Request,
|
request: Request,
|
||||||
user: User = Depends(check_user_exists),
|
user: User = Depends(check_user_exists),
|
||||||
):
|
):
|
||||||
nostr_configured = settings.is_nostr_notifications_configured()
|
|
||||||
telegram_configured = settings.is_telegram_notifications_configured()
|
|
||||||
return template_renderer().TemplateResponse(
|
return template_renderer().TemplateResponse(
|
||||||
request,
|
request,
|
||||||
"core/account.html",
|
"core/account.html",
|
||||||
{
|
{
|
||||||
"user": user.json(),
|
"user": user.json(),
|
||||||
"nostr_configured": nostr_configured,
|
|
||||||
"telegram_configured": telegram_configured,
|
|
||||||
"ajax": _is_ajax_request(request),
|
"ajax": _is_ajax_request(request),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -345,6 +341,7 @@ async def node(request: Request, user: User = Depends(check_admin)):
|
|||||||
"node/index.html",
|
"node/index.html",
|
||||||
{
|
{
|
||||||
"user": user.json(),
|
"user": user.json(),
|
||||||
|
"settings": settings.dict(),
|
||||||
"balance": balance,
|
"balance": balance,
|
||||||
"wallets": user.wallets[0].json(),
|
"wallets": user.wallets[0].json(),
|
||||||
"ajax": _is_ajax_request(request),
|
"ajax": _is_ajax_request(request),
|
||||||
@@ -364,6 +361,7 @@ async def node_public(request: Request):
|
|||||||
request,
|
request,
|
||||||
"node/public.html",
|
"node/public.html",
|
||||||
{
|
{
|
||||||
|
"settings": settings.dict(),
|
||||||
"balance": balance,
|
"balance": balance,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -382,6 +380,7 @@ async def admin_index(request: Request, user: User = Depends(check_admin)):
|
|||||||
"admin/index.html",
|
"admin/index.html",
|
||||||
{
|
{
|
||||||
"user": user.json(),
|
"user": user.json(),
|
||||||
|
"settings": settings.dict(),
|
||||||
"balance": balance,
|
"balance": balance,
|
||||||
"currencies": list(currencies.keys()),
|
"currencies": list(currencies.keys()),
|
||||||
"ajax": _is_ajax_request(request),
|
"ajax": _is_ajax_request(request),
|
||||||
@@ -399,6 +398,7 @@ async def users_index(request: Request, user: User = Depends(check_admin)):
|
|||||||
{
|
{
|
||||||
"request": request,
|
"request": request,
|
||||||
"user": user.json(),
|
"user": user.json(),
|
||||||
|
"settings": settings.dict(),
|
||||||
"currencies": list(currencies.keys()),
|
"currencies": list(currencies.keys()),
|
||||||
"ajax": _is_ajax_request(request),
|
"ajax": _is_ajax_request(request),
|
||||||
},
|
},
|
||||||
@@ -456,7 +456,7 @@ async def lnurlwallet(request: Request, lightning: str = ""):
|
|||||||
if not settings.lnbits_allow_new_accounts:
|
if not settings.lnbits_allow_new_accounts:
|
||||||
return {"status": "ERROR", "reason": "New accounts are not allowed."}
|
return {"status": "ERROR", "reason": "New accounts are not allowed."}
|
||||||
|
|
||||||
lnurl = url_decode(lightning)
|
lnurl = lnurl_decode(lightning)
|
||||||
|
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
check_callback_url(lnurl)
|
check_callback_url(lnurl)
|
||||||
|
|||||||
@@ -1,136 +0,0 @@
|
|||||||
from http import HTTPStatus
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from fastapi import (
|
|
||||||
APIRouter,
|
|
||||||
Depends,
|
|
||||||
HTTPException,
|
|
||||||
)
|
|
||||||
from lnurl import LnurlResponseException
|
|
||||||
from lnurl import execute_login as lnurlauth
|
|
||||||
from lnurl import handle as lnurl_handle
|
|
||||||
from lnurl.models import (
|
|
||||||
LnurlAuthResponse,
|
|
||||||
LnurlErrorResponse,
|
|
||||||
LnurlPayResponse,
|
|
||||||
LnurlResponseModel,
|
|
||||||
LnurlWithdrawResponse,
|
|
||||||
)
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from lnbits.core.models import Payment
|
|
||||||
from lnbits.core.models.lnurl import CreateLnurlPayment, LnurlScan
|
|
||||||
from lnbits.decorators import (
|
|
||||||
WalletTypeInfo,
|
|
||||||
require_admin_key,
|
|
||||||
require_invoice_key,
|
|
||||||
)
|
|
||||||
from lnbits.helpers import check_callback_url
|
|
||||||
from lnbits.settings import settings
|
|
||||||
|
|
||||||
from ..services import fetch_lnurl_pay_request, pay_invoice
|
|
||||||
|
|
||||||
lnurl_router = APIRouter(tags=["LNURL"])
|
|
||||||
|
|
||||||
|
|
||||||
async def _handle(lnurl: str) -> LnurlResponseModel:
|
|
||||||
try:
|
|
||||||
res = await lnurl_handle(lnurl, user_agent=settings.user_agent, timeout=5)
|
|
||||||
if isinstance(res, LnurlErrorResponse):
|
|
||||||
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=res.reason)
|
|
||||||
except LnurlResponseException as exc:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST, detail=str(exc)
|
|
||||||
) from exc
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
@lnurl_router.get(
|
|
||||||
"/api/v1/lnurlscan/{code}",
|
|
||||||
dependencies=[Depends(require_invoice_key)],
|
|
||||||
deprecated=True,
|
|
||||||
response_model=LnurlPayResponse
|
|
||||||
| LnurlWithdrawResponse
|
|
||||||
| LnurlAuthResponse
|
|
||||||
| LnurlErrorResponse,
|
|
||||||
)
|
|
||||||
async def api_lnurlscan(code: str) -> LnurlResponseModel:
|
|
||||||
res = await _handle(code)
|
|
||||||
if isinstance(res, LnurlPayResponse | LnurlWithdrawResponse | LnurlAuthResponse):
|
|
||||||
check_callback_url(res.callback)
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
@lnurl_router.post(
|
|
||||||
"/api/v1/lnurlscan",
|
|
||||||
dependencies=[Depends(require_invoice_key)],
|
|
||||||
response_model=LnurlPayResponse
|
|
||||||
| LnurlWithdrawResponse
|
|
||||||
| LnurlAuthResponse
|
|
||||||
| LnurlErrorResponse,
|
|
||||||
)
|
|
||||||
async def api_lnurlscan_post(scan: LnurlScan) -> LnurlResponseModel:
|
|
||||||
return await _handle(scan.lnurl)
|
|
||||||
|
|
||||||
|
|
||||||
@lnurl_router.post("/api/v1/lnurlauth")
|
|
||||||
async def api_perform_lnurlauth(
|
|
||||||
data: LnurlAuthResponse, key_type: WalletTypeInfo = Depends(require_admin_key)
|
|
||||||
) -> LnurlResponseModel:
|
|
||||||
check_callback_url(data.callback)
|
|
||||||
try:
|
|
||||||
res = await lnurlauth(
|
|
||||||
res=data,
|
|
||||||
seed=key_type.wallet.adminkey,
|
|
||||||
user_agent=settings.user_agent,
|
|
||||||
timeout=5,
|
|
||||||
)
|
|
||||||
return res
|
|
||||||
except LnurlResponseException as exc:
|
|
||||||
logger.warning(exc)
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST, detail=str(exc)
|
|
||||||
) from exc
|
|
||||||
|
|
||||||
|
|
||||||
@lnurl_router.post("/api/v1/payments/lnurl")
|
|
||||||
async def api_payments_pay_lnurl(
|
|
||||||
data: CreateLnurlPayment, wallet: WalletTypeInfo = Depends(require_admin_key)
|
|
||||||
) -> Payment:
|
|
||||||
"""
|
|
||||||
Pay an LNURL payment request.
|
|
||||||
Either provice `res` (LnurlPayResponse) or `lnurl` (str) in the `data` object.
|
|
||||||
"""
|
|
||||||
if not data.res and not data.lnurl:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
|
||||||
detail="Missing LNURL or LnurlPayResponse data.",
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
res, res2 = await fetch_lnurl_pay_request(data=data, wallet=wallet.wallet)
|
|
||||||
except LnurlResponseException as exc:
|
|
||||||
logger.warning(exc)
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST, detail=str(exc)
|
|
||||||
) from exc
|
|
||||||
|
|
||||||
extra: dict[str, Any] = {}
|
|
||||||
if res2.disposable is False:
|
|
||||||
extra["stored"] = True
|
|
||||||
if res2.successAction:
|
|
||||||
extra["success_action"] = res2.successAction.json()
|
|
||||||
if data.comment:
|
|
||||||
extra["comment"] = data.comment
|
|
||||||
if data.unit and data.unit != "sat":
|
|
||||||
extra["fiat_currency"] = data.unit
|
|
||||||
extra["fiat_amount"] = data.amount / 1000
|
|
||||||
|
|
||||||
payment = await pay_invoice(
|
|
||||||
wallet_id=wallet.wallet.id,
|
|
||||||
payment_request=str(res2.pr),
|
|
||||||
description=res.metadata.text,
|
|
||||||
extra=extra,
|
|
||||||
)
|
|
||||||
|
|
||||||
return payment
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from fastapi import APIRouter, Body, Depends, HTTPException
|
from fastapi import APIRouter, Body, Depends, HTTPException
|
||||||
@@ -6,9 +7,8 @@ from pydantic import BaseModel
|
|||||||
from starlette.status import HTTP_503_SERVICE_UNAVAILABLE
|
from starlette.status import HTTP_503_SERVICE_UNAVAILABLE
|
||||||
|
|
||||||
from lnbits.decorators import check_admin, check_super_user, parse_filters
|
from lnbits.decorators import check_admin, check_super_user, parse_filters
|
||||||
|
from lnbits.nodes import get_node_class
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.wallets import get_funding_source
|
|
||||||
from lnbits.wallets.base import Feature
|
|
||||||
|
|
||||||
from ...db import Filters, Page
|
from ...db import Filters, Page
|
||||||
from ...nodes.base import (
|
from ...nodes.base import (
|
||||||
@@ -26,13 +26,9 @@ from ...nodes.base import (
|
|||||||
from ...utils.cache import cache
|
from ...utils.cache import cache
|
||||||
|
|
||||||
|
|
||||||
def require_node() -> Node:
|
def require_node():
|
||||||
funding_source = get_funding_source()
|
node_class = get_node_class()
|
||||||
if (
|
if not node_class:
|
||||||
not funding_source.features
|
|
||||||
or Feature.nodemanager not in funding_source.features
|
|
||||||
or not funding_source.__node_cls__
|
|
||||||
):
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.NOT_IMPLEMENTED,
|
status_code=HTTPStatus.NOT_IMPLEMENTED,
|
||||||
detail="Active backend does not implement Node API",
|
detail="Active backend does not implement Node API",
|
||||||
@@ -42,7 +38,7 @@ def require_node() -> Node:
|
|||||||
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
|
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
|
||||||
detail="Not enabled",
|
detail="Not enabled",
|
||||||
)
|
)
|
||||||
return funding_source.__node_cls__(funding_source)
|
return node_class
|
||||||
|
|
||||||
|
|
||||||
def check_public():
|
def check_public():
|
||||||
@@ -88,14 +84,14 @@ async def api_get_public_info(node: Node = Depends(require_node)) -> PublicNodeI
|
|||||||
@node_router.get("/info")
|
@node_router.get("/info")
|
||||||
async def api_get_info(
|
async def api_get_info(
|
||||||
node: Node = Depends(require_node),
|
node: Node = Depends(require_node),
|
||||||
) -> NodeInfoResponse | None:
|
) -> Optional[NodeInfoResponse]:
|
||||||
return await node.get_info()
|
return await node.get_info()
|
||||||
|
|
||||||
|
|
||||||
@node_router.get("/channels")
|
@node_router.get("/channels")
|
||||||
async def api_get_channels(
|
async def api_get_channels(
|
||||||
node: Node = Depends(require_node),
|
node: Node = Depends(require_node),
|
||||||
) -> list[NodeChannel] | None:
|
) -> Optional[list[NodeChannel]]:
|
||||||
return await node.get_channels()
|
return await node.get_channels()
|
||||||
|
|
||||||
|
|
||||||
@@ -103,7 +99,7 @@ async def api_get_channels(
|
|||||||
async def api_get_channel(
|
async def api_get_channel(
|
||||||
channel_id: str,
|
channel_id: str,
|
||||||
node: Node = Depends(require_node),
|
node: Node = Depends(require_node),
|
||||||
) -> NodeChannel | None:
|
) -> Optional[NodeChannel]:
|
||||||
return await node.get_channel(channel_id)
|
return await node.get_channel(channel_id)
|
||||||
|
|
||||||
|
|
||||||
@@ -112,20 +108,20 @@ async def api_create_channel(
|
|||||||
node: Node = Depends(require_node),
|
node: Node = Depends(require_node),
|
||||||
peer_id: str = Body(),
|
peer_id: str = Body(),
|
||||||
funding_amount: int = Body(),
|
funding_amount: int = Body(),
|
||||||
push_amount: int | None = Body(None),
|
push_amount: Optional[int] = Body(None),
|
||||||
fee_rate: int | None = Body(None),
|
fee_rate: Optional[int] = Body(None),
|
||||||
):
|
):
|
||||||
return await node.open_channel(peer_id, funding_amount, push_amount, fee_rate)
|
return await node.open_channel(peer_id, funding_amount, push_amount, fee_rate)
|
||||||
|
|
||||||
|
|
||||||
@super_node_router.delete("/channels")
|
@super_node_router.delete("/channels")
|
||||||
async def api_delete_channel(
|
async def api_delete_channel(
|
||||||
short_id: str | None,
|
short_id: Optional[str],
|
||||||
funding_txid: str | None,
|
funding_txid: Optional[str],
|
||||||
output_index: int | None,
|
output_index: Optional[int],
|
||||||
force: bool = False,
|
force: bool = False,
|
||||||
node: Node = Depends(require_node),
|
node: Node = Depends(require_node),
|
||||||
) -> list[NodeChannel] | None:
|
) -> Optional[list[NodeChannel]]:
|
||||||
return await node.close_channel(
|
return await node.close_channel(
|
||||||
short_id,
|
short_id,
|
||||||
(
|
(
|
||||||
@@ -151,7 +147,7 @@ async def api_set_channel_fees(
|
|||||||
async def api_get_payments(
|
async def api_get_payments(
|
||||||
node: Node = Depends(require_node),
|
node: Node = Depends(require_node),
|
||||||
filters: Filters = Depends(parse_filters(NodePaymentsFilters)),
|
filters: Filters = Depends(parse_filters(NodePaymentsFilters)),
|
||||||
) -> Page[NodePayment] | None:
|
) -> Optional[Page[NodePayment]]:
|
||||||
if not settings.lnbits_node_ui_transactions:
|
if not settings.lnbits_node_ui_transactions:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTP_503_SERVICE_UNAVAILABLE,
|
HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
@@ -164,7 +160,7 @@ async def api_get_payments(
|
|||||||
async def api_get_invoices(
|
async def api_get_invoices(
|
||||||
node: Node = Depends(require_node),
|
node: Node = Depends(require_node),
|
||||||
filters: Filters = Depends(parse_filters(NodeInvoiceFilters)),
|
filters: Filters = Depends(parse_filters(NodeInvoiceFilters)),
|
||||||
) -> Page[NodeInvoice] | None:
|
) -> Optional[Page[NodeInvoice]]:
|
||||||
if not settings.lnbits_node_ui_transactions:
|
if not settings.lnbits_node_ui_transactions:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTP_503_SERVICE_UNAVAILABLE,
|
HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
@@ -191,25 +187,25 @@ async def api_disconnect_peer(peer_id: str, node: Node = Depends(require_node)):
|
|||||||
|
|
||||||
|
|
||||||
class NodeRank(BaseModel):
|
class NodeRank(BaseModel):
|
||||||
capacity: int | None
|
capacity: Optional[int]
|
||||||
channelcount: int | None
|
channelcount: Optional[int]
|
||||||
age: int | None
|
age: Optional[int]
|
||||||
growth: int | None
|
growth: Optional[int]
|
||||||
availability: int | None
|
availability: Optional[int]
|
||||||
|
|
||||||
|
|
||||||
# Same for public and private api
|
# Same for public and private api
|
||||||
@node_router.get(
|
@node_router.get(
|
||||||
"/rank",
|
"/rank",
|
||||||
description="Retrieve node ranks from https://1ml.com",
|
description="Retrieve node ranks from https://1ml.com",
|
||||||
response_model=NodeRank | None,
|
response_model=Optional[NodeRank],
|
||||||
)
|
)
|
||||||
@public_node_router.get(
|
@public_node_router.get(
|
||||||
"/rank",
|
"/rank",
|
||||||
description="Retrieve node ranks from https://1ml.com",
|
description="Retrieve node ranks from https://1ml.com",
|
||||||
response_model=NodeRank | None,
|
response_model=Optional[NodeRank],
|
||||||
)
|
)
|
||||||
async def api_get_1ml_stats(node: Node = Depends(require_node)) -> NodeRank | None:
|
async def api_get_1ml_stats(node: Node = Depends(require_node)) -> Optional[NodeRank]:
|
||||||
node_id = await node.get_id()
|
node_id = await node.get_id()
|
||||||
headers = {"User-Agent": settings.user_agent}
|
headers = {"User-Agent": settings.user_agent}
|
||||||
async with httpx.AsyncClient(headers=headers) as client:
|
async with httpx.AsyncClient(headers=headers) as client:
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
from hashlib import sha256
|
import json
|
||||||
|
import ssl
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
from math import ceil
|
||||||
|
from typing import Optional
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
import httpx
|
||||||
from fastapi import (
|
from fastapi import (
|
||||||
APIRouter,
|
APIRouter,
|
||||||
Depends,
|
Depends,
|
||||||
@@ -9,7 +14,7 @@ from fastapi import (
|
|||||||
Query,
|
Query,
|
||||||
)
|
)
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from lnurl import url_decode
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits import bolt11
|
from lnbits import bolt11
|
||||||
from lnbits.core.crud.payments import (
|
from lnbits.core.crud.payments import (
|
||||||
@@ -17,11 +22,11 @@ from lnbits.core.crud.payments import (
|
|||||||
get_wallets_stats,
|
get_wallets_stats,
|
||||||
)
|
)
|
||||||
from lnbits.core.models import (
|
from lnbits.core.models import (
|
||||||
CancelInvoice,
|
|
||||||
CreateInvoice,
|
CreateInvoice,
|
||||||
CreateLnurlWithdraw,
|
CreateLnurl,
|
||||||
DecodePayment,
|
DecodePayment,
|
||||||
KeyType,
|
KeyType,
|
||||||
|
PayLnurlWData,
|
||||||
Payment,
|
Payment,
|
||||||
PaymentCountField,
|
PaymentCountField,
|
||||||
PaymentCountStat,
|
PaymentCountStat,
|
||||||
@@ -29,8 +34,6 @@ from lnbits.core.models import (
|
|||||||
PaymentFilters,
|
PaymentFilters,
|
||||||
PaymentHistoryPoint,
|
PaymentHistoryPoint,
|
||||||
PaymentWalletStats,
|
PaymentWalletStats,
|
||||||
SettleInvoice,
|
|
||||||
SimpleStatus,
|
|
||||||
)
|
)
|
||||||
from lnbits.core.models.users import User
|
from lnbits.core.models.users import User
|
||||||
from lnbits.db import Filters, Page
|
from lnbits.db import Filters, Page
|
||||||
@@ -42,10 +45,13 @@ from lnbits.decorators import (
|
|||||||
require_invoice_key,
|
require_invoice_key,
|
||||||
)
|
)
|
||||||
from lnbits.helpers import (
|
from lnbits.helpers import (
|
||||||
|
check_callback_url,
|
||||||
filter_dict_keys,
|
filter_dict_keys,
|
||||||
generate_filter_params_openapi,
|
generate_filter_params_openapi,
|
||||||
)
|
)
|
||||||
from lnbits.wallets.base import InvoiceResponse
|
from lnbits.lnurl import decode as lnurl_decode
|
||||||
|
from lnbits.settings import settings
|
||||||
|
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
|
||||||
|
|
||||||
from ..crud import (
|
from ..crud import (
|
||||||
DateTrunc,
|
DateTrunc,
|
||||||
@@ -54,15 +60,13 @@ from ..crud import (
|
|||||||
get_payments_paginated,
|
get_payments_paginated,
|
||||||
get_standalone_payment,
|
get_standalone_payment,
|
||||||
get_wallet_for_key,
|
get_wallet_for_key,
|
||||||
|
update_payment_extra,
|
||||||
)
|
)
|
||||||
from ..services import (
|
from ..services import (
|
||||||
cancel_hold_invoice,
|
|
||||||
create_payment_request,
|
create_payment_request,
|
||||||
fee_reserve_total,
|
fee_reserve_total,
|
||||||
get_payments_daily_stats,
|
get_payments_daily_stats,
|
||||||
pay_invoice,
|
pay_invoice,
|
||||||
perform_withdraw,
|
|
||||||
settle_hold_invoice,
|
|
||||||
update_pending_payment,
|
update_pending_payment,
|
||||||
update_pending_payments,
|
update_pending_payments,
|
||||||
)
|
)
|
||||||
@@ -260,6 +264,38 @@ async def api_payments_create(
|
|||||||
return await create_payment_request(wallet_id, invoice_data)
|
return await create_payment_request(wallet_id, invoice_data)
|
||||||
|
|
||||||
|
|
||||||
|
@payment_router.patch(
|
||||||
|
"/extra/{payment_hash}", description="Update extra data for a payment"
|
||||||
|
)
|
||||||
|
async def api_payments_update_extra(
|
||||||
|
payment_hash: str,
|
||||||
|
extra: dict,
|
||||||
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
|
) -> Payment:
|
||||||
|
payment = await get_standalone_payment(payment_hash, wallet_id=wallet.wallet.id)
|
||||||
|
if payment is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND, detail="Payment does not exist."
|
||||||
|
)
|
||||||
|
if not isinstance(extra, dict):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
detail="Extra data must be a dictionary.",
|
||||||
|
)
|
||||||
|
if payment.extra is None:
|
||||||
|
payment.extra = {}
|
||||||
|
|
||||||
|
for key, value in extra.items():
|
||||||
|
if key in payment.extra:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
detail=f"Key '{key}' already exists in extra data.",
|
||||||
|
)
|
||||||
|
payment.extra[key] = value
|
||||||
|
|
||||||
|
return await update_payment_extra(payment)
|
||||||
|
|
||||||
|
|
||||||
@payment_router.get("/fee-reserve")
|
@payment_router.get("/fee-reserve")
|
||||||
async def api_payments_fee_reserve(invoice: str = Query("invoice")) -> JSONResponse:
|
async def api_payments_fee_reserve(invoice: str = Query("invoice")) -> JSONResponse:
|
||||||
invoice_obj = bolt11.decode(invoice)
|
invoice_obj = bolt11.decode(invoice)
|
||||||
@@ -275,9 +311,95 @@ async def api_payments_fee_reserve(invoice: str = Query("invoice")) -> JSONRespo
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_lnurl_response(
|
||||||
|
params: dict, domain: str, amount_msat: int
|
||||||
|
) -> bolt11.Invoice:
|
||||||
|
if params.get("status") == "ERROR":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
detail=f"{domain} said: '{params.get('reason', '')}'",
|
||||||
|
)
|
||||||
|
if not params.get("pr"):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
detail=f"{domain} did not return a payment request.",
|
||||||
|
)
|
||||||
|
invoice = bolt11.decode(params["pr"])
|
||||||
|
if invoice.amount_msat != amount_msat:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
detail=(
|
||||||
|
f"{domain} returned an invalid invoice. Expected"
|
||||||
|
f" {amount_msat} msat, got {invoice.amount_msat}."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return invoice
|
||||||
|
|
||||||
|
|
||||||
|
async def _fetch_lnurl_params(data: CreateLnurl, amount_msat: int, domain: str) -> dict:
|
||||||
|
headers = {"User-Agent": settings.user_agent}
|
||||||
|
async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client:
|
||||||
|
try:
|
||||||
|
r: httpx.Response = await client.get(
|
||||||
|
data.callback,
|
||||||
|
params={"amount": amount_msat, "comment": data.comment},
|
||||||
|
timeout=40,
|
||||||
|
)
|
||||||
|
if r.is_error:
|
||||||
|
raise httpx.ConnectError("LNURL callback connection error")
|
||||||
|
r.raise_for_status()
|
||||||
|
except (httpx.HTTPError, ssl.SSLError) as exc:
|
||||||
|
logger.warning(exc)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
detail=f"Failed to connect to {domain}.",
|
||||||
|
) from exc
|
||||||
|
return json.loads(r.text)
|
||||||
|
|
||||||
|
|
||||||
|
@payment_router.post("/lnurl")
|
||||||
|
async def api_payments_pay_lnurl(
|
||||||
|
data: CreateLnurl, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||||
|
) -> Payment:
|
||||||
|
domain = urlparse(data.callback).netloc
|
||||||
|
check_callback_url(data.callback)
|
||||||
|
|
||||||
|
if data.unit and data.unit != "sat":
|
||||||
|
amount_msat = await fiat_amount_as_satoshis(data.amount, data.unit)
|
||||||
|
amount_msat = ceil(amount_msat // 1000) * 1000
|
||||||
|
else:
|
||||||
|
amount_msat = data.amount
|
||||||
|
|
||||||
|
params = await _fetch_lnurl_params(data, amount_msat, domain)
|
||||||
|
_validate_lnurl_response(params, domain, amount_msat)
|
||||||
|
|
||||||
|
extra = {}
|
||||||
|
if params.get("successAction"):
|
||||||
|
extra["success_action"] = params["successAction"]
|
||||||
|
if data.comment:
|
||||||
|
extra["comment"] = data.comment
|
||||||
|
if data.unit and data.unit != "sat":
|
||||||
|
extra["fiat_currency"] = data.unit
|
||||||
|
extra["fiat_amount"] = data.amount / 1000
|
||||||
|
if data.internal_memo is not None:
|
||||||
|
if len(data.internal_memo) > 512:
|
||||||
|
raise ValueError("Internal memo must be 512 characters or less.")
|
||||||
|
extra["internal_memo"] = data.internal_memo
|
||||||
|
if data.description is None:
|
||||||
|
raise ValueError("Description is required")
|
||||||
|
|
||||||
|
payment = await pay_invoice(
|
||||||
|
wallet_id=wallet.wallet.id,
|
||||||
|
payment_request=params["pr"],
|
||||||
|
description=data.description,
|
||||||
|
extra=extra,
|
||||||
|
)
|
||||||
|
return payment
|
||||||
|
|
||||||
|
|
||||||
# TODO: refactor this route into a public and admin one
|
# TODO: refactor this route into a public and admin one
|
||||||
@payment_router.get("/{payment_hash}")
|
@payment_router.get("/{payment_hash}")
|
||||||
async def api_payment(payment_hash, x_api_key: str | None = Header(None)):
|
async def api_payment(payment_hash, x_api_key: Optional[str] = Header(None)):
|
||||||
# We use X_Api_Key here because we want this call to work with and without keys
|
# We use X_Api_Key here because we want this call to work with and without keys
|
||||||
# If a valid key is given, we also return the field "details", otherwise not
|
# If a valid key is given, we also return the field "details", otherwise not
|
||||||
wallet = await get_wallet_for_key(x_api_key) if isinstance(x_api_key, str) else None
|
wallet = await get_wallet_for_key(x_api_key) if isinstance(x_api_key, str) else None
|
||||||
@@ -320,7 +442,7 @@ async def api_payments_decode(data: DecodePayment) -> JSONResponse:
|
|||||||
payment_str = data.data
|
payment_str = data.data
|
||||||
try:
|
try:
|
||||||
if payment_str[:5] == "LNURL":
|
if payment_str[:5] == "LNURL":
|
||||||
url = str(url_decode(payment_str))
|
url = str(lnurl_decode(payment_str))
|
||||||
return JSONResponse({"domain": url})
|
return JSONResponse({"domain": url})
|
||||||
else:
|
else:
|
||||||
invoice = bolt11.decode(payment_str)
|
invoice = bolt11.decode(payment_str)
|
||||||
@@ -333,52 +455,57 @@ async def api_payments_decode(data: DecodePayment) -> JSONResponse:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@payment_router.post("/settle")
|
@payment_router.post("/{payment_request}/pay-with-nfc", status_code=HTTPStatus.OK)
|
||||||
async def api_payments_settle(
|
|
||||||
data: SettleInvoice, key_type: WalletTypeInfo = Depends(require_admin_key)
|
|
||||||
) -> InvoiceResponse:
|
|
||||||
payment_hash = sha256(bytes.fromhex(data.preimage)).hexdigest()
|
|
||||||
payment = await get_standalone_payment(
|
|
||||||
payment_hash, incoming=True, wallet_id=key_type.wallet.id
|
|
||||||
)
|
|
||||||
if not payment:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND,
|
|
||||||
detail="Payment does not exist or does not belong to this wallet.",
|
|
||||||
)
|
|
||||||
return await settle_hold_invoice(payment, data.preimage)
|
|
||||||
|
|
||||||
|
|
||||||
@payment_router.post("/cancel")
|
|
||||||
async def api_payments_cancel(
|
|
||||||
data: CancelInvoice, key_type: WalletTypeInfo = Depends(require_admin_key)
|
|
||||||
) -> InvoiceResponse:
|
|
||||||
payment = await get_standalone_payment(
|
|
||||||
data.payment_hash, incoming=True, wallet_id=key_type.wallet.id
|
|
||||||
)
|
|
||||||
if not payment:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND,
|
|
||||||
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(
|
async def api_payment_pay_with_nfc(
|
||||||
payment_request: str,
|
payment_request: str,
|
||||||
lnurl_data: CreateLnurlWithdraw,
|
lnurl_data: PayLnurlWData,
|
||||||
) -> SimpleStatus:
|
) -> JSONResponse:
|
||||||
if not lnurl_data.lnurl_w.lud17:
|
lnurl = lnurl_data.lnurl_w.lower()
|
||||||
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.")
|
# Follow LUD-17 -> https://github.com/lnurl/luds/blob/luds/17.md
|
||||||
|
url = lnurl.replace("lnurlw://", "https://")
|
||||||
|
|
||||||
|
headers = {"User-Agent": settings.user_agent}
|
||||||
|
async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client:
|
||||||
|
try:
|
||||||
|
check_callback_url(url)
|
||||||
|
lnurl_req = await client.get(url, timeout=10)
|
||||||
|
if lnurl_req.is_error:
|
||||||
|
return JSONResponse(
|
||||||
|
{"success": False, "detail": "Error loading LNURL request"}
|
||||||
|
)
|
||||||
|
|
||||||
|
lnurl_res = lnurl_req.json()
|
||||||
|
|
||||||
|
if lnurl_res.get("status") == "ERROR":
|
||||||
|
return JSONResponse({"success": False, "detail": lnurl_res["reason"]})
|
||||||
|
|
||||||
|
if lnurl_res.get("tag") != "withdrawRequest":
|
||||||
|
return JSONResponse(
|
||||||
|
{"success": False, "detail": "Invalid LNURL-withdraw"}
|
||||||
|
)
|
||||||
|
|
||||||
|
callback_url = lnurl_res["callback"]
|
||||||
|
k1 = lnurl_res["k1"]
|
||||||
|
|
||||||
|
callback_req = await client.get(
|
||||||
|
callback_url,
|
||||||
|
params={"k1": k1, "pr": payment_request},
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
if callback_req.is_error:
|
||||||
|
return JSONResponse(
|
||||||
|
{"success": False, "detail": "Error loading callback request"}
|
||||||
|
)
|
||||||
|
|
||||||
|
callback_res = callback_req.json()
|
||||||
|
|
||||||
|
if callback_res.get("status") == "ERROR":
|
||||||
|
return JSONResponse(
|
||||||
|
{"success": False, "detail": callback_res["reason"]}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return JSONResponse({"success": True, "detail": callback_res})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return JSONResponse({"success": False, "detail": f"Unexpected error: {e}"})
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import base64
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
from typing import Optional
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import shortuuid
|
import shortuuid
|
||||||
@@ -34,7 +35,7 @@ from lnbits.core.models.notifications import NotificationType
|
|||||||
from lnbits.core.models.users import Account
|
from lnbits.core.models.users import Account
|
||||||
from lnbits.core.services import (
|
from lnbits.core.services import (
|
||||||
create_user_account_no_ckeck,
|
create_user_account_no_ckeck,
|
||||||
enqueue_admin_notification,
|
enqueue_notification,
|
||||||
update_user_account,
|
update_user_account,
|
||||||
update_user_extensions,
|
update_user_extensions,
|
||||||
update_wallet_balance,
|
update_wallet_balance,
|
||||||
@@ -47,7 +48,6 @@ from lnbits.helpers import (
|
|||||||
)
|
)
|
||||||
from lnbits.settings import EditableSettings, settings
|
from lnbits.settings import EditableSettings, settings
|
||||||
from lnbits.utils.exchange_rates import allowed_currencies
|
from lnbits.utils.exchange_rates import allowed_currencies
|
||||||
from lnbits.utils.nostr import normalize_public_key
|
|
||||||
|
|
||||||
users_router = APIRouter(
|
users_router = APIRouter(
|
||||||
prefix="/users/api/v1", dependencies=[Depends(check_admin)], tags=["Users"]
|
prefix="/users/api/v1", dependencies=[Depends(check_admin)], tags=["Users"]
|
||||||
@@ -95,9 +95,6 @@ async def api_create_user(data: CreateUser) -> CreateUser:
|
|||||||
data.extra = data.extra or UserExtra()
|
data.extra = data.extra or UserExtra()
|
||||||
data.extra.provider = data.extra.provider or "lnbits"
|
data.extra.provider = data.extra.provider or "lnbits"
|
||||||
|
|
||||||
if data.pubkey:
|
|
||||||
data.pubkey = normalize_public_key(data.pubkey)
|
|
||||||
|
|
||||||
account = Account(
|
account = Account(
|
||||||
id=uuid4().hex,
|
id=uuid4().hex,
|
||||||
username=data.username,
|
username=data.username,
|
||||||
@@ -131,9 +128,6 @@ async def api_update_user(
|
|||||||
HTTPStatus.BAD_REQUEST, "Use 'reset password' functionality."
|
HTTPStatus.BAD_REQUEST, "Use 'reset password' functionality."
|
||||||
)
|
)
|
||||||
|
|
||||||
if data.pubkey:
|
|
||||||
data.pubkey = normalize_public_key(data.pubkey)
|
|
||||||
|
|
||||||
account = Account(
|
account = Account(
|
||||||
id=user_id,
|
id=user_id,
|
||||||
username=data.username,
|
username=data.username,
|
||||||
@@ -229,7 +223,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")
|
@users_router.post("/user/{user_id}/wallet", name="Create a new wallet for user")
|
||||||
async def api_users_create_user_wallet(
|
async def api_users_create_user_wallet(
|
||||||
user_id: str, name: str | None = Body(None), currency: str | None = Body(None)
|
user_id: str, name: Optional[str] = Body(None), currency: Optional[str] = Body(None)
|
||||||
):
|
):
|
||||||
if currency and currency not in allowed_currencies():
|
if currency and currency not in allowed_currencies():
|
||||||
raise ValueError(f"Currency '{currency}' not allowed.")
|
raise ValueError(f"Currency '{currency}' not allowed.")
|
||||||
@@ -327,7 +321,7 @@ async def api_update_balance(data: UpdateBalance) -> SimpleStatus:
|
|||||||
if not wallet:
|
if not wallet:
|
||||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Wallet not found.")
|
raise HTTPException(HTTPStatus.NOT_FOUND, "Wallet not found.")
|
||||||
await update_wallet_balance(wallet=wallet, amount=int(data.amount))
|
await update_wallet_balance(wallet=wallet, amount=int(data.amount))
|
||||||
enqueue_admin_notification(
|
enqueue_notification(
|
||||||
NotificationType.balance_update,
|
NotificationType.balance_update,
|
||||||
{
|
{
|
||||||
"amount": int(data.amount),
|
"amount": int(data.amount),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
from typing import Optional
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from fastapi import (
|
from fastapi import (
|
||||||
@@ -9,11 +10,11 @@ from fastapi import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from lnbits.core.crud.wallets import get_wallets_paginated
|
from lnbits.core.crud.wallets import get_wallets_paginated
|
||||||
from lnbits.core.models import CreateWallet, KeyType, User, Wallet, WalletTypeInfo
|
from lnbits.core.models import CreateWallet, KeyType, User, Wallet
|
||||||
from lnbits.core.models.lnurl import StoredPayLink, StoredPayLinks
|
|
||||||
from lnbits.core.models.wallets import WalletsFilters
|
from lnbits.core.models.wallets import WalletsFilters
|
||||||
from lnbits.db import Filters, Page
|
from lnbits.db import Filters, Page
|
||||||
from lnbits.decorators import (
|
from lnbits.decorators import (
|
||||||
|
WalletTypeInfo,
|
||||||
check_user_exists,
|
check_user_exists,
|
||||||
parse_filters,
|
parse_filters,
|
||||||
require_admin_key,
|
require_admin_key,
|
||||||
@@ -92,28 +93,13 @@ async def api_reset_wallet_keys(
|
|||||||
return wallet
|
return wallet
|
||||||
|
|
||||||
|
|
||||||
@wallet_router.put("/stored_paylinks/{wallet_id}")
|
|
||||||
async def api_put_stored_paylinks(
|
|
||||||
wallet_id: str,
|
|
||||||
data: StoredPayLinks,
|
|
||||||
key_info: WalletTypeInfo = Depends(require_admin_key),
|
|
||||||
) -> list[StoredPayLink]:
|
|
||||||
if key_info.wallet.id != wallet_id:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN, detail="You cannot modify this wallet"
|
|
||||||
)
|
|
||||||
key_info.wallet.stored_paylinks.links = data.links
|
|
||||||
wallet = await update_wallet(key_info.wallet)
|
|
||||||
return wallet.stored_paylinks.links
|
|
||||||
|
|
||||||
|
|
||||||
@wallet_router.patch("")
|
@wallet_router.patch("")
|
||||||
async def api_update_wallet(
|
async def api_update_wallet(
|
||||||
name: str | None = Body(None),
|
name: Optional[str] = Body(None),
|
||||||
icon: str | None = Body(None),
|
icon: Optional[str] = Body(None),
|
||||||
color: str | None = Body(None),
|
color: Optional[str] = Body(None),
|
||||||
currency: str | None = Body(None),
|
currency: Optional[str] = Body(None),
|
||||||
pinned: bool | None = Body(None),
|
pinned: Optional[bool] = Body(None),
|
||||||
key_info: WalletTypeInfo = Depends(require_admin_key),
|
key_info: WalletTypeInfo = Depends(require_admin_key),
|
||||||
) -> Wallet:
|
) -> Wallet:
|
||||||
wallet = await get_wallet(key_info.wallet.id)
|
wallet = await get_wallet(key_info.wallet.id)
|
||||||
|
|||||||
@@ -1,20 +1,33 @@
|
|||||||
from fastapi import APIRouter, WebSocket
|
from fastapi import (
|
||||||
|
APIRouter,
|
||||||
|
WebSocket,
|
||||||
|
WebSocketDisconnect,
|
||||||
|
)
|
||||||
|
|
||||||
from ..services import websocket_manager
|
from lnbits.settings import settings
|
||||||
|
|
||||||
|
from ..services import (
|
||||||
|
websocket_manager,
|
||||||
|
websocket_updater,
|
||||||
|
)
|
||||||
|
|
||||||
websocket_router = APIRouter(prefix="/api/v1/ws", tags=["Websocket"])
|
websocket_router = APIRouter(prefix="/api/v1/ws", tags=["Websocket"])
|
||||||
|
|
||||||
|
|
||||||
@websocket_router.websocket("/{item_id}")
|
@websocket_router.websocket("/{item_id}")
|
||||||
async def websocket_connect(websocket: WebSocket, item_id: str) -> None:
|
async def websocket_connect(websocket: WebSocket, item_id: str):
|
||||||
conn = await websocket_manager.connect(item_id, websocket)
|
await websocket_manager.connect(websocket, item_id)
|
||||||
await websocket_manager.listen(conn)
|
try:
|
||||||
|
while settings.lnbits_running:
|
||||||
|
await websocket.receive_text()
|
||||||
|
except WebSocketDisconnect:
|
||||||
|
websocket_manager.disconnect(websocket)
|
||||||
|
|
||||||
|
|
||||||
@websocket_router.post("/{item_id}")
|
@websocket_router.post("/{item_id}")
|
||||||
async def websocket_update_post(item_id: str, data: str):
|
async def websocket_update_post(item_id: str, data: str):
|
||||||
try:
|
try:
|
||||||
await websocket_manager.send(item_id, data)
|
await websocket_updater(item_id, data)
|
||||||
return {"sent": True, "data": data}
|
return {"sent": True, "data": data}
|
||||||
except Exception:
|
except Exception:
|
||||||
return {"sent": False, "data": data}
|
return {"sent": False, "data": data}
|
||||||
@@ -23,7 +36,7 @@ async def websocket_update_post(item_id: str, data: str):
|
|||||||
@websocket_router.get("/{item_id}/{data}")
|
@websocket_router.get("/{item_id}/{data}")
|
||||||
async def websocket_update_get(item_id: str, data: str):
|
async def websocket_update_get(item_id: str, data: str):
|
||||||
try:
|
try:
|
||||||
await websocket_manager.send(item_id, data)
|
await websocket_updater(item_id, data)
|
||||||
return {"sent": True, "data": data}
|
return {"sent": True, "data": data}
|
||||||
except Exception:
|
except Exception:
|
||||||
return {"sent": False, "data": data}
|
return {"sent": False, "data": data}
|
||||||
|
|||||||
+5
-9
@@ -281,19 +281,15 @@ class Database(Compat):
|
|||||||
self.schema = self.name
|
self.schema = self.name
|
||||||
self.type = DB_TYPE
|
self.type = DB_TYPE
|
||||||
|
|
||||||
if self.type == POSTGRES and settings.lnbits_database_url:
|
if DB_TYPE == SQLITE:
|
||||||
database_uri = settings.lnbits_database_url.replace(
|
|
||||||
"postgres://", "postgresql+asyncpg://"
|
|
||||||
)
|
|
||||||
elif self.type == COCKROACH and settings.lnbits_database_url:
|
|
||||||
database_uri = settings.lnbits_database_url.replace(
|
|
||||||
"cockroachdb://", "cockroachdb+asyncpg://"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.path = os.path.join(
|
self.path = os.path.join(
|
||||||
settings.lnbits_data_folder, f"{self.name}.sqlite3"
|
settings.lnbits_data_folder, f"{self.name}.sqlite3"
|
||||||
)
|
)
|
||||||
database_uri = f"sqlite+aiosqlite:///{self.path}"
|
database_uri = f"sqlite+aiosqlite:///{self.path}"
|
||||||
|
else:
|
||||||
|
database_uri = settings.lnbits_database_url.replace(
|
||||||
|
"postgres://", "postgresql+asyncpg://"
|
||||||
|
)
|
||||||
|
|
||||||
if self.name.startswith("ext_"):
|
if self.name.startswith("ext_"):
|
||||||
self.schema = self.name[4:]
|
self.schema = self.name[4:]
|
||||||
|
|||||||
+21
-21
@@ -1,5 +1,5 @@
|
|||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import Annotated, Literal
|
from typing import Annotated, Literal, Optional, Union
|
||||||
|
|
||||||
import jwt
|
import jwt
|
||||||
from fastapi import Cookie, Depends, Query, Request, Security
|
from fastapi import Cookie, Depends, Query, Request, Security
|
||||||
@@ -55,8 +55,8 @@ api_key_query = APIKeyQuery(
|
|||||||
class KeyChecker(SecurityBase):
|
class KeyChecker(SecurityBase):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
api_key: str | None = None,
|
api_key: Optional[str] = None,
|
||||||
expected_key_type: KeyType | None = None,
|
expected_key_type: Optional[KeyType] = None,
|
||||||
):
|
):
|
||||||
self.auto_error: bool = True
|
self.auto_error: bool = True
|
||||||
self.expected_key_type = expected_key_type
|
self.expected_key_type = expected_key_type
|
||||||
@@ -137,17 +137,17 @@ async def require_invoice_key(
|
|||||||
|
|
||||||
|
|
||||||
async def check_access_token(
|
async def check_access_token(
|
||||||
header_access_token: Annotated[str | None, Depends(oauth2_scheme)],
|
header_access_token: Annotated[Union[str, None], Depends(oauth2_scheme)],
|
||||||
cookie_access_token: Annotated[str | None, Cookie()] = None,
|
cookie_access_token: Annotated[Union[str, None], Cookie()] = None,
|
||||||
bearer_access_token: Annotated[str | None, Depends(http_bearer)] = None,
|
bearer_access_token: Annotated[Union[str, None], Depends(http_bearer)] = None,
|
||||||
) -> str | None:
|
) -> Optional[str]:
|
||||||
return header_access_token or cookie_access_token or bearer_access_token
|
return header_access_token or cookie_access_token or bearer_access_token
|
||||||
|
|
||||||
|
|
||||||
async def check_user_exists(
|
async def check_user_exists(
|
||||||
r: Request,
|
r: Request,
|
||||||
access_token: Annotated[str | None, Depends(check_access_token)],
|
access_token: Annotated[Optional[str], Depends(check_access_token)],
|
||||||
usr: UUID4 | None = None,
|
usr: Optional[UUID4] = None,
|
||||||
) -> User:
|
) -> User:
|
||||||
if access_token:
|
if access_token:
|
||||||
account = await _get_account_from_token(access_token, r["path"], r["method"])
|
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(
|
async def optional_user_id(
|
||||||
r: Request,
|
r: Request,
|
||||||
access_token: Annotated[str | None, Depends(check_access_token)],
|
access_token: Annotated[Optional[str], Depends(check_access_token)],
|
||||||
usr: UUID4 | None = None,
|
usr: Optional[UUID4] = None,
|
||||||
) -> str | None:
|
) -> Optional[str]:
|
||||||
if usr and settings.is_auth_method_allowed(AuthMethods.user_id_only):
|
if usr and settings.is_auth_method_allowed(AuthMethods.user_id_only):
|
||||||
return usr.hex
|
return usr.hex
|
||||||
if access_token:
|
if access_token:
|
||||||
@@ -189,7 +189,7 @@ async def optional_user_id(
|
|||||||
|
|
||||||
|
|
||||||
async def access_token_payload(
|
async def access_token_payload(
|
||||||
access_token: Annotated[str | None, Depends(check_access_token)],
|
access_token: Annotated[Optional[str], Depends(check_access_token)],
|
||||||
) -> AccessTokenPayload:
|
) -> AccessTokenPayload:
|
||||||
if not access_token:
|
if not access_token:
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Missing access token.")
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Missing access token.")
|
||||||
@@ -231,11 +231,11 @@ def parse_filters(model: type[TFilterModel]):
|
|||||||
|
|
||||||
def dependency(
|
def dependency(
|
||||||
request: Request,
|
request: Request,
|
||||||
limit: int | None = None,
|
limit: Optional[int] = None,
|
||||||
offset: int | None = None,
|
offset: Optional[int] = None,
|
||||||
sortby: str | None = None,
|
sortby: Optional[str] = None,
|
||||||
direction: Literal["asc", "desc"] | None = None,
|
direction: Optional[Literal["asc", "desc"]] = None,
|
||||||
search: str | None = Query(None, description="Text based search"),
|
search: Optional[str] = Query(None, description="Text based search"),
|
||||||
):
|
):
|
||||||
params = request.query_params
|
params = request.query_params
|
||||||
filters = []
|
filters = []
|
||||||
@@ -259,7 +259,7 @@ def parse_filters(model: type[TFilterModel]):
|
|||||||
|
|
||||||
|
|
||||||
async def check_user_extension_access(
|
async def check_user_extension_access(
|
||||||
user_id: str, ext_id: str, conn: Connection | None = None
|
user_id: str, ext_id: str, conn: Optional[Connection] = None
|
||||||
) -> SimpleStatus:
|
) -> SimpleStatus:
|
||||||
"""
|
"""
|
||||||
Check if the user has access to a particular extension.
|
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(
|
async def _get_account_from_token(
|
||||||
access_token: str, path: str, method: str
|
access_token: str, path: str, method: str
|
||||||
) -> Account | None:
|
) -> Optional[Account]:
|
||||||
try:
|
try:
|
||||||
payload: dict = jwt.decode(access_token, settings.auth_secret_key, ["HS256"])
|
payload: dict = jwt.decode(access_token, settings.auth_secret_key, ["HS256"])
|
||||||
return await _get_account_from_jwt_payload(
|
return await _get_account_from_jwt_payload(
|
||||||
@@ -310,7 +310,7 @@ async def _get_account_from_token(
|
|||||||
|
|
||||||
async def _get_account_from_jwt_payload(
|
async def _get_account_from_jwt_payload(
|
||||||
payload: AccessTokenPayload, path: str, method: str
|
payload: AccessTokenPayload, path: str, method: str
|
||||||
) -> Account | None:
|
) -> Optional[Account]:
|
||||||
account = None
|
account = None
|
||||||
if payload.sub:
|
if payload.sub:
|
||||||
account = await get_account_by_username(payload.sub)
|
account = await get_account_by_username(payload.sub)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import FastAPI, HTTPException, Request
|
from fastapi import FastAPI, HTTPException, Request
|
||||||
from fastapi.exceptions import RequestValidationError
|
from fastapi.exceptions import RequestValidationError
|
||||||
@@ -29,7 +30,7 @@ class UnsupportedError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def render_html_error(request: Request, exc: Exception) -> Response | None:
|
def render_html_error(request: Request, exc: Exception) -> Optional[Response]:
|
||||||
# Only the browser sends "text/html" request
|
# Only the browser sends "text/html" request
|
||||||
# not fail proof, but everything else get's a JSON response
|
# not fail proof, but everything else get's a JSON response
|
||||||
|
|
||||||
|
|||||||
+4
-11
@@ -3,8 +3,6 @@ from __future__ import annotations
|
|||||||
import importlib
|
import importlib
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from lnbits.fiat.base import FiatProvider
|
from lnbits.fiat.base import FiatProvider
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
@@ -17,7 +15,7 @@ class FiatProviderType(Enum):
|
|||||||
stripe = "StripeWallet"
|
stripe = "StripeWallet"
|
||||||
|
|
||||||
|
|
||||||
async def get_fiat_provider(name: str) -> FiatProvider | None:
|
async def get_fiat_provider(name: str) -> FiatProvider:
|
||||||
if name not in FiatProviderType.__members__:
|
if name not in FiatProviderType.__members__:
|
||||||
raise ValueError(f"Fiat provider '{name}' is not supported.")
|
raise ValueError(f"Fiat provider '{name}' is not supported.")
|
||||||
|
|
||||||
@@ -29,18 +27,13 @@ async def get_fiat_provider(name: str) -> FiatProvider | None:
|
|||||||
del fiat_providers[name]
|
del fiat_providers[name]
|
||||||
else:
|
else:
|
||||||
return fiat_provider
|
return fiat_provider
|
||||||
_provider = _init_fiat_provider(FiatProviderType[name])
|
fiat_providers[name] = _init_fiat_provider(FiatProviderType[name])
|
||||||
if not _provider:
|
|
||||||
return None
|
|
||||||
|
|
||||||
fiat_providers[name] = _provider
|
|
||||||
return fiat_providers[name]
|
return fiat_providers[name]
|
||||||
|
|
||||||
|
|
||||||
def _init_fiat_provider(fiat_provider: FiatProviderType) -> FiatProvider | None:
|
def _init_fiat_provider(fiat_provider: FiatProviderType) -> FiatProvider:
|
||||||
if not settings.is_fiat_provider_enabled(fiat_provider.name):
|
if not settings.is_fiat_provider_enabled(fiat_provider.name):
|
||||||
logger.warning(f"Fiat provider '{fiat_provider.name}' not enabled.")
|
raise ValueError(f"Fiat provider '{fiat_provider.name}' not enabled.")
|
||||||
return None
|
|
||||||
provider_constructor = getattr(fiat_module, fiat_provider.value)
|
provider_constructor = getattr(fiat_module, fiat_provider.value)
|
||||||
return provider_constructor()
|
return provider_constructor()
|
||||||
|
|
||||||
|
|||||||
+1
-2
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from collections.abc import AsyncGenerator, Coroutine
|
from collections.abc import AsyncGenerator, Coroutine
|
||||||
from typing import TYPE_CHECKING, Any, NamedTuple
|
from typing import TYPE_CHECKING, NamedTuple
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
pass
|
pass
|
||||||
@@ -106,7 +106,6 @@ class FiatProvider(ABC):
|
|||||||
payment_hash: str,
|
payment_hash: str,
|
||||||
currency: str,
|
currency: str,
|
||||||
memo: str | None = None,
|
memo: str | None = None,
|
||||||
extra: dict[str, Any] | None = None,
|
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> Coroutine[None, None, FiatInvoiceResponse]:
|
) -> Coroutine[None, None, FiatInvoiceResponse]:
|
||||||
pass
|
pass
|
||||||
|
|||||||
+65
-225
@@ -2,12 +2,11 @@ import asyncio
|
|||||||
import json
|
import json
|
||||||
from collections.abc import AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Any, Literal
|
from typing import Optional
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pydantic import BaseModel, Field, ValidationError
|
|
||||||
|
|
||||||
from lnbits.helpers import normalize_endpoint
|
from lnbits.helpers import normalize_endpoint
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
@@ -23,34 +22,6 @@ from .base import (
|
|||||||
FiatStatusResponse,
|
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):
|
class StripeWallet(FiatProvider):
|
||||||
"""https://docs.stripe.com/api"""
|
"""https://docs.stripe.com/api"""
|
||||||
@@ -60,9 +31,9 @@ class StripeWallet(FiatProvider):
|
|||||||
self._settings_fields = self._settings_connection_fields()
|
self._settings_fields = self._settings_connection_fields()
|
||||||
if not settings.stripe_api_endpoint:
|
if not settings.stripe_api_endpoint:
|
||||||
raise ValueError("Cannot initialize StripeWallet: missing endpoint.")
|
raise ValueError("Cannot initialize StripeWallet: missing endpoint.")
|
||||||
|
|
||||||
if not settings.stripe_api_secret_key:
|
if not settings.stripe_api_secret_key:
|
||||||
raise ValueError("Cannot initialize StripeWallet: missing API secret key.")
|
raise ValueError("Cannot initialize StripeWallet: missing API secret key.")
|
||||||
|
|
||||||
self.endpoint = normalize_endpoint(settings.stripe_api_endpoint)
|
self.endpoint = normalize_endpoint(settings.stripe_api_endpoint)
|
||||||
self.headers = {
|
self.headers = {
|
||||||
"Authorization": f"Bearer {settings.stripe_api_secret_key}",
|
"Authorization": f"Bearer {settings.stripe_api_secret_key}",
|
||||||
@@ -78,7 +49,7 @@ class StripeWallet(FiatProvider):
|
|||||||
logger.warning(f"Error closing stripe wallet connection: {e}")
|
logger.warning(f"Error closing stripe wallet connection: {e}")
|
||||||
|
|
||||||
async def status(
|
async def status(
|
||||||
self, only_check_settings: bool | None = False
|
self, only_check_settings: Optional[bool] = False
|
||||||
) -> FiatStatusResponse:
|
) -> FiatStatusResponse:
|
||||||
if only_check_settings:
|
if only_check_settings:
|
||||||
if self._settings_fields != self._settings_connection_fields():
|
if self._settings_fields != self._settings_connection_fields():
|
||||||
@@ -90,10 +61,8 @@ class StripeWallet(FiatProvider):
|
|||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
data = r.json()
|
data = r.json()
|
||||||
|
|
||||||
available = data.get("available") or []
|
available_balance = data.get("available", [{}])[0].get("amount", 0)
|
||||||
available_balance = 0
|
# pending_balance = data.get("pending", {}).get("amount", 0)
|
||||||
if available and isinstance(available, list):
|
|
||||||
available_balance = int(available[0].get("amount", 0))
|
|
||||||
|
|
||||||
return FiatStatusResponse(balance=available_balance)
|
return FiatStatusResponse(balance=available_balance)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
@@ -107,48 +76,82 @@ class StripeWallet(FiatProvider):
|
|||||||
amount: float,
|
amount: float,
|
||||||
payment_hash: str,
|
payment_hash: str,
|
||||||
currency: str,
|
currency: str,
|
||||||
memo: str | None = None,
|
memo: Optional[str] = None,
|
||||||
extra: dict[str, Any] | None = None,
|
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> FiatInvoiceResponse:
|
) -> FiatInvoiceResponse:
|
||||||
amount_cents = int(amount * 100)
|
amount_cents = int(amount * 100)
|
||||||
opts = self._parse_create_opts(extra or {})
|
form_data = [
|
||||||
if not opts:
|
("mode", "payment"),
|
||||||
return FiatInvoiceResponse(ok=False, error_message="Invalid Stripe options")
|
(
|
||||||
|
"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)
|
||||||
|
|
||||||
if opts.fiat_method == "checkout":
|
try:
|
||||||
return await self._create_checkout_invoice(
|
headers = self.headers.copy()
|
||||||
amount_cents, currency, payment_hash, memo, opts
|
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||||
)
|
r = await self.client.post(
|
||||||
if opts.fiat_method == "terminal":
|
url="/v1/checkout/sessions", headers=headers, content=encoded_data
|
||||||
return await self._create_terminal_invoice(
|
|
||||||
amount_cents, currency, payment_hash, opts
|
|
||||||
)
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
data = r.json()
|
||||||
|
|
||||||
return FiatInvoiceResponse(
|
session_id = data.get("id")
|
||||||
ok=False, error_message=f"Unsupported fiat_method: {opts.fiat_method}"
|
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}."
|
||||||
|
)
|
||||||
|
|
||||||
async def pay_invoice(self, payment_request: str) -> FiatPaymentResponse:
|
async def pay_invoice(self, payment_request: str) -> FiatPaymentResponse:
|
||||||
raise NotImplementedError("Stripe does not support paying invoices directly.")
|
raise NotImplementedError("Stripe does not support paying invoices directly.")
|
||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> FiatPaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> FiatPaymentStatus:
|
||||||
try:
|
try:
|
||||||
stripe_id = self._normalize_stripe_id(checking_id)
|
r = await self.client.get(
|
||||||
|
url=f"/v1/checkout/sessions/{checking_id}",
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
if stripe_id.startswith("cs_"):
|
data = r.json()
|
||||||
r = await self.client.get(f"/v1/checkout/sessions/{stripe_id}")
|
payment_status = data.get("payment_status")
|
||||||
r.raise_for_status()
|
if not payment_status:
|
||||||
return self._status_from_checkout_session(r.json())
|
return FiatPaymentPendingStatus()
|
||||||
|
if payment_status == "paid":
|
||||||
|
# todo: handle fee
|
||||||
|
return FiatPaymentSuccessStatus()
|
||||||
|
|
||||||
if stripe_id.startswith("pi_"):
|
expires_at = data.get("expires_at")
|
||||||
r = await self.client.get(f"/v1/payment_intents/{stripe_id}")
|
_24_hours_ago = datetime.now(timezone.utc) - timedelta(hours=24)
|
||||||
r.raise_for_status()
|
if expires_at and expires_at < _24_hours_ago.timestamp():
|
||||||
return self._status_from_payment_intent(r.json())
|
# be defensive: add a 24 hour buffer
|
||||||
|
return FiatPaymentFailedStatus()
|
||||||
|
|
||||||
logger.debug(f"Unknown Stripe id prefix: {checking_id}")
|
|
||||||
return FiatPaymentPendingStatus()
|
return FiatPaymentPendingStatus()
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.debug(f"Error getting invoice status: {exc}")
|
logger.debug(f"Error getting invoice status: {exc}")
|
||||||
return FiatPaymentPendingStatus()
|
return FiatPaymentPendingStatus()
|
||||||
@@ -165,169 +168,6 @@ class StripeWallet(FiatProvider):
|
|||||||
value = await mock_queue.get()
|
value = await mock_queue.get()
|
||||||
yield value
|
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:
|
def _settings_connection_fields(self) -> str:
|
||||||
return "-".join(
|
return "-".join(
|
||||||
[str(settings.stripe_api_endpoint), str(settings.stripe_api_secret_key)]
|
[str(settings.stripe_api_endpoint), str(settings.stripe_api_secret_key)]
|
||||||
|
|||||||
+21
-13
@@ -3,7 +3,7 @@ import json
|
|||||||
import re
|
import re
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any, Optional
|
||||||
from urllib import request
|
from urllib import request
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
@@ -16,6 +16,7 @@ from packaging import version
|
|||||||
from pydantic.schema import field_schema
|
from pydantic.schema import field_schema
|
||||||
|
|
||||||
from lnbits.jinja2_templating import Jinja2Templates
|
from lnbits.jinja2_templating import Jinja2Templates
|
||||||
|
from lnbits.nodes import get_node_class
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.utils.crypto import AESCipher
|
from lnbits.utils.crypto import AESCipher
|
||||||
|
|
||||||
@@ -39,7 +40,7 @@ def urlsafe_short_hash() -> str:
|
|||||||
return shortuuid.uuid()
|
return shortuuid.uuid()
|
||||||
|
|
||||||
|
|
||||||
def url_for(endpoint: str, external: bool | None = False, **params: Any) -> str:
|
def url_for(endpoint: str, external: Optional[bool] = False, **params: Any) -> str:
|
||||||
base = f"http://{settings.host}:{settings.port}" if external else ""
|
base = f"http://{settings.host}:{settings.port}" if external else ""
|
||||||
url_params = "?"
|
url_params = "?"
|
||||||
for key, value in params.items():
|
for key, value in params.items():
|
||||||
@@ -52,7 +53,7 @@ def static_url_for(static: str, path: str) -> str:
|
|||||||
return f"/{static}/{path}?v={settings.server_startup_time}"
|
return f"/{static}/{path}?v={settings.server_startup_time}"
|
||||||
|
|
||||||
|
|
||||||
def template_renderer(additional_folders: list | None = None) -> Jinja2Templates:
|
def template_renderer(additional_folders: Optional[list] = None) -> Jinja2Templates:
|
||||||
folders = ["lnbits/templates", "lnbits/core/templates"]
|
folders = ["lnbits/templates", "lnbits/core/templates"]
|
||||||
if additional_folders:
|
if additional_folders:
|
||||||
additional_folders += [
|
additional_folders += [
|
||||||
@@ -82,8 +83,8 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
|
|||||||
"LNBITS_CUSTOM_BADGE_COLOR": settings.lnbits_custom_badge_color,
|
"LNBITS_CUSTOM_BADGE_COLOR": settings.lnbits_custom_badge_color,
|
||||||
"LNBITS_EXTENSIONS_DEACTIVATE_ALL": settings.lnbits_extensions_deactivate_all,
|
"LNBITS_EXTENSIONS_DEACTIVATE_ALL": settings.lnbits_extensions_deactivate_all,
|
||||||
"LNBITS_NEW_ACCOUNTS_ALLOWED": settings.new_accounts_allowed,
|
"LNBITS_NEW_ACCOUNTS_ALLOWED": settings.new_accounts_allowed,
|
||||||
"LNBITS_NODE_UI": settings.lnbits_node_ui and settings.has_nodemanager,
|
"LNBITS_NODE_UI": settings.lnbits_node_ui and get_node_class() is not None,
|
||||||
"LNBITS_NODE_UI_AVAILABLE": settings.has_nodemanager,
|
"LNBITS_NODE_UI_AVAILABLE": get_node_class() is not None,
|
||||||
"LNBITS_QR_LOGO": settings.lnbits_qr_logo,
|
"LNBITS_QR_LOGO": settings.lnbits_qr_logo,
|
||||||
"LNBITS_SERVICE_FEE": settings.lnbits_service_fee,
|
"LNBITS_SERVICE_FEE": settings.lnbits_service_fee,
|
||||||
"LNBITS_SERVICE_FEE_MAX": settings.lnbits_service_fee_max,
|
"LNBITS_SERVICE_FEE_MAX": settings.lnbits_service_fee_max,
|
||||||
@@ -99,8 +100,11 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
|
|||||||
"USE_DEFAULT_BGIMAGE": settings.lnbits_default_bgimage,
|
"USE_DEFAULT_BGIMAGE": settings.lnbits_default_bgimage,
|
||||||
"VOIDWALLET": settings.lnbits_backend_wallet_class == "VoidWallet",
|
"VOIDWALLET": settings.lnbits_backend_wallet_class == "VoidWallet",
|
||||||
"WEBPUSH_PUBKEY": settings.lnbits_webpush_pubkey,
|
"WEBPUSH_PUBKEY": settings.lnbits_webpush_pubkey,
|
||||||
"LNBITS_DENOMINATION": settings.lnbits_denomination,
|
"LNBITS_DENOMINATION": (
|
||||||
"has_holdinvoice": settings.has_holdinvoice,
|
settings.lnbits_denomination
|
||||||
|
if settings.lnbits_denomination == "FakeWallet"
|
||||||
|
else "sats"
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
t.env.globals["WINDOW_SETTINGS"] = window_settings
|
t.env.globals["WINDOW_SETTINGS"] = window_settings
|
||||||
@@ -211,7 +215,7 @@ def is_valid_pubkey(pubkey: str) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def create_access_token(data: dict, token_expire_minutes: int | None = None) -> str:
|
def create_access_token(data: dict, token_expire_minutes: Optional[int] = None) -> str:
|
||||||
minutes = token_expire_minutes or settings.auth_token_expire_minutes
|
minutes = token_expire_minutes or settings.auth_token_expire_minutes
|
||||||
expire = datetime.now(timezone.utc) + timedelta(minutes=minutes)
|
expire = datetime.now(timezone.utc) + timedelta(minutes=minutes)
|
||||||
to_encode = {k: v for k, v in data.items() if v is not None}
|
to_encode = {k: v for k, v in data.items() if v is not None}
|
||||||
@@ -219,7 +223,9 @@ def create_access_token(data: dict, token_expire_minutes: int | None = None) ->
|
|||||||
return jwt.encode(to_encode, settings.auth_secret_key, "HS256")
|
return jwt.encode(to_encode, settings.auth_secret_key, "HS256")
|
||||||
|
|
||||||
|
|
||||||
def encrypt_internal_message(m: str | None = None, urlsafe: bool = False) -> str | None:
|
def encrypt_internal_message(
|
||||||
|
m: Optional[str] = None, urlsafe: bool = False
|
||||||
|
) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Encrypt message with the internal secret key
|
Encrypt message with the internal secret key
|
||||||
|
|
||||||
@@ -232,7 +238,9 @@ def encrypt_internal_message(m: str | None = None, urlsafe: bool = False) -> str
|
|||||||
return AESCipher(key=settings.auth_secret_key).encrypt(m.encode(), urlsafe=urlsafe)
|
return AESCipher(key=settings.auth_secret_key).encrypt(m.encode(), urlsafe=urlsafe)
|
||||||
|
|
||||||
|
|
||||||
def decrypt_internal_message(m: str | None = None, urlsafe: bool = False) -> str | None:
|
def decrypt_internal_message(
|
||||||
|
m: Optional[str] = None, urlsafe: bool = False
|
||||||
|
) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Decrypt message with the internal secret key
|
Decrypt message with the internal secret key
|
||||||
|
|
||||||
@@ -245,7 +253,7 @@ def decrypt_internal_message(m: str | None = None, urlsafe: bool = False) -> str
|
|||||||
return AESCipher(key=settings.auth_secret_key).decrypt(m, urlsafe=urlsafe)
|
return AESCipher(key=settings.auth_secret_key).decrypt(m, urlsafe=urlsafe)
|
||||||
|
|
||||||
|
|
||||||
def filter_dict_keys(data: dict, filter_keys: list[str] | None) -> dict:
|
def filter_dict_keys(data: dict, filter_keys: Optional[list[str]]) -> dict:
|
||||||
if not filter_keys:
|
if not filter_keys:
|
||||||
# return shallow clone of the dict even if there are no filters
|
# return shallow clone of the dict even if there are no filters
|
||||||
return {**data}
|
return {**data}
|
||||||
@@ -266,7 +274,7 @@ def version_parse(v: str):
|
|||||||
|
|
||||||
|
|
||||||
def is_lnbits_version_ok(
|
def is_lnbits_version_ok(
|
||||||
min_lnbits_version: str | None, max_lnbits_version: str | None
|
min_lnbits_version: Optional[str], max_lnbits_version: Optional[str]
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if min_lnbits_version and (
|
if min_lnbits_version and (
|
||||||
version_parse(min_lnbits_version) > version_parse(settings.version)
|
version_parse(min_lnbits_version) > version_parse(settings.version)
|
||||||
@@ -343,7 +351,7 @@ def path_segments(path: str) -> list[str]:
|
|||||||
return segments[0:]
|
return segments[0:]
|
||||||
|
|
||||||
|
|
||||||
def normalize_path(path: str | None) -> str:
|
def normalize_path(path: Optional[str]) -> str:
|
||||||
path = path or ""
|
path = path or ""
|
||||||
return "/" + "/".join(path_segments(path))
|
return "/" + "/".join(path_segments(path))
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
from http import HTTPStatus
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
from fastapi import HTTPException, Request, Response
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from fastapi.routing import APIRoute
|
||||||
|
from lnurl import LnurlErrorResponse, decode, encode, handle
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from lnbits.exceptions import InvoiceError, PaymentError
|
||||||
|
|
||||||
|
|
||||||
|
class LnurlErrorResponseHandler(APIRoute):
|
||||||
|
"""
|
||||||
|
Custom APIRoute class to handle LNURL errors.
|
||||||
|
LNURL errors always return with status 200 and
|
||||||
|
a JSON response with `status="ERROR"` and a `reason` key.
|
||||||
|
Helps to catch HTTPException and return a valid lnurl error response
|
||||||
|
|
||||||
|
Example:
|
||||||
|
withdraw_lnurl_router = APIRouter(prefix="/api/v1/lnurl")
|
||||||
|
withdraw_lnurl_router.route_class = LnurlErrorResponseHandler
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_route_handler(self) -> Callable:
|
||||||
|
original_route_handler = super().get_route_handler()
|
||||||
|
|
||||||
|
async def lnurl_route_handler(request: Request) -> Response:
|
||||||
|
try:
|
||||||
|
response = await original_route_handler(request)
|
||||||
|
return response
|
||||||
|
except (InvoiceError, PaymentError) as exc:
|
||||||
|
logger.debug(f"Wallet Error: {exc}")
|
||||||
|
response = JSONResponse(
|
||||||
|
status_code=HTTPStatus.OK,
|
||||||
|
content={"status": "ERROR", "reason": f"{exc.message}"},
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
except HTTPException as exc:
|
||||||
|
logger.debug(f"HTTPException: {exc}")
|
||||||
|
response = JSONResponse(
|
||||||
|
status_code=HTTPStatus.OK,
|
||||||
|
content={"status": "ERROR", "reason": f"{exc.detail}"},
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error("Unknown Error:", exc)
|
||||||
|
response = JSONResponse(
|
||||||
|
status_code=HTTPStatus.OK,
|
||||||
|
content={
|
||||||
|
"status": "ERROR",
|
||||||
|
"reason": f"UNKNOWN ERROR: {exc!s}",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
|
return lnurl_route_handler
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"LnurlErrorResponse",
|
||||||
|
"LnurlErrorResponseHandler",
|
||||||
|
"decode",
|
||||||
|
"encode",
|
||||||
|
"handle",
|
||||||
|
]
|
||||||
@@ -2,7 +2,7 @@ import asyncio
|
|||||||
import json
|
import json
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import Any
|
from typing import Any, Optional, Union
|
||||||
|
|
||||||
from fastapi import FastAPI, Request, Response
|
from fastapi import FastAPI, Request, Response
|
||||||
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
||||||
@@ -62,7 +62,7 @@ class InstalledExtensionMiddleware:
|
|||||||
|
|
||||||
def _response_by_accepted_type(
|
def _response_by_accepted_type(
|
||||||
self, scope: Scope, headers: list[Any], msg: str, status_code: HTTPStatus
|
self, scope: Scope, headers: list[Any], msg: str, status_code: HTTPStatus
|
||||||
) -> HTMLResponse | JSONResponse:
|
) -> Union[HTMLResponse, JSONResponse]:
|
||||||
"""
|
"""
|
||||||
Build an HTTP response containing the `msg` as HTTP body and the `status_code`
|
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
|
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:
|
async def dispatch(self, request: Request, call_next) -> Response:
|
||||||
start_time = datetime.now(timezone.utc)
|
start_time = datetime.now(timezone.utc)
|
||||||
request_details = await self._request_details(request)
|
request_details = await self._request_details(request)
|
||||||
response: Response | None = None
|
response: Optional[Response] = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = await call_next(request)
|
response = await call_next(request)
|
||||||
@@ -140,13 +140,13 @@ class AuditMiddleware(BaseHTTPMiddleware):
|
|||||||
async def _log_audit(
|
async def _log_audit(
|
||||||
self,
|
self,
|
||||||
request: Request,
|
request: Request,
|
||||||
response: Response | None,
|
response: Optional[Response],
|
||||||
duration: float,
|
duration: float,
|
||||||
request_details: str | None,
|
request_details: Optional[str],
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
http_method = request.scope.get("method", None)
|
http_method = request.scope.get("method", None)
|
||||||
path: str | None = getattr(request.scope.get("route", {}), "path", None)
|
path: Optional[str] = getattr(request.scope.get("route", {}), "path", None)
|
||||||
response_code = str(response.status_code) if response else None
|
response_code = str(response.status_code) if response else None
|
||||||
if not settings.audit_http_request(http_method, path, response_code):
|
if not settings.audit_http_request(http_method, path, response_code):
|
||||||
return None
|
return None
|
||||||
@@ -178,7 +178,7 @@ class AuditMiddleware(BaseHTTPMiddleware):
|
|||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning(ex)
|
logger.warning(ex)
|
||||||
|
|
||||||
async def _request_details(self, request: Request) -> str | None:
|
async def _request_details(self, request: Request) -> Optional[str]:
|
||||||
if not settings.audit_http_request_details():
|
if not settings.audit_http_request_details():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from .base import Node
|
||||||
|
|
||||||
|
|
||||||
|
def get_node_class() -> Optional[Node]:
|
||||||
|
return NODE
|
||||||
|
|
||||||
|
|
||||||
|
def set_node_class(node: Node):
|
||||||
|
global NODE
|
||||||
|
NODE = node
|
||||||
|
|
||||||
|
|
||||||
|
NODE: Optional[Node] = None
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ from httpx import HTTPStatusError
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.db import Filters, Page
|
from lnbits.db import Filters, Page
|
||||||
|
from lnbits.nodes import Node
|
||||||
from lnbits.nodes.base import (
|
from lnbits.nodes.base import (
|
||||||
ChannelBalance,
|
ChannelBalance,
|
||||||
ChannelPoint,
|
ChannelPoint,
|
||||||
ChannelState,
|
ChannelState,
|
||||||
ChannelStats,
|
ChannelStats,
|
||||||
Node,
|
|
||||||
NodeChannel,
|
NodeChannel,
|
||||||
NodeFees,
|
NodeFees,
|
||||||
NodeInfoResponse,
|
NodeInfoResponse,
|
||||||
|
|||||||
+1
-1
@@ -35,7 +35,7 @@ def main(
|
|||||||
ssl_certfile: str,
|
ssl_certfile: str,
|
||||||
reload: bool,
|
reload: bool,
|
||||||
):
|
):
|
||||||
"""Launched with `uv run lnbits` at root level"""
|
"""Launched with `poetry run lnbits` at root level"""
|
||||||
|
|
||||||
# create data dir if it does not exist
|
# create data dir if it does not exist
|
||||||
Path(settings.lnbits_data_folder).mkdir(parents=True, exist_ok=True)
|
Path(settings.lnbits_data_folder).mkdir(parents=True, exist_ok=True)
|
||||||
|
|||||||
+8
-24
@@ -142,14 +142,14 @@ class ExchangeRateProvider(BaseModel):
|
|||||||
|
|
||||||
class InstalledExtensionsSettings(LNbitsSettings):
|
class InstalledExtensionsSettings(LNbitsSettings):
|
||||||
# installed extensions that have been deactivated
|
# installed extensions that have been deactivated
|
||||||
lnbits_deactivated_extensions: set[str] = Field(default=set())
|
lnbits_deactivated_extensions: set[str] = Field(default=[])
|
||||||
# upgraded extensions that require API redirects
|
# upgraded extensions that require API redirects
|
||||||
lnbits_upgraded_extensions: dict[str, str] = Field(default={})
|
lnbits_upgraded_extensions: dict[str, str] = Field(default={})
|
||||||
# list of redirects that extensions want to perform
|
# list of redirects that extensions want to perform
|
||||||
lnbits_extensions_redirects: list[RedirectPath] = Field(default=[])
|
lnbits_extensions_redirects: list[RedirectPath] = Field(default=[])
|
||||||
|
|
||||||
# list of all extension ids
|
# list of all extension ids
|
||||||
lnbits_installed_extensions_ids: set[str] = Field(default=set())
|
lnbits_installed_extensions_ids: set[str] = Field(default=[])
|
||||||
|
|
||||||
def find_extension_redirect(
|
def find_extension_redirect(
|
||||||
self, path: str, req_headers: list[tuple[bytes, bytes]]
|
self, path: str, req_headers: list[tuple[bytes, bytes]]
|
||||||
@@ -266,7 +266,7 @@ class ThemesSettings(LNbitsSettings):
|
|||||||
lnbits_default_theme: str = Field(default="salvador")
|
lnbits_default_theme: str = Field(default="salvador")
|
||||||
lnbits_default_border: str = Field(default="hard-border")
|
lnbits_default_border: str = Field(default="hard-border")
|
||||||
lnbits_default_gradient: bool = Field(default=True)
|
lnbits_default_gradient: bool = Field(default=True)
|
||||||
lnbits_default_bgimage: str | None = Field(default=None)
|
lnbits_default_bgimage: str = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class OpsSettings(LNbitsSettings):
|
class OpsSettings(LNbitsSettings):
|
||||||
@@ -432,18 +432,6 @@ class NotificationsSettings(LNbitsSettings):
|
|||||||
default=1_000_000, ge=0
|
default=1_000_000, ge=0
|
||||||
)
|
)
|
||||||
|
|
||||||
def is_nostr_notifications_configured(self) -> bool:
|
|
||||||
return (
|
|
||||||
self.lnbits_nostr_notifications_enabled
|
|
||||||
and self.lnbits_nostr_notifications_private_key is not None
|
|
||||||
)
|
|
||||||
|
|
||||||
def is_telegram_notifications_configured(self) -> bool:
|
|
||||||
return (
|
|
||||||
self.lnbits_telegram_notifications_enabled
|
|
||||||
and self.lnbits_telegram_notifications_access_token is not None
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class FakeWalletFundingSource(LNbitsSettings):
|
class FakeWalletFundingSource(LNbitsSettings):
|
||||||
fake_wallet_secret: str = Field(default="ToTheMoon1")
|
fake_wallet_secret: str = Field(default="ToTheMoon1")
|
||||||
@@ -729,7 +717,7 @@ class AuthMethods(Enum):
|
|||||||
|
|
||||||
class AuthSettings(LNbitsSettings):
|
class AuthSettings(LNbitsSettings):
|
||||||
auth_token_expire_minutes: int = Field(default=525600, gt=0)
|
auth_token_expire_minutes: int = Field(default=525600, gt=0)
|
||||||
auth_all_methods: list[str] = [a.value for a in AuthMethods]
|
auth_all_methods = [a.value for a in AuthMethods]
|
||||||
auth_allowed_methods: list[str] = Field(
|
auth_allowed_methods: list[str] = Field(
|
||||||
default=[
|
default=[
|
||||||
AuthMethods.user_id_only.value,
|
AuthMethods.user_id_only.value,
|
||||||
@@ -947,7 +935,7 @@ class EnvSettings(LNbitsSettings):
|
|||||||
|
|
||||||
class PersistenceSettings(LNbitsSettings):
|
class PersistenceSettings(LNbitsSettings):
|
||||||
lnbits_data_folder: str = Field(default="./data")
|
lnbits_data_folder: str = Field(default="./data")
|
||||||
lnbits_database_url: str | None = Field(default=None)
|
lnbits_database_url: str = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class SuperUserSettings(LNbitsSettings):
|
class SuperUserSettings(LNbitsSettings):
|
||||||
@@ -974,7 +962,6 @@ class SuperUserSettings(LNbitsSettings):
|
|||||||
"ZBDWallet",
|
"ZBDWallet",
|
||||||
"NWCWallet",
|
"NWCWallet",
|
||||||
"StrikeWallet",
|
"StrikeWallet",
|
||||||
"ArkFakeWallet",
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -994,14 +981,11 @@ class TransientSettings(InstalledExtensionsSettings, ExchangeHistorySettings):
|
|||||||
lnbits_running: bool = Field(default=True)
|
lnbits_running: bool = Field(default=True)
|
||||||
|
|
||||||
# Remember the latest balance delta in order to compare with the current one
|
# Remember the latest balance delta in order to compare with the current one
|
||||||
latest_balance_delta_sats: int = Field(default=0)
|
latest_balance_delta_sats: int = Field(default=None)
|
||||||
|
|
||||||
lnbits_all_extensions_ids: set[str] = Field(default=set())
|
lnbits_all_extensions_ids: set[str] = Field(default=[])
|
||||||
|
|
||||||
server_startup_time: int = Field(default=int(time()))
|
server_startup_time: int = Field(default=time())
|
||||||
|
|
||||||
has_holdinvoice: bool = Field(default=False)
|
|
||||||
has_nodemanager: bool = Field(default=False)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def lnbits_server_up_time(self) -> str:
|
def lnbits_server_up_time(self) -> str:
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+5
-5
File diff suppressed because one or more lines are too long
@@ -62,14 +62,6 @@ body[data-theme=monochrome].neon-border .q-date--dark {
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[data-theme=salvador].neon-border .q-card,
|
|
||||||
body[data-theme=salvador].neon-border .q-card.q-card--dark,
|
|
||||||
body[data-theme=salvador].neon-border .q-date,
|
|
||||||
body[data-theme=salvador].neon-border .q-date--dark {
|
|
||||||
border: 2px solid #1976d2;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.hard-border .q-card,
|
body.hard-border .q-card,
|
||||||
body.hard-border .q-card.q-card--dark,
|
body.hard-border .q-card.q-card--dark,
|
||||||
body.hard-border .q-date,
|
body.hard-border .q-date,
|
||||||
@@ -117,21 +109,21 @@ body[data-theme=bitcoin] [data-theme=bitcoin] .q-stepper--dark {
|
|||||||
body[data-theme=freedom] {
|
body[data-theme=freedom] {
|
||||||
--q-primary: #e22156;
|
--q-primary: #e22156;
|
||||||
--q-secondary: #b91a45;
|
--q-secondary: #b91a45;
|
||||||
--q-dark-page: #462f36;
|
--q-dark-page: #0a0a0a;
|
||||||
}
|
}
|
||||||
body[data-theme=freedom] [data-theme=freedom] .q-card--dark,
|
body[data-theme=freedom] [data-theme=freedom] .q-card--dark,
|
||||||
body[data-theme=freedom] [data-theme=freedom] .q-stepper--dark {
|
body[data-theme=freedom] [data-theme=freedom] .q-stepper--dark {
|
||||||
background: #47393d !important;
|
background: #1b1b1b !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[data-theme=cyber] {
|
body[data-theme=cyber] {
|
||||||
--q-primary: #7cb342;
|
--q-primary: #7cb342;
|
||||||
--q-secondary: #558b2f;
|
--q-secondary: #558b2f;
|
||||||
--q-dark-page: #000;
|
--q-dark-page: #0a0a0a;
|
||||||
}
|
}
|
||||||
body[data-theme=cyber] [data-theme=cyber] .q-card--dark,
|
body[data-theme=cyber] [data-theme=cyber] .q-card--dark,
|
||||||
body[data-theme=cyber] [data-theme=cyber] .q-stepper--dark {
|
body[data-theme=cyber] [data-theme=cyber] .q-stepper--dark {
|
||||||
background: #1f2915 !important;
|
background: #1b1b1b !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[data-theme=mint] {
|
body[data-theme=mint] {
|
||||||
@@ -174,16 +166,6 @@ body[data-theme=monochrome] [data-theme=monochrome] .q-stepper--dark {
|
|||||||
background: rgb(39, 39, 39) !important;
|
background: rgb(39, 39, 39) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[data-theme=salvador] {
|
|
||||||
--q-primary: #1976d2;
|
|
||||||
--q-secondary: #26a69a;
|
|
||||||
--q-dark-page: #253647;
|
|
||||||
}
|
|
||||||
body[data-theme=salvador] [data-theme=salvador] .q-card--dark,
|
|
||||||
body[data-theme=salvador] [data-theme=salvador] .q-stepper--dark {
|
|
||||||
background: #343d47 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.gradient-bg {
|
body.gradient-bg {
|
||||||
background-image: linear-gradient(to bottom right, var(--q-dark-page), #0a0a0a);
|
background-image: linear-gradient(to bottom right, var(--q-dark-page), #0a0a0a);
|
||||||
background-attachment: fixed;
|
background-attachment: fixed;
|
||||||
|
|||||||
@@ -49,11 +49,9 @@ window.localisation.en = {
|
|||||||
export_to_phone_desc:
|
export_to_phone_desc:
|
||||||
'This QR code contains your wallet URL with full access. You can scan it from your phone to open your wallet from there.',
|
'This QR code contains your wallet URL with full access. You can scan it from your phone to open your wallet from there.',
|
||||||
access_wallet_on_mobile: 'Mobile Access',
|
access_wallet_on_mobile: 'Mobile Access',
|
||||||
stored_paylinks: 'Stored LNURL pay links',
|
|
||||||
wallet: 'Wallet: ',
|
wallet: 'Wallet: ',
|
||||||
wallet_name: 'Wallet name',
|
wallet_name: 'Wallet name',
|
||||||
wallets: 'Wallets',
|
wallets: 'Wallets',
|
||||||
exclude_wallets: 'Exclude Wallets',
|
|
||||||
add_wallet: 'Add wallet',
|
add_wallet: 'Add wallet',
|
||||||
add_new_wallet: 'Add a new wallet',
|
add_new_wallet: 'Add a new wallet',
|
||||||
pin_wallet: 'Pin wallet',
|
pin_wallet: 'Pin wallet',
|
||||||
@@ -115,7 +113,6 @@ window.localisation.en = {
|
|||||||
payment_check: 'Check payment',
|
payment_check: 'Check payment',
|
||||||
not_enough_funds: 'Not enough funds!',
|
not_enough_funds: 'Not enough funds!',
|
||||||
search_by_tag_memo_amount: 'Search by tag, memo, amount',
|
search_by_tag_memo_amount: 'Search by tag, memo, amount',
|
||||||
search: 'Search',
|
|
||||||
invoice_waiting: 'Invoice waiting to be paid',
|
invoice_waiting: 'Invoice waiting to be paid',
|
||||||
payment_received: 'Payment Received',
|
payment_received: 'Payment Received',
|
||||||
payment_sent: 'Payment Sent',
|
payment_sent: 'Payment Sent',
|
||||||
@@ -179,17 +176,7 @@ window.localisation.en = {
|
|||||||
extension_required_lnbits_version: 'This release requires LNbits version',
|
extension_required_lnbits_version: 'This release requires LNbits version',
|
||||||
min_version: 'Minimum (included)',
|
min_version: 'Minimum (included)',
|
||||||
max_version: 'Maximum (excluded)',
|
max_version: 'Maximum (excluded)',
|
||||||
preimage: 'Preimage',
|
|
||||||
preimage_hint: 'Preimage to settle the hold invoice',
|
|
||||||
hold_invoice: 'Hold Invoice',
|
|
||||||
hold_invoice_description:
|
|
||||||
'This invoice is on hold and requires a preimage to settle.',
|
|
||||||
payment_hash: 'Payment Hash',
|
payment_hash: 'Payment Hash',
|
||||||
invoice_cancelled: 'Invoice Cancelled',
|
|
||||||
invoice_settled: 'Invoice Settled',
|
|
||||||
hold_invoice_payment_hash: 'Payment hash for hold invoice (optional)',
|
|
||||||
settle_invoice: 'Settle Invoice',
|
|
||||||
cancel_invoice: 'Cancel Invoice',
|
|
||||||
fee: 'Fee',
|
fee: 'Fee',
|
||||||
amount: 'Amount',
|
amount: 'Amount',
|
||||||
amount_limits: 'Amount Limits',
|
amount_limits: 'Amount Limits',
|
||||||
@@ -232,9 +219,6 @@ window.localisation.en = {
|
|||||||
notifications_nostr_private_key: 'Nostr Private Key',
|
notifications_nostr_private_key: 'Nostr Private Key',
|
||||||
notifications_nostr_private_key_desc:
|
notifications_nostr_private_key_desc:
|
||||||
'Private key (hex or nsec) to sign the messages sent to Nostr',
|
'Private key (hex or nsec) to sign the messages sent to Nostr',
|
||||||
notifications_nostr_identifier: 'Nostr Identifier',
|
|
||||||
notifications_nostr_identifier_desc:
|
|
||||||
'Nip5 identifier to send notifications to',
|
|
||||||
notifications_nostr_identifiers: 'Nostr Identifiers',
|
notifications_nostr_identifiers: 'Nostr Identifiers',
|
||||||
notifications_nostr_identifiers_desc:
|
notifications_nostr_identifiers_desc:
|
||||||
'List of identifiers to send notifications to',
|
'List of identifiers to send notifications to',
|
||||||
@@ -244,10 +228,9 @@ window.localisation.en = {
|
|||||||
notifications_enable_telegram_desc: 'Send notfications over Telegram',
|
notifications_enable_telegram_desc: 'Send notfications over Telegram',
|
||||||
notifications_telegram_access_token: 'Access Token',
|
notifications_telegram_access_token: 'Access Token',
|
||||||
notifications_telegram_access_token_desc: 'Access token for the bot',
|
notifications_telegram_access_token_desc: 'Access token for the bot',
|
||||||
notifications_chat_id: 'Telegram Chat ID',
|
notifications_chat_id: 'Chat ID',
|
||||||
notifications_chat_id_desc: 'Telegram Chat ID to send the notifications to',
|
notifications_chat_id_desc: 'Chat ID to send the notifications to',
|
||||||
notifications_excluded_wallets_desc:
|
|
||||||
'Do not send notifications for these wallets',
|
|
||||||
notifications_email_config: 'Email Configuration',
|
notifications_email_config: 'Email Configuration',
|
||||||
notifications_enable_email: 'Enable Email',
|
notifications_enable_email: 'Enable Email',
|
||||||
notifications_enable_email_desc: 'Send notifications over email',
|
notifications_enable_email_desc: 'Send notifications over email',
|
||||||
@@ -651,7 +634,5 @@ window.localisation.en = {
|
|||||||
'Signing secret for the webhook. Messages will be signed with this secret.',
|
'Signing secret for the webhook. Messages will be signed with this secret.',
|
||||||
callback_success_url: 'Callback Success URL',
|
callback_success_url: 'Callback Success URL',
|
||||||
callback_success_url_hint:
|
callback_success_url_hint:
|
||||||
'The user will be redirected to this URL after the payment is successful',
|
'The user will be redirected to this URL after the payment is successful'
|
||||||
connected: 'Connected',
|
|
||||||
not_connected: 'Not Connected'
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ window.localisation.fi = {
|
|||||||
active_channels: 'Aktiivisia kanavia',
|
active_channels: 'Aktiivisia kanavia',
|
||||||
connect_peer: 'Yhdistä naapuriin',
|
connect_peer: 'Yhdistä naapuriin',
|
||||||
connect: 'Yhdistä',
|
connect: 'Yhdistä',
|
||||||
reconnect: 'Uudista yhteys',
|
|
||||||
open_channel: 'Avaa kanava',
|
open_channel: 'Avaa kanava',
|
||||||
open: 'Avaa',
|
open: 'Avaa',
|
||||||
close_channel: 'Sulje kanava',
|
close_channel: 'Sulje kanava',
|
||||||
@@ -52,18 +51,14 @@ window.localisation.fi = {
|
|||||||
'Tämä QR-koodi sisältää URL-osoitteen, jolla saa lompakkoosi täydet valtuudet. Voit lukea sen puhelimellasi ja avata sillä lompakkosi. Voit myös lisätä lompakkosi selaimella käytettäväksi PWA-sovellukseksi puhelimen aloitusruudulle. ',
|
'Tämä QR-koodi sisältää URL-osoitteen, jolla saa lompakkoosi täydet valtuudet. Voit lukea sen puhelimellasi ja avata sillä lompakkosi. Voit myös lisätä lompakkosi selaimella käytettäväksi PWA-sovellukseksi puhelimen aloitusruudulle. ',
|
||||||
access_wallet_on_mobile: 'Mobiili käyttö',
|
access_wallet_on_mobile: 'Mobiili käyttö',
|
||||||
wallet: 'Lompakko:',
|
wallet: 'Lompakko:',
|
||||||
wallet_name: 'Lompakon nimi',
|
|
||||||
wallets: 'Lompakot',
|
wallets: 'Lompakot',
|
||||||
add_wallet: 'Lisää lompakko',
|
add_wallet: 'Lisää lompakko',
|
||||||
add_new_wallet: 'Lisää uusi lompakko',
|
|
||||||
pin_wallet: 'Kiinnitä lompakko',
|
|
||||||
delete_wallet: 'Poista lompakko',
|
delete_wallet: 'Poista lompakko',
|
||||||
delete_wallet_desc:
|
delete_wallet_desc:
|
||||||
'Lompakko poistetaan pysyvästi. Siirrä lompakosta varat ennalta muualle, sillä tämä toiminto on PERUUTTAMATON!',
|
'Lompakko poistetaan pysyvästi. Siirrä lompakosta varat ennalta muualle, sillä tämä toiminto on PERUUTTAMATON!',
|
||||||
rename_wallet: 'Nimeä lompakko uudelleen',
|
rename_wallet: 'Nimeä lompakko uudelleen',
|
||||||
update_name: 'Tallenna',
|
update_name: 'Tallenna',
|
||||||
fiat_tracking: 'Käytettävä valuutta',
|
fiat_tracking: 'Käytettävä valuutta',
|
||||||
fiat_providers: 'Valuutan välittäjät',
|
|
||||||
currency: 'Valuutta',
|
currency: 'Valuutta',
|
||||||
update_currency: 'Tallenna',
|
update_currency: 'Tallenna',
|
||||||
press_to_claim: 'Lunasta varat painamalla tästä',
|
press_to_claim: 'Lunasta varat painamalla tästä',
|
||||||
@@ -127,7 +122,6 @@ window.localisation.fi = {
|
|||||||
no_extensions: 'Laajennuksia ei ole asennettu :(',
|
no_extensions: 'Laajennuksia ei ole asennettu :(',
|
||||||
created: 'Luotu',
|
created: 'Luotu',
|
||||||
search_extensions: 'Etsi laajennuksia',
|
search_extensions: 'Etsi laajennuksia',
|
||||||
search_wallets: 'Etsi lompakkoa',
|
|
||||||
extension_sources: 'Laajennuslähteet',
|
extension_sources: 'Laajennuslähteet',
|
||||||
ext_sources_hint: 'Lähteet joista laajennuksia voi ladata',
|
ext_sources_hint: 'Lähteet joista laajennuksia voi ladata',
|
||||||
ext_sources_label:
|
ext_sources_label:
|
||||||
@@ -140,7 +134,6 @@ window.localisation.fi = {
|
|||||||
uninstall: 'Poista',
|
uninstall: 'Poista',
|
||||||
drop_db: 'Poista tiedot',
|
drop_db: 'Poista tiedot',
|
||||||
enable: 'Ota käyttöön',
|
enable: 'Ota käyttöön',
|
||||||
enabled: 'Käytössä',
|
|
||||||
pay_to_enable: 'Maksa ottaaksesi käyttöön',
|
pay_to_enable: 'Maksa ottaaksesi käyttöön',
|
||||||
enable_extension_details: 'Ota laajennus käyttöön tälle käyttäjälle',
|
enable_extension_details: 'Ota laajennus käyttöön tälle käyttäjälle',
|
||||||
disable: 'Poista käytöstä',
|
disable: 'Poista käytöstä',
|
||||||
@@ -172,33 +165,12 @@ window.localisation.fi = {
|
|||||||
payment_hash: 'Maksun tiiviste',
|
payment_hash: 'Maksun tiiviste',
|
||||||
fee: 'Kulu',
|
fee: 'Kulu',
|
||||||
amount: 'Määrä',
|
amount: 'Määrä',
|
||||||
amount_limits: 'Määrien rajat',
|
|
||||||
amount_sats: 'Määrä (sat)',
|
amount_sats: 'Määrä (sat)',
|
||||||
faucest_wallet: 'Faucet Wallet',
|
|
||||||
faucest_wallet_desc_1:
|
|
||||||
'Each time a payment is confirmed by the {provider} provider funds will be subtracted from this wallet.',
|
|
||||||
faucest_wallet_desc_2:
|
|
||||||
'This helps monitor all {provider} payments and their status.',
|
|
||||||
faucest_wallet_desc_3:
|
|
||||||
'This wallet must be topped up with the amount of sats that the admin is willing to offer in exchange for the fiat currency.',
|
|
||||||
faucest_wallet_desc_4:
|
|
||||||
'If this wallet is configured, but is empty, the {provider} payments will not be processed.',
|
|
||||||
faucest_wallet_desc_5:
|
|
||||||
'This wallet can eventually get to a negative balance if parallel fiat payments are made.',
|
|
||||||
faucest_wallet_id: 'Faucet Wallet ID (optional)',
|
|
||||||
faucest_wallet_id_hint:
|
|
||||||
'Wallet ID to use for the faucet. It will be used to send the funds to the user.',
|
|
||||||
tag: 'Tunniste',
|
tag: 'Tunniste',
|
||||||
unit: 'Yksikkö',
|
unit: 'Yksikkö',
|
||||||
description: 'Kuvaus',
|
description: 'Kuvaus',
|
||||||
expiry: 'Vanhenee',
|
expiry: 'Vanhenee',
|
||||||
webhook: 'Webhook',
|
webhook: 'Webhook',
|
||||||
webhook_url: 'Webhook URL',
|
|
||||||
webhook_url_hint:
|
|
||||||
'Webhook URL to send the payment details to. It will be called when the payment is completed.',
|
|
||||||
webhook_events_list: 'The following events must be supported by the webhook:',
|
|
||||||
webhook_stripe_description:
|
|
||||||
'One the stripe side you must configure a webhook with a URL that points to your LNbits server.',
|
|
||||||
payment_proof: 'Maksun varmenne',
|
payment_proof: 'Maksun varmenne',
|
||||||
update: 'Päivitä',
|
update: 'Päivitä',
|
||||||
update_available: 'Saatavilla on päivitys {version}-versioon!',
|
update_available: 'Saatavilla on päivitys {version}-versioon!',
|
||||||
@@ -290,7 +262,6 @@ window.localisation.fi = {
|
|||||||
notification_source_label:
|
notification_source_label:
|
||||||
'Lähde-URL (käytä ainoastaan LNbits:iä tai muuta luotettavaa lähdettä)',
|
'Lähde-URL (käytä ainoastaan LNbits:iä tai muuta luotettavaa lähdettä)',
|
||||||
more: 'näytä lisää',
|
more: 'näytä lisää',
|
||||||
more_count: 'näytä {count} lisää',
|
|
||||||
less: 'supista',
|
less: 'supista',
|
||||||
releases: 'Julkaisut',
|
releases: 'Julkaisut',
|
||||||
watchdog: 'Watchdog',
|
watchdog: 'Watchdog',
|
||||||
@@ -305,7 +276,7 @@ window.localisation.fi = {
|
|||||||
callback_url_rules: 'Callback URL -säännöt',
|
callback_url_rules: 'Callback URL -säännöt',
|
||||||
enter_callback_url_rule: 'Anna URL-sääntö regex-muodossa ja paina enter',
|
enter_callback_url_rule: 'Anna URL-sääntö regex-muodossa ja paina enter',
|
||||||
callback_url_rule_hint:
|
callback_url_rule_hint:
|
||||||
'Callback URL:it (kuten LNURL) tarkistetaan kaikkien näiden sääntöjen mukaisesti. Jos sääntöjä ei ole määritetty, kaikki URL:it ovat sallittuja.',
|
'Callback URL:it (kuten LNURL) tarkistetaan näiden sääntöjen mukaisesti. Jos sääntöjä ei ole määritetty, kaikki URL:it ovat sallittuja.',
|
||||||
wallet_limiter: 'Lompakon käyttörajoitin',
|
wallet_limiter: 'Lompakon käyttörajoitin',
|
||||||
wallet_config: 'Wallet Config',
|
wallet_config: 'Wallet Config',
|
||||||
wallet_charts: 'Wallet Charts',
|
wallet_charts: 'Wallet Charts',
|
||||||
@@ -330,9 +301,6 @@ window.localisation.fi = {
|
|||||||
login_with_user_id: 'Kirjaudu käyttäjä-ID:llä',
|
login_with_user_id: 'Kirjaudu käyttäjä-ID:llä',
|
||||||
or: 'tai',
|
or: 'tai',
|
||||||
create_new_wallet: 'Avaa uusi lompakko',
|
create_new_wallet: 'Avaa uusi lompakko',
|
||||||
delete_all_wallets: 'Poista kaikki lompakot',
|
|
||||||
confirm_delete_all_wallets:
|
|
||||||
'Oletko todellakin varma, että haluat poistaa käyttäjältä KAIKKI lompakot?',
|
|
||||||
login_to_account: 'Kirjaudu käyttäjänimellä',
|
login_to_account: 'Kirjaudu käyttäjänimellä',
|
||||||
create_account: 'Luo tili',
|
create_account: 'Luo tili',
|
||||||
account_settings: 'Tilin asetukset',
|
account_settings: 'Tilin asetukset',
|
||||||
@@ -341,7 +309,7 @@ window.localisation.fi = {
|
|||||||
signin_with_nostr: 'Kirjaudu Nostr:lla',
|
signin_with_nostr: 'Kirjaudu Nostr:lla',
|
||||||
signin_with_google: 'Kirjaudu Google-tunnuksella',
|
signin_with_google: 'Kirjaudu Google-tunnuksella',
|
||||||
signin_with_github: 'Kirjaudu GitHub-tunnuksella',
|
signin_with_github: 'Kirjaudu GitHub-tunnuksella',
|
||||||
signin_with_custom_org: 'Kirjaudu {custom_org}-palvelulla',
|
signin_with_keycloak: 'Kirjaudu Keycloak-tunnuksella',
|
||||||
username_or_email: 'Käyttäjänimi tai sähköposti',
|
username_or_email: 'Käyttäjänimi tai sähköposti',
|
||||||
password: 'Anna uusi salasana',
|
password: 'Anna uusi salasana',
|
||||||
password_config: 'Salasanan määritys',
|
password_config: 'Salasanan määritys',
|
||||||
@@ -350,10 +318,7 @@ window.localisation.fi = {
|
|||||||
change_password: 'Vaihda salasana',
|
change_password: 'Vaihda salasana',
|
||||||
update_credentials: 'Päivitä käyttöoikeustiedot',
|
update_credentials: 'Päivitä käyttöoikeustiedot',
|
||||||
update_pubkey: 'Päivitä julkinen avain',
|
update_pubkey: 'Päivitä julkinen avain',
|
||||||
nostr_pubkey_tooltip:
|
|
||||||
'Syötä tämän käyttäjän julkinen Nostr avain (hex arvona)',
|
|
||||||
set_password: 'Aseta salasana',
|
set_password: 'Aseta salasana',
|
||||||
set_password_tooltip: 'Aseta käyttäjätunnukselle salasana',
|
|
||||||
invalid_password: 'Salasanassa tulee olla vähintään kahdeksan merkkiä',
|
invalid_password: 'Salasanassa tulee olla vähintään kahdeksan merkkiä',
|
||||||
invalid_password_repeat: 'Salasanat eivät täsmää',
|
invalid_password_repeat: 'Salasanat eivät täsmää',
|
||||||
reset_key_generated: 'Salasanan vaihtoavain on luotu.',
|
reset_key_generated: 'Salasanan vaihtoavain on luotu.',
|
||||||
@@ -362,8 +327,7 @@ window.localisation.fi = {
|
|||||||
register: 'Rekisteröidy',
|
register: 'Rekisteröidy',
|
||||||
username: 'Käyttäjänimi',
|
username: 'Käyttäjänimi',
|
||||||
pubkey: 'Julkinen avain',
|
pubkey: 'Julkinen avain',
|
||||||
user_id: 'Käyttäjä tunnus',
|
user_id: 'Käyttäjä ID',
|
||||||
id: 'tunnus',
|
|
||||||
email: 'Sähköposti',
|
email: 'Sähköposti',
|
||||||
first_name: 'Etunimi',
|
first_name: 'Etunimi',
|
||||||
last_name: 'Sukunimi',
|
last_name: 'Sukunimi',
|
||||||
@@ -379,8 +343,6 @@ window.localisation.fi = {
|
|||||||
back: 'Takaisin',
|
back: 'Takaisin',
|
||||||
logout: 'Poistu',
|
logout: 'Poistu',
|
||||||
look_and_feel: 'Kieli ja värit',
|
look_and_feel: 'Kieli ja värit',
|
||||||
endpoint: 'Endpoint',
|
|
||||||
api: 'API',
|
|
||||||
api_token: 'API Token',
|
api_token: 'API Token',
|
||||||
api_tokens: 'API Tokens',
|
api_tokens: 'API Tokens',
|
||||||
access_control_list: 'Access Control List',
|
access_control_list: 'Access Control List',
|
||||||
@@ -392,14 +354,11 @@ window.localisation.fi = {
|
|||||||
gradient_background: 'Gradient Background',
|
gradient_background: 'Gradient Background',
|
||||||
language: 'Kieli',
|
language: 'Kieli',
|
||||||
color_scheme: 'Väriteema',
|
color_scheme: 'Väriteema',
|
||||||
visible_wallet_count: 'Näytettävien lompakkojen määrä',
|
|
||||||
admin_settings: 'Pääkäyttäjän asetukset',
|
admin_settings: 'Pääkäyttäjän asetukset',
|
||||||
extension_cost: 'Tämä laajennus edellyttää vähintään {cost} sat maksua.',
|
extension_cost: 'Tämä laajennus edellyttää vähintään {cost} sat maksua.',
|
||||||
extension_paid_sats: 'Olet jo maksanut {paid_sats} satsia.',
|
extension_paid_sats: 'Olet jo maksanut {paid_sats} satsia.',
|
||||||
release_details_error: 'Ei voi hakea julkaisun tietoja.',
|
release_details_error: 'Ei voi hakea julkaisun tietoja.',
|
||||||
pay_from_wallet: 'Maksa lompakosta',
|
pay_from_wallet: 'Maksa lompakosta',
|
||||||
pay_with: 'Maksa {provider}:lla',
|
|
||||||
select_payment_provider: 'Valitse maksun välittäjä',
|
|
||||||
wallet_required: 'Lompakko *',
|
wallet_required: 'Lompakko *',
|
||||||
show_qr: 'Näytä QR',
|
show_qr: 'Näytä QR',
|
||||||
retry_install: 'Yritä asennusta uudelleen',
|
retry_install: 'Yritä asennusta uudelleen',
|
||||||
@@ -412,9 +371,6 @@ window.localisation.fi = {
|
|||||||
'{name} -laajennuksen aktivointi edellyttää vähintään {amount} sat maksua.',
|
'{name} -laajennuksen aktivointi edellyttää vähintään {amount} sat maksua.',
|
||||||
hide_empty_wallets: 'Piilota tyhjät lompakot',
|
hide_empty_wallets: 'Piilota tyhjät lompakot',
|
||||||
recheck: 'Tarkista uudelleen',
|
recheck: 'Tarkista uudelleen',
|
||||||
check: 'Tarkista',
|
|
||||||
check_connection: 'Tarkista yhteys',
|
|
||||||
check_webhook: 'Tarkista Webhook',
|
|
||||||
contributors: 'Avustajat',
|
contributors: 'Avustajat',
|
||||||
license: 'Lisenssi',
|
license: 'Lisenssi',
|
||||||
reset_key: 'Vaihda avain',
|
reset_key: 'Vaihda avain',
|
||||||
@@ -491,9 +447,9 @@ window.localisation.fi = {
|
|||||||
fee_reserve_percent: 'Kuluvaraus prosentteina',
|
fee_reserve_percent: 'Kuluvaraus prosentteina',
|
||||||
fee_reserve_msats: 'Kuluvaraus milli-sat',
|
fee_reserve_msats: 'Kuluvaraus milli-sat',
|
||||||
reserve_fee_in_percent: 'Kuluvaraus prosentteina',
|
reserve_fee_in_percent: 'Kuluvaraus prosentteina',
|
||||||
payment_wait_time: 'Maksun odotusaika (sekuntia)',
|
payment_wait_time: 'Maksun odotusaika',
|
||||||
payment_wait_time_desc:
|
payment_wait_time_desc:
|
||||||
'Kuinka pitkään maksua odotetaan saapuvaksi, ennen kuin se merkitään Odotetaan-tilaan. Aseta pidemmäksi käytettäessä HODL-laskuja, Boltz-palvelua, tms',
|
'Kuinka pitkään maksua odotetaan saapuvaksi ennenkuin se merkitään Odotetaan -tilaan. Aseta pidemmäksi käytettäessä HODL-laskuja, Boltz-palvelua tms',
|
||||||
server_management: 'Palvelimen hallinta',
|
server_management: 'Palvelimen hallinta',
|
||||||
base_url: 'Palvelimen URL-osoite',
|
base_url: 'Palvelimen URL-osoite',
|
||||||
base_url_label: 'Palvelun staattinen pohja-URL',
|
base_url_label: 'Palvelun staattinen pohja-URL',
|
||||||
@@ -518,16 +474,12 @@ window.localisation.fi = {
|
|||||||
auth_keycloak_ci_hint:
|
auth_keycloak_ci_hint:
|
||||||
'Varmista, että valtuutuksen palautus-URL on asetettu muotoon https://{domain}/api/v1/auth/keycloak/token',
|
'Varmista, että valtuutuksen palautus-URL on asetettu muotoon https://{domain}/api/v1/auth/keycloak/token',
|
||||||
auth_keycloak_cs_label: 'Keycloak-asiakassalasana',
|
auth_keycloak_cs_label: 'Keycloak-asiakassalasana',
|
||||||
auth_keycloak_custom_org_label: 'Valinnainen Keycloak-organisaatio',
|
|
||||||
auth_keycloak_custom_icon_label: 'Valinnainen Keycloak-kuvake (URL)',
|
|
||||||
currency_settings: 'Valuutta-asetukset',
|
currency_settings: 'Valuutta-asetukset',
|
||||||
allowed_currencies: 'Käytettävät valuutat',
|
allowed_currencies: 'Käytettävät valuutat',
|
||||||
allowed_currencies_hint: 'Valitse käytettävissä olevat fiat-valuutat',
|
allowed_currencies_hint: 'Valitse käytettävissä olevat fiat-valuutat',
|
||||||
default_account_currency: 'Tilin oletusvaluutta',
|
default_account_currency: 'Tilin oletusvaluutta',
|
||||||
default_account_currency_hint: 'Kirjanpidon oletusvaluutta',
|
default_account_currency_hint: 'Kirjanpidon oletusvaluutta',
|
||||||
min_incoming_payment_amount: 'Pienin vastaanotettava maksun määrä',
|
|
||||||
min_incoming_payment_amount_desc:
|
|
||||||
'Pienin maksun määrä jolle voi luoda laskun',
|
|
||||||
max_incoming_payment_amount: 'Saapuvan maksun enimmäismäärä',
|
max_incoming_payment_amount: 'Saapuvan maksun enimmäismäärä',
|
||||||
max_incoming_payment_amount_desc: 'Enimmäismäärä jonka voi laskuttaa',
|
max_incoming_payment_amount_desc: 'Enimmäismäärä jonka voi laskuttaa',
|
||||||
max_outgoing_payment_amount: 'Lähtevän maksun enimmäismäärä',
|
max_outgoing_payment_amount: 'Lähtevän maksun enimmäismäärä',
|
||||||
@@ -590,8 +542,6 @@ window.localisation.fi = {
|
|||||||
admin_users_label: 'Käyttäjätunnus',
|
admin_users_label: 'Käyttäjätunnus',
|
||||||
allowed_users: 'Sallitut käyttäjät',
|
allowed_users: 'Sallitut käyttäjät',
|
||||||
allowed_users_hint: 'Vain nämä käyttäjät voivat käyttää LNbitsiä',
|
allowed_users_hint: 'Vain nämä käyttäjät voivat käyttää LNbitsiä',
|
||||||
allowed_users_hint_feature:
|
|
||||||
'Ainoastaan nämä käyttäjät voivat käyttää ominaisuutta {feature}',
|
|
||||||
allowed_users_label: 'Käyttäjätunnus',
|
allowed_users_label: 'Käyttäjätunnus',
|
||||||
allow_creation_user: 'Salli uusien käyttäjien luominen',
|
allow_creation_user: 'Salli uusien käyttäjien luominen',
|
||||||
allow_creation_user_desc: 'Etusivulta on mahdollisuus luoda uusia käyttäjiä',
|
allow_creation_user_desc: 'Etusivulta on mahdollisuus luoda uusia käyttäjiä',
|
||||||
@@ -627,12 +577,5 @@ window.localisation.fi = {
|
|||||||
view_column: 'Näytä lompakot rinnakkain',
|
view_column: 'Näytä lompakot rinnakkain',
|
||||||
filter_payments: 'Suodata maksuja',
|
filter_payments: 'Suodata maksuja',
|
||||||
filter_date: 'Suodata päiväyksellä',
|
filter_date: 'Suodata päiväyksellä',
|
||||||
websocket_example: 'Websocket example',
|
websocket_example: 'Websocket esimerkki'
|
||||||
secret_key: 'Secret Key',
|
|
||||||
signing_secret: 'Signing Secret',
|
|
||||||
signing_secret_hint:
|
|
||||||
'Signing secret for the webhook. Messages will be signed with this secret.',
|
|
||||||
callback_success_url: 'Callback Success URL',
|
|
||||||
callback_success_url_hint:
|
|
||||||
'The user will be redirected to this URL after the payment is successful'
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,11 +80,6 @@ window.AccountPageLogic = {
|
|||||||
token_id_list: [],
|
token_id_list: [],
|
||||||
allRead: false,
|
allRead: false,
|
||||||
allWrite: false
|
allWrite: false
|
||||||
},
|
|
||||||
notifications: {
|
|
||||||
nostr: {
|
|
||||||
identifier: ''
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -401,7 +396,6 @@ window.AccountPageLogic = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async created() {
|
async created() {
|
||||||
try {
|
try {
|
||||||
const {data} = await LNbits.api.getAuthenticatedUser()
|
const {data} = await LNbits.api.getAuthenticatedUser()
|
||||||
|
|||||||
+36
-14
@@ -19,19 +19,17 @@ window.LNbits = {
|
|||||||
amount,
|
amount,
|
||||||
memo,
|
memo,
|
||||||
unit = 'sat',
|
unit = 'sat',
|
||||||
lnurlWithdraw = null,
|
lnurlCallback = null,
|
||||||
fiatProvider = null,
|
fiatProvider = null,
|
||||||
internalMemo = null,
|
internalMemo = null
|
||||||
payment_hash = null
|
|
||||||
) {
|
) {
|
||||||
const data = {
|
const data = {
|
||||||
out: false,
|
out: false,
|
||||||
amount: amount,
|
amount: amount,
|
||||||
memo: memo,
|
memo: memo,
|
||||||
unit: unit,
|
unit: unit,
|
||||||
lnurl_withdraw: lnurlWithdraw,
|
lnurl_callback: lnurlCallback,
|
||||||
fiat_provider: fiatProvider,
|
fiat_provider: fiatProvider
|
||||||
payment_hash: payment_hash
|
|
||||||
}
|
}
|
||||||
if (internalMemo) {
|
if (internalMemo) {
|
||||||
data.extra = {
|
data.extra = {
|
||||||
@@ -52,14 +50,39 @@ window.LNbits = {
|
|||||||
}
|
}
|
||||||
return this.request('post', '/api/v1/payments', wallet.adminkey, data)
|
return this.request('post', '/api/v1/payments', wallet.adminkey, data)
|
||||||
},
|
},
|
||||||
cancelInvoice(wallet, paymentHash) {
|
payLnurl(
|
||||||
return this.request('post', '/api/v1/payments/cancel', wallet.adminkey, {
|
wallet,
|
||||||
payment_hash: paymentHash
|
callback,
|
||||||
})
|
description_hash,
|
||||||
|
amount,
|
||||||
|
description = '',
|
||||||
|
comment = '',
|
||||||
|
unit = '',
|
||||||
|
internalMemo = null
|
||||||
|
) {
|
||||||
|
const data = {
|
||||||
|
callback,
|
||||||
|
description_hash,
|
||||||
|
amount,
|
||||||
|
comment,
|
||||||
|
description,
|
||||||
|
unit
|
||||||
|
}
|
||||||
|
|
||||||
|
if (internalMemo) {
|
||||||
|
data.internal_memo = String(internalMemo)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.request(
|
||||||
|
'post',
|
||||||
|
'/api/v1/payments/lnurl',
|
||||||
|
wallet.adminkey,
|
||||||
|
data
|
||||||
|
)
|
||||||
},
|
},
|
||||||
settleInvoice(wallet, preimage) {
|
authLnurl(wallet, callback) {
|
||||||
return this.request('post', `/api/v1/payments/settle`, wallet.adminkey, {
|
return this.request('post', '/api/v1/lnurlauth', wallet.adminkey, {
|
||||||
preimage: preimage
|
callback
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
createAccount(name) {
|
createAccount(name) {
|
||||||
@@ -615,7 +638,6 @@ window.windowMixin = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
applyBackgroundImage() {
|
applyBackgroundImage() {
|
||||||
if (this.bgimageChoice == 'null') this.bgimageChoice = ''
|
|
||||||
if (this.bgimageChoice == '') {
|
if (this.bgimageChoice == '') {
|
||||||
document.body.classList.remove('bg-image')
|
document.body.classList.remove('bg-image')
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -187,6 +187,34 @@ window.app.component('lnbits-lnurlpay-success-action', {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
window.app.component('lnbits-qrcode', {
|
||||||
|
mixins: [window.windowMixin],
|
||||||
|
template: '#lnbits-qrcode',
|
||||||
|
components: {
|
||||||
|
QrcodeVue
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
options: Object
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
custom: {
|
||||||
|
margin: 3,
|
||||||
|
width: 350,
|
||||||
|
size: 350,
|
||||||
|
logo: LNBITS_QR_LOGO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.custom = {...this.custom, ...this.options}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
window.app.component('lnbits-notifications-btn', {
|
window.app.component('lnbits-notifications-btn', {
|
||||||
template: '#lnbits-notifications-btn',
|
template: '#lnbits-notifications-btn',
|
||||||
mixins: [window.windowMixin],
|
mixins: [window.windowMixin],
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
window.app.component('lnbits-qrcode-lnurl', {
|
|
||||||
template: '#lnbits-qrcode-lnurl',
|
|
||||||
mixins: [window.windowMixin],
|
|
||||||
props: {
|
|
||||||
url: {
|
|
||||||
required: true,
|
|
||||||
type: String
|
|
||||||
},
|
|
||||||
prefix: {
|
|
||||||
type: String,
|
|
||||||
default: 'lnurlp'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
tab: 'bech32',
|
|
||||||
lnurl: ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
setLnurl() {
|
|
||||||
if (this.tab == 'bech32') {
|
|
||||||
const bytes = new TextEncoder().encode(this.url)
|
|
||||||
const bech32 = NostrTools.nip19.encodeBytes('lnurl', bytes)
|
|
||||||
this.lnurl = `lightning:${bech32.toUpperCase()}`
|
|
||||||
} else if (this.tab == 'lud17') {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
url() {
|
|
||||||
this.setLnurl()
|
|
||||||
},
|
|
||||||
tab() {
|
|
||||||
this.setLnurl()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
this.setLnurl()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
window.app.component('lnbits-qrcode', {
|
|
||||||
mixins: [window.windowMixin],
|
|
||||||
template: '#lnbits-qrcode',
|
|
||||||
components: {
|
|
||||||
QrcodeVue
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
value: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
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 {
|
|
||||||
nfcTagWriting: false,
|
|
||||||
nfcSupported: typeof NDEFReader != 'undefined'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
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')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -118,13 +118,7 @@ window.app.component('payment-list', {
|
|||||||
field: row => row.extra.wallet_fiat_amount
|
field: row => row.extra.wallet_fiat_amount
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
preimage: null,
|
|
||||||
loading: false
|
loading: false
|
||||||
},
|
|
||||||
hodlInvoice: {
|
|
||||||
show: false,
|
|
||||||
payment: null,
|
|
||||||
preimage: null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -229,35 +223,6 @@ window.app.component('payment-list', {
|
|||||||
})
|
})
|
||||||
.catch(LNbits.utils.notifyApiError)
|
.catch(LNbits.utils.notifyApiError)
|
||||||
},
|
},
|
||||||
showHoldInvoiceDialog(payment) {
|
|
||||||
this.hodlInvoice.show = true
|
|
||||||
this.hodlInvoice.preimage = ''
|
|
||||||
this.hodlInvoice.payment = payment
|
|
||||||
},
|
|
||||||
cancelHoldInvoice(payment_hash) {
|
|
||||||
LNbits.api
|
|
||||||
.cancelInvoice(this.g.wallet, payment_hash)
|
|
||||||
.then(() => {
|
|
||||||
this.update = !this.update
|
|
||||||
Quasar.Notify.create({
|
|
||||||
type: 'positive',
|
|
||||||
message: this.$t('invoice_cancelled')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch(LNbits.utils.notifyApiError)
|
|
||||||
},
|
|
||||||
settleHoldInvoice(preimage) {
|
|
||||||
LNbits.api
|
|
||||||
.settleInvoice(this.g.wallet, preimage)
|
|
||||||
.then(() => {
|
|
||||||
this.update = !this.update
|
|
||||||
Quasar.Notify.create({
|
|
||||||
type: 'positive',
|
|
||||||
message: this.$t('invoice_settled')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch(LNbits.utils.notifyApiError)
|
|
||||||
},
|
|
||||||
paymentTableRowKey(row) {
|
paymentTableRowKey(row) {
|
||||||
return row.payment_hash + row.amount
|
return row.payment_hash + row.amount
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ function confettiStars() {
|
|||||||
setTimeout(shoot, 200)
|
setTimeout(shoot, 200)
|
||||||
}
|
}
|
||||||
!(function (t, e) {
|
!(function (t, e) {
|
||||||
;(!(function t(e, n, a, i) {
|
!(function t(e, n, a, i) {
|
||||||
var o = !!(
|
var o = !!(
|
||||||
e.Worker &&
|
e.Worker &&
|
||||||
e.Blob &&
|
e.Blob &&
|
||||||
@@ -248,12 +248,12 @@ function confettiStars() {
|
|||||||
function e(e, n) {
|
function e(e, n) {
|
||||||
t.postMessage({options: e || {}, callback: n})
|
t.postMessage({options: e || {}, callback: n})
|
||||||
}
|
}
|
||||||
;((t.init = function (e) {
|
;(t.init = function (e) {
|
||||||
var n = e.transferControlToOffscreen()
|
var n = e.transferControlToOffscreen()
|
||||||
t.postMessage({canvas: n}, [n])
|
t.postMessage({canvas: n}, [n])
|
||||||
}),
|
}),
|
||||||
(t.fire = function (n, a, i) {
|
(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)
|
var o = Math.random().toString(36).slice(2)
|
||||||
return (g = l(function (a) {
|
return (g = l(function (a) {
|
||||||
function r(e) {
|
function r(e) {
|
||||||
@@ -264,15 +264,15 @@ function confettiStars() {
|
|||||||
i(),
|
i(),
|
||||||
a())
|
a())
|
||||||
}
|
}
|
||||||
;(t.addEventListener('message', r),
|
t.addEventListener('message', r),
|
||||||
e(n, o),
|
e(n, o),
|
||||||
(m[o] = r.bind(null, {data: {callback: o}})))
|
(m[o] = r.bind(null, {data: {callback: o}}))
|
||||||
}))
|
}))
|
||||||
}),
|
}),
|
||||||
(t.reset = function () {
|
(t.reset = function () {
|
||||||
for (var e in (t.postMessage({reset: !0}), m))
|
for (var e in (t.postMessage({reset: !0}), m))
|
||||||
(m[e](), delete m[e])
|
m[e](), delete m[e]
|
||||||
}))
|
})
|
||||||
})(h)
|
})(h)
|
||||||
}
|
}
|
||||||
return h
|
return h
|
||||||
@@ -328,12 +328,12 @@ function confettiStars() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
function k(t) {
|
function k(t) {
|
||||||
;((t.width = document.documentElement.clientWidth),
|
;(t.width = document.documentElement.clientWidth),
|
||||||
(t.height = document.documentElement.clientHeight))
|
(t.height = document.documentElement.clientHeight)
|
||||||
}
|
}
|
||||||
function I(t) {
|
function I(t) {
|
||||||
var e = t.getBoundingClientRect()
|
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) {
|
function T(t, e, n, o, r) {
|
||||||
var c,
|
var c,
|
||||||
@@ -342,10 +342,10 @@ function confettiStars() {
|
|||||||
d = t.getContext('2d'),
|
d = t.getContext('2d'),
|
||||||
f = l(function (e) {
|
f = l(function (e) {
|
||||||
function l() {
|
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() {
|
;(c = b.frame(function e() {
|
||||||
;(!a ||
|
!a ||
|
||||||
(o.width === i.width && o.height === i.height) ||
|
(o.width === i.width && o.height === i.height) ||
|
||||||
((o.width = t.width = i.width), (o.height = t.height = i.height)),
|
((o.width = t.width = i.width), (o.height = t.height = i.height)),
|
||||||
o.width ||
|
o.width ||
|
||||||
@@ -354,7 +354,7 @@ function confettiStars() {
|
|||||||
d.clearRect(0, 0, o.width, o.height),
|
d.clearRect(0, 0, o.width, o.height),
|
||||||
(u = u.filter(function (t) {
|
(u = u.filter(function (t) {
|
||||||
return (function (t, e) {
|
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.y += Math.sin(e.angle2D) * e.velocity + e.gravity),
|
||||||
(e.wobble += 0.1),
|
(e.wobble += 0.1),
|
||||||
(e.velocity *= e.decay),
|
(e.velocity *= e.decay),
|
||||||
@@ -363,7 +363,7 @@ function confettiStars() {
|
|||||||
(e.tiltCos = Math.cos(e.tiltAngle)),
|
(e.tiltCos = Math.cos(e.tiltAngle)),
|
||||||
(e.random = Math.random() + 5),
|
(e.random = Math.random() + 5),
|
||||||
(e.wobbleX = e.x + 10 * e.scalar * Math.cos(e.wobble)),
|
(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,
|
var n = e.tick++ / e.totalTicks,
|
||||||
a = e.x + e.random * e.tiltCos,
|
a = e.x + e.random * e.tiltCos,
|
||||||
i = e.y + e.random * e.tiltSin,
|
i = e.y + e.random * e.tiltSin,
|
||||||
@@ -393,12 +393,12 @@ function confettiStars() {
|
|||||||
2 * Math.PI
|
2 * Math.PI
|
||||||
)
|
)
|
||||||
: (function (t, e, n, a, i, o, r, l, c) {
|
: (function (t, e, n, a, i, o, r, l, c) {
|
||||||
;(t.save(),
|
t.save(),
|
||||||
t.translate(e, n),
|
t.translate(e, n),
|
||||||
t.rotate(o),
|
t.rotate(o),
|
||||||
t.scale(a, i),
|
t.scale(a, i),
|
||||||
t.arc(0, 0, 1, r, l, c),
|
t.arc(0, 0, 1, r, l, c),
|
||||||
t.restore())
|
t.restore()
|
||||||
})(
|
})(
|
||||||
t,
|
t,
|
||||||
e.x,
|
e.x,
|
||||||
@@ -420,18 +420,18 @@ function confettiStars() {
|
|||||||
})(d, t)
|
})(d, t)
|
||||||
})).length
|
})).length
|
||||||
? (c = b.frame(e))
|
? (c = b.frame(e))
|
||||||
: l())
|
: l()
|
||||||
})),
|
})),
|
||||||
(s = l))
|
(s = l)
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
addFettis: function (t) {
|
addFettis: function (t) {
|
||||||
return ((u = u.concat(t)), f)
|
return (u = u.concat(t)), f
|
||||||
},
|
},
|
||||||
canvas: t,
|
canvas: t,
|
||||||
promise: f,
|
promise: f,
|
||||||
reset: function () {
|
reset: function () {
|
||||||
;(c && b.cancel(c), s && s())
|
c && b.cancel(c), s && s()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -466,7 +466,7 @@ function confettiStars() {
|
|||||||
k = p(e, 'scalar'),
|
k = p(e, 'scalar'),
|
||||||
I = (function (t) {
|
I = (function (t) {
|
||||||
var e = p(t, 'origin', Object)
|
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),
|
||||||
E = d,
|
E = d,
|
||||||
S = [],
|
S = [],
|
||||||
@@ -531,7 +531,7 @@ function confettiStars() {
|
|||||||
return l(function (t) {
|
return l(function (t) {
|
||||||
t()
|
t()
|
||||||
})
|
})
|
||||||
;(i && a
|
i && a
|
||||||
? (t = a.canvas)
|
? (t = a.canvas)
|
||||||
: i &&
|
: i &&
|
||||||
!t &&
|
!t &&
|
||||||
@@ -547,7 +547,7 @@ function confettiStars() {
|
|||||||
)
|
)
|
||||||
})(g)),
|
})(g)),
|
||||||
document.body.appendChild(t)),
|
document.body.appendChild(t)),
|
||||||
r && !d && u(t))
|
r && !d && u(t)
|
||||||
var m = {width: t.width, height: t.height}
|
var m = {width: t.width, height: t.height}
|
||||||
function b() {
|
function b() {
|
||||||
if (s) {
|
if (s) {
|
||||||
@@ -564,9 +564,9 @@ function confettiStars() {
|
|||||||
m.width = m.height = null
|
m.width = m.height = null
|
||||||
}
|
}
|
||||||
function v() {
|
function v() {
|
||||||
;((a = null),
|
;(a = null),
|
||||||
r && e.removeEventListener('resize', b),
|
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 (
|
return (
|
||||||
s && !d && s.init(t),
|
s && !d && s.init(t),
|
||||||
@@ -578,13 +578,12 @@ function confettiStars() {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
(g.reset = function () {
|
(g.reset = function () {
|
||||||
;(s && s.reset(), a && a.reset())
|
s && s.reset(), a && a.reset()
|
||||||
}),
|
}),
|
||||||
g
|
g
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
;((n.exports = E(null, {useWorker: !0, resize: !0})),
|
;(n.exports = E(null, {useWorker: !0, resize: !0})), (n.exports.create = E)
|
||||||
(n.exports.create = E))
|
|
||||||
})(
|
})(
|
||||||
(function () {
|
(function () {
|
||||||
return void 0 !== t ? t : 'undefined' != typeof self ? self : this || {}
|
return void 0 !== t ? t : 'undefined' != typeof self ? self : this || {}
|
||||||
@@ -592,5 +591,5 @@ function confettiStars() {
|
|||||||
e,
|
e,
|
||||||
!1
|
!1
|
||||||
),
|
),
|
||||||
(t.confetti = e.exports))
|
(t.confetti = e.exports)
|
||||||
})(window, {})
|
})(window, {})
|
||||||
|
|||||||
+121
-149
@@ -5,7 +5,6 @@ window.WalletPageLogic = {
|
|||||||
origin: window.location.origin,
|
origin: window.location.origin,
|
||||||
baseUrl: `${window.location.protocol}//${window.location.host}/`,
|
baseUrl: `${window.location.protocol}//${window.location.host}/`,
|
||||||
websocketUrl: `${'http:' ? 'ws://' : 'wss://'}${window.location.host}/api/v1/ws`,
|
websocketUrl: `${'http:' ? 'ws://' : 'wss://'}${window.location.host}/api/v1/ws`,
|
||||||
stored_paylinks: [],
|
|
||||||
parse: {
|
parse: {
|
||||||
show: false,
|
show: false,
|
||||||
invoice: null,
|
invoice: null,
|
||||||
@@ -41,10 +40,10 @@ window.WalletPageLogic = {
|
|||||||
data: {
|
data: {
|
||||||
amount: null,
|
amount: null,
|
||||||
memo: '',
|
memo: '',
|
||||||
internalMemo: null,
|
internalMemo: null
|
||||||
payment_hash: null
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
invoiceQrCode: '',
|
||||||
disclaimerDialog: {
|
disclaimerDialog: {
|
||||||
show: false,
|
show: false,
|
||||||
location: window.location
|
location: window.location
|
||||||
@@ -141,13 +140,6 @@ window.WalletPageLogic = {
|
|||||||
},
|
},
|
||||||
canPay() {
|
canPay() {
|
||||||
if (!this.parse.invoice) return false
|
if (!this.parse.invoice) return false
|
||||||
if (this.parse.invoice.expired) {
|
|
||||||
Quasar.Notify.create({
|
|
||||||
message: 'Invoice has expired',
|
|
||||||
color: 'negative'
|
|
||||||
})
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return this.parse.invoice.sat <= this.g.wallet.sat
|
return this.parse.invoice.sat <= this.g.wallet.sat
|
||||||
},
|
},
|
||||||
formattedAmount() {
|
formattedAmount() {
|
||||||
@@ -177,10 +169,6 @@ window.WalletPageLogic = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
dateFromNow(unix) {
|
|
||||||
const date = new Date(unix * 1000)
|
|
||||||
return moment.utc(date).fromNow()
|
|
||||||
},
|
|
||||||
formatFiatAmount(amount, currency) {
|
formatFiatAmount(amount, currency) {
|
||||||
this.update.currency = currency
|
this.update.currency = currency
|
||||||
this.formattedFiatAmount = LNbits.utils.formatCurrency(
|
this.formattedFiatAmount = LNbits.utils.formatCurrency(
|
||||||
@@ -212,7 +200,6 @@ window.WalletPageLogic = {
|
|||||||
this.receive.data.amount = null
|
this.receive.data.amount = null
|
||||||
this.receive.data.memo = null
|
this.receive.data.memo = null
|
||||||
this.receive.data.internalMemo = null
|
this.receive.data.internalMemo = null
|
||||||
this.receive.data.payment_hash = null
|
|
||||||
this.receive.unit = this.isFiatPriority
|
this.receive.unit = this.isFiatPriority
|
||||||
? this.g.wallet.currency || 'sat'
|
? this.g.wallet.currency || 'sat'
|
||||||
: 'sat'
|
: 'sat'
|
||||||
@@ -269,10 +256,9 @@ window.WalletPageLogic = {
|
|||||||
this.receive.data.amount,
|
this.receive.data.amount,
|
||||||
this.receive.data.memo,
|
this.receive.data.memo,
|
||||||
this.receive.unit,
|
this.receive.unit,
|
||||||
this.receive.lnurlWithdraw,
|
this.receive.lnurl && this.receive.lnurl.callback,
|
||||||
this.receive.fiatProvider,
|
this.receive.fiatProvider,
|
||||||
this.receive.data.internalMemo,
|
this.receive.data.internalMemo
|
||||||
this.receive.data.payment_hash
|
|
||||||
)
|
)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
this.g.updatePayments = !this.g.updatePayments
|
this.g.updatePayments = !this.g.updatePayments
|
||||||
@@ -285,33 +271,38 @@ window.WalletPageLogic = {
|
|||||||
if (!this.receive.lnurl) {
|
if (!this.receive.lnurl) {
|
||||||
this.readNfcTag()
|
this.readNfcTag()
|
||||||
}
|
}
|
||||||
|
// TODO: lnurl_callback and lnurl_response
|
||||||
// WITHDRAW
|
// WITHDRAW
|
||||||
if (
|
if (response.data.lnurl_response !== null) {
|
||||||
this.receive.lnurl &&
|
if (response.data.lnurl_response === false) {
|
||||||
response.data.extra?.lnurl_response !== null
|
response.data.lnurl_response = `Unable to connect`
|
||||||
) {
|
|
||||||
if (response.data.extra.lnurl_response === false) {
|
|
||||||
response.data.extra.lnurl_response = `Unable to connect`
|
|
||||||
}
|
}
|
||||||
const domain = this.receive.lnurl.callback.split('/')[2]
|
|
||||||
if (typeof response.data.extra.lnurl_response === 'string') {
|
if (typeof response.data.lnurl_response === 'string') {
|
||||||
// failure
|
// failure
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: `${domain} lnurl-withdraw call failed.`,
|
message: `${this.receive.lnurl.domain} lnurl-withdraw call failed.`,
|
||||||
caption: response.data.extra.lnurl_response
|
caption: response.data.lnurl_response
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
} else if (response.data.extra.lnurl_response === true) {
|
} else if (response.data.lnurl_response === true) {
|
||||||
// success
|
// success
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
timeout: 3000,
|
timeout: 5000,
|
||||||
message: `Invoice sent to ${domain}!`,
|
message: `Invoice sent to ${this.receive.lnurl.domain}!`,
|
||||||
spinner: true
|
spinner: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 => {
|
.catch(err => {
|
||||||
LNbits.utils.notifyApiError(err)
|
LNbits.utils.notifyApiError(err)
|
||||||
@@ -349,30 +340,32 @@ window.WalletPageLogic = {
|
|||||||
},
|
},
|
||||||
lnurlScan() {
|
lnurlScan() {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request('POST', '/api/v1/lnurlscan', this.g.wallet.adminkey, {
|
.request(
|
||||||
lnurl: this.parse.data.request
|
'GET',
|
||||||
})
|
'/api/v1/lnurlscan/' + this.parse.data.request,
|
||||||
|
this.g.wallet.adminkey
|
||||||
|
)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
const data = response.data
|
const data = response.data
|
||||||
|
|
||||||
if (data.status === 'ERROR') {
|
if (data.status === 'ERROR') {
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: `lnurl scan failed.`,
|
message: `${data.domain} lnurl call failed.`,
|
||||||
caption: data.reason
|
caption: data.reason
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.tag === 'payRequest') {
|
if (data.kind === 'pay') {
|
||||||
this.parse.lnurlpay = Object.freeze(data)
|
this.parse.lnurlpay = Object.freeze(data)
|
||||||
this.parse.data.amount = data.minSendable / 1000
|
this.parse.data.amount = data.minSendable / 1000
|
||||||
} else if (data.tag === 'login') {
|
} else if (data.kind === 'auth') {
|
||||||
this.parse.lnurlauth = Object.freeze(data)
|
this.parse.lnurlauth = Object.freeze(data)
|
||||||
} else if (data.tag === 'withdrawRequest') {
|
} else if (data.kind === 'withdraw') {
|
||||||
this.parse.show = false
|
this.parse.show = false
|
||||||
this.receive.show = true
|
this.receive.show = true
|
||||||
this.receive.lnurlWithdraw = Object.freeze(data)
|
|
||||||
this.receive.status = 'pending'
|
this.receive.status = 'pending'
|
||||||
this.receive.paymentReq = null
|
this.receive.paymentReq = null
|
||||||
this.receive.paymentHash = null
|
this.receive.paymentHash = null
|
||||||
@@ -382,9 +375,8 @@ window.WalletPageLogic = {
|
|||||||
data.minWithdrawable / 1000,
|
data.minWithdrawable / 1000,
|
||||||
data.maxWithdrawable / 1000
|
data.maxWithdrawable / 1000
|
||||||
]
|
]
|
||||||
const domain = data.callback.split('/')[2]
|
|
||||||
this.receive.lnurl = {
|
this.receive.lnurl = {
|
||||||
domain: domain,
|
domain: data.domain,
|
||||||
callback: data.callback,
|
callback: data.callback,
|
||||||
fixed: data.fixed
|
fixed: data.fixed
|
||||||
}
|
}
|
||||||
@@ -399,29 +391,19 @@ window.WalletPageLogic = {
|
|||||||
this.decodeRequest()
|
this.decodeRequest()
|
||||||
this.parse.camera.show = false
|
this.parse.camera.show = false
|
||||||
},
|
},
|
||||||
isLnurl(req) {
|
|
||||||
return (
|
|
||||||
req.toLowerCase().startsWith('lnurl1') ||
|
|
||||||
req.startsWith('lnurlp://') ||
|
|
||||||
req.startsWith('lnurlw://') ||
|
|
||||||
req.startsWith('lnurlauth://') ||
|
|
||||||
req.match(/[\w.+-~_]+@[\w.+-~_]/)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
decodeRequest() {
|
decodeRequest() {
|
||||||
this.parse.show = true
|
this.parse.show = true
|
||||||
this.parse.data.request = this.parse.data.request.trim()
|
this.parse.data.request = this.parse.data.request.trim().toLowerCase()
|
||||||
const req = this.parse.data.request.toLowerCase()
|
let req = this.parse.data.request
|
||||||
if (req.startsWith('lightning:')) {
|
if (req.startsWith('lightning:')) {
|
||||||
this.parse.data.request = this.parse.data.request.slice(10)
|
this.parse.data.request = req.slice(10)
|
||||||
} else if (req.startsWith('lnurl:')) {
|
} else if (req.startsWith('lnurl:')) {
|
||||||
this.parse.data.request = this.parse.data.request.slice(6)
|
this.parse.data.request = req.slice(6)
|
||||||
} else if (req.includes('lightning=lnurl1')) {
|
} else if (req.includes('lightning=lnurl1')) {
|
||||||
this.parse.data.request = this.parse.data.request
|
this.parse.data.request = req.split('lightning=')[1].split('&')[0]
|
||||||
.split('lightning=')[1]
|
|
||||||
.split('&')[0]
|
|
||||||
}
|
}
|
||||||
if (this.isLnurl(this.parse.data.request)) {
|
req = this.parse.data.request
|
||||||
|
if (req.startsWith('lnurl1') || req.match(/[\w.+-~_]+@[\w.+-~_]/)) {
|
||||||
this.lnurlScan()
|
this.lnurlScan()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -530,65 +512,96 @@ window.WalletPageLogic = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
payLnurl() {
|
payLnurl() {
|
||||||
|
const dismissPaymentMsg = Quasar.Notify.create({
|
||||||
|
timeout: 0,
|
||||||
|
message: 'Processing payment...'
|
||||||
|
})
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request('post', '/api/v1/payments/lnurl', this.g.wallet.adminkey, {
|
.payLnurl(
|
||||||
res: this.parse.lnurlpay,
|
this.g.wallet,
|
||||||
lnurl: this.parse.data.request,
|
this.parse.lnurlpay.callback,
|
||||||
unit: this.parse.data.unit,
|
this.parse.lnurlpay.description_hash,
|
||||||
amount: this.parse.data.amount * 1000,
|
this.parse.data.amount * 1000,
|
||||||
comment: this.parse.data.comment,
|
this.parse.lnurlpay.description.slice(0, 120),
|
||||||
internalMemo: this.parse.data.internalMemo
|
this.parse.data.comment,
|
||||||
})
|
this.parse.data.unit,
|
||||||
|
this.parse.data.internalMemo
|
||||||
|
)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
this.parse.show = false
|
this.parse.show = false
|
||||||
if (response.data.extra.success_action) {
|
|
||||||
const action = JSON.parse(response.data.extra.success_action)
|
clearInterval(this.parse.paymentChecker)
|
||||||
switch (action.tag) {
|
setTimeout(() => {
|
||||||
case 'url':
|
clearInterval(this.parse.paymentChecker)
|
||||||
Quasar.Notify.create({
|
}, 40000)
|
||||||
message: `<a target="_blank" style="color: inherit" href="${action.url}">${action.url}</a>`,
|
this.parse.paymentChecker = setInterval(() => {
|
||||||
caption: action.description,
|
LNbits.api
|
||||||
html: true,
|
.getPayment(this.g.wallet, response.data.payment_hash)
|
||||||
type: 'positive',
|
.then(res => {
|
||||||
timeout: 0,
|
if (res.data.paid) {
|
||||||
closeBtn: true
|
dismissPaymentMsg()
|
||||||
})
|
clearInterval(this.parse.paymentChecker)
|
||||||
break
|
// show lnurlpay success action
|
||||||
case 'message':
|
const extra = response.data.extra
|
||||||
Quasar.Notify.create({
|
if (extra.success_action) {
|
||||||
message: action.message,
|
switch (extra.success_action.tag) {
|
||||||
type: 'positive',
|
case 'url':
|
||||||
timeout: 0,
|
Quasar.Notify.create({
|
||||||
closeBtn: true
|
message: `<a target="_blank" style="color: inherit" href="${extra.success_action.url}">${extra.success_action.url}</a>`,
|
||||||
})
|
caption: extra.success_action.description,
|
||||||
break
|
html: true,
|
||||||
case 'aes':
|
type: 'positive',
|
||||||
decryptLnurlPayAES(action, response.data.preimage)
|
timeout: 0,
|
||||||
Quasar.Notify.create({
|
closeBtn: true
|
||||||
message: value,
|
})
|
||||||
caption: extra.success_action.description,
|
break
|
||||||
html: true,
|
case 'message':
|
||||||
type: 'positive',
|
Quasar.Notify.create({
|
||||||
timeout: 0,
|
message: extra.success_action.message,
|
||||||
closeBtn: true
|
type: 'positive',
|
||||||
})
|
timeout: 0,
|
||||||
}
|
closeBtn: true
|
||||||
}
|
})
|
||||||
|
break
|
||||||
|
case 'aes':
|
||||||
|
LNbits.api
|
||||||
|
.getPayment(this.g.wallet, response.data.payment_hash)
|
||||||
|
.then(({data: payment}) =>
|
||||||
|
decryptLnurlPayAES(
|
||||||
|
extra.success_action,
|
||||||
|
payment.preimage
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.then(value => {
|
||||||
|
Quasar.Notify.create({
|
||||||
|
message: value,
|
||||||
|
caption: extra.success_action.description,
|
||||||
|
html: true,
|
||||||
|
type: 'positive',
|
||||||
|
timeout: 0,
|
||||||
|
closeBtn: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, 2000)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
dismissPaymentMsg()
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
})
|
})
|
||||||
.catch(LNbits.utils.notifyApiError)
|
|
||||||
},
|
},
|
||||||
authLnurl() {
|
authLnurl() {
|
||||||
const dismissAuthMsg = Quasar.Notify.create({
|
const dismissAuthMsg = Quasar.Notify.create({
|
||||||
timeout: 10,
|
timeout: 10,
|
||||||
message: 'Performing authentication...'
|
message: 'Performing authentication...'
|
||||||
})
|
})
|
||||||
|
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request(
|
.authLnurl(this.g.wallet, this.parse.lnurlauth.callback)
|
||||||
'post',
|
|
||||||
'/api/v1/lnurlauth',
|
|
||||||
wallet.adminkey,
|
|
||||||
this.parse.lnurlauth
|
|
||||||
)
|
|
||||||
.then(_ => {
|
.then(_ => {
|
||||||
dismissAuthMsg()
|
dismissAuthMsg()
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
@@ -599,9 +612,10 @@ window.WalletPageLogic = {
|
|||||||
this.parse.show = false
|
this.parse.show = false
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
|
dismissAuthMsg()
|
||||||
if (err.response.data.reason) {
|
if (err.response.data.reason) {
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
message: `Authentication failed. ${this.parse.lnurlauth.callback} says:`,
|
message: `Authentication failed. ${this.parse.lnurlauth.domain} says:`,
|
||||||
caption: err.response.data.reason,
|
caption: err.response.data.reason,
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
timeout: 5000
|
timeout: 5000
|
||||||
@@ -1097,51 +1111,9 @@ window.WalletPageLogic = {
|
|||||||
saveChartsPreferences() {
|
saveChartsPreferences() {
|
||||||
this.$q.localStorage.set('lnbits.wallets.chartConfig', this.chartConfig)
|
this.$q.localStorage.set('lnbits.wallets.chartConfig', this.chartConfig)
|
||||||
this.refreshCharts()
|
this.refreshCharts()
|
||||||
},
|
|
||||||
updatePaylinks() {
|
|
||||||
LNbits.api
|
|
||||||
.request(
|
|
||||||
'PUT',
|
|
||||||
`/api/v1/wallet/stored_paylinks/${this.g.wallet.id}`,
|
|
||||||
this.g.wallet.adminkey,
|
|
||||||
{
|
|
||||||
links: this.stored_paylinks
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
Quasar.Notify.create({
|
|
||||||
message: 'Paylinks updated.',
|
|
||||||
type: 'positive',
|
|
||||||
timeout: 3500
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
LNbits.utils.notifyApiError(err)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
sendToPaylink(lnurl) {
|
|
||||||
this.parse.data.request = lnurl
|
|
||||||
this.parse.show = true
|
|
||||||
this.lnurlScan()
|
|
||||||
},
|
|
||||||
editPaylink() {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.updatePaylinks()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
deletePaylink(lnurl) {
|
|
||||||
const links = []
|
|
||||||
this.stored_paylinks.forEach(link => {
|
|
||||||
if (link.lnurl !== lnurl) {
|
|
||||||
links.push(link)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.stored_paylinks = links
|
|
||||||
this.updatePaylinks()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.stored_paylinks = wallet.stored_paylinks.links
|
|
||||||
const urlParams = new URLSearchParams(window.location.search)
|
const urlParams = new URLSearchParams(window.location.search)
|
||||||
if (urlParams.has('lightning') || urlParams.has('lnurl')) {
|
if (urlParams.has('lightning') || urlParams.has('lnurl')) {
|
||||||
this.parse.data.request =
|
this.parse.data.request =
|
||||||
|
|||||||
@@ -18,16 +18,16 @@ $themes: (
|
|||||||
'freedom': (
|
'freedom': (
|
||||||
primary: #e22156,
|
primary: #e22156,
|
||||||
secondary: #b91a45,
|
secondary: #b91a45,
|
||||||
dark: #462f36,
|
dark: #0a0a0a,
|
||||||
info: #47393d,
|
info: #1b1b1b,
|
||||||
marginal-bg: #2d293b,
|
marginal-bg: #2d293b,
|
||||||
marginal-text: #fff
|
marginal-text: #fff
|
||||||
),
|
),
|
||||||
'cyber': (
|
'cyber': (
|
||||||
primary: #7cb342,
|
primary: #7cb342,
|
||||||
secondary: #558b2f,
|
secondary: #558b2f,
|
||||||
dark: #000,
|
dark: #0a0a0a,
|
||||||
info: #1f2915,
|
info: #1b1b1b,
|
||||||
marginal-bg: #2d293b,
|
marginal-bg: #2d293b,
|
||||||
marginal-text: #fff
|
marginal-text: #fff
|
||||||
),
|
),
|
||||||
@@ -62,13 +62,5 @@ $themes: (
|
|||||||
info: rgb(39, 39, 39),
|
info: rgb(39, 39, 39),
|
||||||
marginal-bg: #000,
|
marginal-bg: #000,
|
||||||
marginal-text: rgb(255, 255, 255)
|
marginal-text: rgb(255, 255, 255)
|
||||||
),
|
|
||||||
'salvador': (
|
|
||||||
primary: #1976d2,
|
|
||||||
secondary: #26a69a,
|
|
||||||
dark: #253647,
|
|
||||||
info: #343d47,
|
|
||||||
marginal-bg: #0d1620,
|
|
||||||
marginal-text: rgb(255, 255, 255)
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -38,8 +38,6 @@
|
|||||||
"js/bolt11-decoder.js"
|
"js/bolt11-decoder.js"
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
"js/components/lnbits-qrcode.js",
|
|
||||||
"js/components/lnbits-qrcode-lnurl.js",
|
|
||||||
"js/components/lnbits-funding-sources.js",
|
"js/components/lnbits-funding-sources.js",
|
||||||
"js/components/extension-settings.js",
|
"js/components/extension-settings.js",
|
||||||
"js/components/payment-list.js",
|
"js/components/payment-list.js",
|
||||||
|
|||||||
Vendored
+308
-430
File diff suppressed because it is too large
Load Diff
+2
-2
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user