Compare commits
83
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86a1313258 | ||
|
|
0fa8fca70e | ||
|
|
b33518f075 | ||
|
|
3dce95968e | ||
|
|
7c92788cd8 | ||
|
|
2e31d1fd91 | ||
|
|
6c353f366d | ||
|
|
902f3c7c6f | ||
|
|
7fbffa20d7 | ||
|
|
0185f1e465 | ||
|
|
42b3616b03 | ||
|
|
a446cd6c5b | ||
|
|
3c0c1ca930 | ||
|
|
1a4c706ed4 | ||
|
|
8969c0235d | ||
|
|
34ee5b273a | ||
|
|
c7c1af9465 | ||
|
|
f7ed194e02 | ||
|
|
fd4b6af1de | ||
|
|
8d1542d982 | ||
|
|
342bab5714 | ||
|
|
3d9a882066 | ||
|
|
146a74761a | ||
|
|
85b2f30a1a | ||
|
|
07eb0ecdde | ||
|
|
ea0e1e032e | ||
|
|
b23e8ea781 | ||
|
|
fa61016ed1 | ||
|
|
6f44b755e4 | ||
|
|
31664a4ba0 | ||
|
|
f985a1eb08 | ||
|
|
4e5b001901 | ||
|
|
3a2cf12052 | ||
|
|
f17ff3d12b | ||
|
|
32c042741d | ||
|
|
5997fd74fa | ||
|
|
fb5659de2a | ||
|
|
e13fd39e75 | ||
|
|
acd3b598b7 | ||
|
|
1a6671b4b3 | ||
|
|
7fc723c2dd | ||
|
|
468a786f78 | ||
|
|
f74a7cbe1f | ||
|
|
ce9b63c54e | ||
|
|
3dd1c6992b | ||
|
|
1c49430ece | ||
|
|
b14b3bbaad | ||
|
|
020f6b6b1b | ||
|
|
0314453100 | ||
|
|
37dca56ccc | ||
|
|
d234ab12d8 | ||
|
|
37187bfc2c | ||
|
|
368da935db | ||
|
|
81aef06f75 | ||
|
|
27241e01f3 | ||
|
|
3ff85363f2 | ||
|
|
b81b30c896 | ||
|
|
25d59e4142 | ||
|
|
5f8ccee5b8 | ||
|
|
740180d332 | ||
|
|
b66a8b3de9 | ||
|
|
b3351f2b17 | ||
|
|
36dbdaa159 | ||
|
|
524a4c9213 | ||
|
|
200b9b127c | ||
|
|
646c341840 | ||
|
|
826f8ad2ac | ||
|
|
1b9bb32ca0 | ||
|
|
61c71660b8 | ||
|
|
80ac158ca0 | ||
|
|
291c69e470 | ||
|
|
8ff4962e86 | ||
|
|
57e9d2271d | ||
|
|
c18816bf1b | ||
|
|
8f290a647d | ||
|
|
d1cc317044 | ||
|
|
5a1d660c7b | ||
|
|
7ee78248b7 | ||
|
|
5104cbb285 | ||
|
|
fa71219ce7 | ||
|
|
f5f43e7668 | ||
|
|
4b63d0924e | ||
|
|
dc22088dcd |
@@ -4,7 +4,7 @@ inputs:
|
|||||||
python-version:
|
python-version:
|
||||||
description: "Python Version"
|
description: "Python Version"
|
||||||
required: true
|
required: true
|
||||||
default: "3.9"
|
default: "3.10"
|
||||||
poetry-version:
|
poetry-version:
|
||||||
description: "Poetry Version"
|
description: "Poetry Version"
|
||||||
default: "1.7.0"
|
default: "1.7.0"
|
||||||
@@ -47,6 +47,7 @@ runs:
|
|||||||
- name: Install the project dependencies
|
- name: Install the project dependencies
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
|
poetry env use python${{ inputs.python-version }}
|
||||||
poetry install
|
poetry install
|
||||||
# needed for conv tests
|
# needed for conv tests
|
||||||
poetry add psycopg2-binary
|
poetry add psycopg2-binary
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
needs: [ lint ]
|
needs: [ lint ]
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.9", "3.10"]
|
python-version: ["3.10", "3.11", "3.12"]
|
||||||
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:
|
||||||
@@ -30,7 +30,7 @@ jobs:
|
|||||||
needs: [ lint ]
|
needs: [ lint ]
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.9", "3.10"]
|
python-version: ["3.10", "3.11", "3.12"]
|
||||||
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:
|
||||||
@@ -44,7 +44,7 @@ jobs:
|
|||||||
needs: [ lint ]
|
needs: [ lint ]
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.9", "3.10"]
|
python-version: ["3.10", "3.11", "3.12"]
|
||||||
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:
|
||||||
@@ -58,7 +58,7 @@ jobs:
|
|||||||
needs: [ lint ]
|
needs: [ lint ]
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.9", "3.10"]
|
python-version: ["3.10", "3.11", "3.12"]
|
||||||
uses: ./.github/workflows/migration.yml
|
uses: ./.github/workflows/migration.yml
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
@@ -74,7 +74,7 @@ jobs:
|
|||||||
uses: ./.github/workflows/regtest.yml
|
uses: ./.github/workflows/regtest.yml
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.9"]
|
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: "poetry run pytest tests/regtest"
|
custom-pytest: "poetry run pytest tests/regtest"
|
||||||
@@ -87,7 +87,7 @@ jobs:
|
|||||||
needs: [ lint ]
|
needs: [ lint ]
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.9"]
|
python-version: ["3.10"]
|
||||||
poetry-version: ["1.5.1"]
|
poetry-version: ["1.5.1"]
|
||||||
uses: ./.github/workflows/jmeter.yml
|
uses: ./.github/workflows/jmeter.yml
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
analyze:
|
analyze:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
push_to_dockerhub:
|
push_to_dockerhub:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ on:
|
|||||||
python-version:
|
python-version:
|
||||||
description: "Python Version"
|
description: "Python Version"
|
||||||
required: true
|
required: true
|
||||||
default: "3.9"
|
default: "3.10"
|
||||||
type: string
|
type: string
|
||||||
poetry-version:
|
poetry-version:
|
||||||
description: "Poetry Version"
|
description: "Poetry Version"
|
||||||
@@ -16,7 +16,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
jmeter:
|
jmeter:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
@@ -31,9 +31,18 @@ jobs:
|
|||||||
LNBITS_BACKEND_WALLET_CLASS: FakeWallet
|
LNBITS_BACKEND_WALLET_CLASS: FakeWallet
|
||||||
run: |
|
run: |
|
||||||
poetry run lnbits &
|
poetry run lnbits &
|
||||||
sleep 5
|
sleep 10
|
||||||
|
|
||||||
|
- name: setup java version
|
||||||
|
run: |
|
||||||
|
update-java-alternatives --list
|
||||||
|
sudo update-java-alternatives --set /usr/lib/jvm/temurin-8-jdk-amd64
|
||||||
|
java -version
|
||||||
|
|
||||||
- name: clone lnbits-extensions, install jmeter and run tests
|
- name: clone lnbits-extensions, install jmeter and run tests
|
||||||
|
env:
|
||||||
|
JAVA_HOME: /usr/lib/jvm/temurin-8-jdk-amd64
|
||||||
|
EXTENSIONS_MANIFEST_PATH: "/lnbits/lnbits-extensions/refs/heads/main/extensions.json"
|
||||||
run: |
|
run: |
|
||||||
git clone https://github.com/lnbits/lnbits-extensions
|
git clone https://github.com/lnbits/lnbits-extensions
|
||||||
cd lnbits-extensions
|
cd lnbits-extensions
|
||||||
@@ -43,6 +52,13 @@ jobs:
|
|||||||
make start-mirror-server
|
make start-mirror-server
|
||||||
make test
|
make test
|
||||||
|
|
||||||
|
- name: print lnbits log
|
||||||
|
if: ${{ always() }}
|
||||||
|
run: |
|
||||||
|
# catch up time for lnbits
|
||||||
|
sleep 1
|
||||||
|
cat data/logs/debug.log
|
||||||
|
|
||||||
- name: upload jmeter test results
|
- name: upload jmeter test results
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ jobs:
|
|||||||
uses: ./.github/workflows/make.yml
|
uses: ./.github/workflows/make.yml
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.9", "3.10"]
|
python-version: ["3.10", "3.11", "3.12"]
|
||||||
with:
|
with:
|
||||||
make: checkblack
|
make: checkblack
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
uses: ./.github/workflows/make.yml
|
uses: ./.github/workflows/make.yml
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.9", "3.10"]
|
python-version: ["3.10", "3.11", "3.12"]
|
||||||
with:
|
with:
|
||||||
make: checkruff
|
make: checkruff
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
uses: ./.github/workflows/make.yml
|
uses: ./.github/workflows/make.yml
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.9", "3.10"]
|
python-version: ["3.10", "3.11", "3.12"]
|
||||||
with:
|
with:
|
||||||
make: mypy
|
make: mypy
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
uses: ./.github/workflows/make.yml
|
uses: ./.github/workflows/make.yml
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.9", "3.10"]
|
python-version: ["3.10", "3.11", "3.12"]
|
||||||
with:
|
with:
|
||||||
make: pyright
|
make: pyright
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
name: ${{ inputs.make }} (${{ inputs.python-version }})
|
name: ${{ inputs.make }} (${{ inputs.python-version }})
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os-version: ["ubuntu-latest"]
|
os-version: ["ubuntu-24.04"]
|
||||||
node-version: ["18.x"]
|
node-version: ["18.x"]
|
||||||
runs-on: ${{ matrix.os-version }}
|
runs-on: ${{ matrix.os-version }}
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ jobs:
|
|||||||
name: migration (${{ inputs.python-version }})
|
name: migration (${{ inputs.python-version }})
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os-version: ["ubuntu-latest"]
|
os-version: ["ubuntu-24.04"]
|
||||||
runs-on: ${{ matrix.os-version }}
|
runs-on: ${{ matrix.os-version }}
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
nix:
|
nix:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: cachix/install-nix-action@v27
|
- uses: cachix/install-nix-action@v27
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ on:
|
|||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
python-version:
|
python-version:
|
||||||
default: "3.9"
|
default: "3.10"
|
||||||
type: string
|
type: string
|
||||||
os-version:
|
os-version:
|
||||||
default: "ubuntu-latest"
|
default: "ubuntu-24.04"
|
||||||
type: string
|
type: string
|
||||||
backend-wallet-class:
|
backend-wallet-class:
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
pypi:
|
pypi:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Install dependencies for building secp256k1
|
- name: Install dependencies for building secp256k1
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Create github release
|
- name: Create github release
|
||||||
@@ -41,7 +41,7 @@ jobs:
|
|||||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
pypi:
|
pypi:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Install dependencies for building secp256k1
|
- name: Install dependencies for building secp256k1
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ on:
|
|||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
python-version:
|
python-version:
|
||||||
default: "3.9"
|
default: "3.10"
|
||||||
type: string
|
type: string
|
||||||
os-version:
|
os-version:
|
||||||
default: "ubuntu-latest"
|
default: "ubuntu-24.04"
|
||||||
type: string
|
type: string
|
||||||
db-url:
|
db-url:
|
||||||
default: ""
|
default: ""
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3.10-slim-bookworm AS builder
|
FROM python:3.12-slim-bookworm AS builder
|
||||||
|
|
||||||
RUN apt-get clean
|
RUN apt-get clean
|
||||||
RUN apt-get update
|
RUN apt-get update
|
||||||
@@ -21,7 +21,7 @@ ENV POETRY_NO_INTERACTION=1 \
|
|||||||
|
|
||||||
RUN poetry install --only main
|
RUN poetry install --only main
|
||||||
|
|
||||||
FROM python:3.10-slim-bookworm
|
FROM python:3.12-slim-bookworm
|
||||||
|
|
||||||
# needed for backups postgresql-client version 14 (pg_dump)
|
# needed for backups postgresql-client version 14 (pg_dump)
|
||||||
RUN apt-get update && apt-get -y upgrade && \
|
RUN apt-get update && apt-get -y upgrade && \
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ You can access your super user account at `/wallet?usr=super_user_id`. You just
|
|||||||
|
|
||||||
After that you will find the **`Admin` / `Manage Server`** between `Wallets` and `Extensions`
|
After that you will find the **`Admin` / `Manage Server`** between `Wallets` and `Extensions`
|
||||||
|
|
||||||
Here you can design the interface, it has TOPUP to fill wallets and you can restrict access rights to extensions only for admins or generally deactivated for everyone. You can make users admins or set up Allowed Users if you want to restrict access. And of course the classic settings of the .env file, e.g. to change the funding source wallet or set a charge fee.
|
Here you can design the interface, it has credit/debit to change wallets balances and you can restrict access rights to extensions only for admins or generally deactivated for everyone. You can make users admins or set up Allowed Users if you want to restrict access. And of course the classic settings of the .env file, e.g. to change the funding source wallet or set a charge fee.
|
||||||
|
|
||||||
Do not forget
|
Do not forget
|
||||||
|
|
||||||
|
|||||||
+18
-16
@@ -12,11 +12,15 @@ Note that by default LNbits uses SQLite as its database, which is simple and eff
|
|||||||
|
|
||||||
## Option 1 (recommended): Poetry
|
## Option 1 (recommended): Poetry
|
||||||
|
|
||||||
It is recommended to use the latest version of Poetry. Make sure you have Python version 3.9 or higher installed.
|
It is recommended to use the latest version of Poetry. Make sure you have Python version `3.12` installed.
|
||||||
|
|
||||||
### Verify Python version
|
### Install Python 3.12
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
sudo add-apt-repository ppa:deadsnakes/ppa
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install python3.12
|
||||||
|
sudo apt-get install python3.12-dev # ensure correct headers needed for secp256k1
|
||||||
python3 --version
|
python3 --version
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -28,16 +32,16 @@ curl -sSL https://install.python-poetry.org | python3 -
|
|||||||
export PATH="/home/user/.local/bin:$PATH"
|
export PATH="/home/user/.local/bin:$PATH"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### install LNbits
|
||||||
|
|
||||||
```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
|
||||||
|
|
||||||
poetry install --only main
|
poetry install --only main
|
||||||
|
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
# set funding source amongst other options
|
# Optional: to set funding source amongst other options via the env `nano .env`
|
||||||
nano .env
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Running the server
|
#### Running the server
|
||||||
@@ -49,9 +53,16 @@ poetry run lnbits
|
|||||||
# 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.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### LNbits-cli
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# A very useful terminal client for getting the supersuer ID, updating extensions, etc
|
||||||
|
poetry run lnbits-cli --help
|
||||||
|
```
|
||||||
|
|
||||||
#### Updating the server
|
#### Updating the server
|
||||||
|
|
||||||
```
|
```sh
|
||||||
cd lnbits
|
cd lnbits
|
||||||
# Stop LNbits with `ctrl + x`
|
# Stop LNbits with `ctrl + x`
|
||||||
git pull
|
git pull
|
||||||
@@ -348,15 +359,6 @@ Assuming your LNbits is running on port `5000` add:
|
|||||||
|
|
||||||
```
|
```
|
||||||
yourdomain.com {
|
yourdomain.com {
|
||||||
handle /api/v1/payments/sse* {
|
|
||||||
reverse_proxy 0.0.0.0:5000 {
|
|
||||||
header_up X-Forwarded-Host yourdomain.com
|
|
||||||
transport http {
|
|
||||||
keepalive off
|
|
||||||
compression off
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reverse_proxy 0.0.0.0:5000 {
|
reverse_proxy 0.0.0.0:5000 {
|
||||||
header_up X-Forwarded-Host yourdomain.com
|
header_up X-Forwarded-Host yourdomain.com
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-17
@@ -14,6 +14,7 @@ from fastapi.staticfiles import StaticFiles
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
from slowapi import Limiter
|
from slowapi import Limiter
|
||||||
from slowapi.util import get_remote_address
|
from slowapi.util import get_remote_address
|
||||||
|
from starlette.middleware.gzip import GZipMiddleware
|
||||||
from starlette.middleware.sessions import SessionMiddleware
|
from starlette.middleware.sessions import SessionMiddleware
|
||||||
|
|
||||||
from lnbits.core.crud import (
|
from lnbits.core.crud import (
|
||||||
@@ -26,6 +27,7 @@ from lnbits.core.helpers import migrate_extension_database
|
|||||||
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.tasks import ( # watchdog_task
|
from lnbits.core.tasks import ( # watchdog_task
|
||||||
audit_queue,
|
audit_queue,
|
||||||
|
collect_exchange_rates_data,
|
||||||
killswitch_task,
|
killswitch_task,
|
||||||
purge_audit_data,
|
purge_audit_data,
|
||||||
wait_for_audit_data,
|
wait_for_audit_data,
|
||||||
@@ -54,7 +56,6 @@ from .core.models.extensions import Extension, ExtensionMeta, InstallableExtensi
|
|||||||
from .core.services import check_admin_settings, check_webpush_settings
|
from .core.services import check_admin_settings, check_webpush_settings
|
||||||
from .middleware import (
|
from .middleware import (
|
||||||
AuditMiddleware,
|
AuditMiddleware,
|
||||||
CustomGZipMiddleware,
|
|
||||||
ExtensionsRedirectMiddleware,
|
ExtensionsRedirectMiddleware,
|
||||||
InstalledExtensionMiddleware,
|
InstalledExtensionMiddleware,
|
||||||
add_first_install_middleware,
|
add_first_install_middleware,
|
||||||
@@ -64,7 +65,6 @@ from .middleware import (
|
|||||||
from .requestvars import g
|
from .requestvars import g
|
||||||
from .tasks import (
|
from .tasks import (
|
||||||
check_pending_payments,
|
check_pending_payments,
|
||||||
create_task,
|
|
||||||
internal_invoice_listener,
|
internal_invoice_listener,
|
||||||
invoice_listener,
|
invoice_listener,
|
||||||
)
|
)
|
||||||
@@ -80,6 +80,10 @@ async def startup(app: FastAPI):
|
|||||||
await check_admin_settings()
|
await check_admin_settings()
|
||||||
await check_webpush_settings()
|
await check_webpush_settings()
|
||||||
|
|
||||||
|
# check extensions after restart
|
||||||
|
if not settings.lnbits_extensions_deactivate_all:
|
||||||
|
await check_and_register_extensions(app)
|
||||||
|
|
||||||
log_server_info()
|
log_server_info()
|
||||||
|
|
||||||
# initialize WALLET
|
# initialize WALLET
|
||||||
@@ -96,7 +100,7 @@ async def startup(app: FastAPI):
|
|||||||
init_core_routers(app)
|
init_core_routers(app)
|
||||||
|
|
||||||
# initialize tasks
|
# initialize tasks
|
||||||
register_async_tasks(app)
|
register_async_tasks()
|
||||||
|
|
||||||
|
|
||||||
async def shutdown():
|
async def shutdown():
|
||||||
@@ -149,10 +153,7 @@ def create_app() -> FastAPI:
|
|||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]
|
CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]
|
||||||
)
|
)
|
||||||
|
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
||||||
app.add_middleware(
|
|
||||||
CustomGZipMiddleware, minimum_size=1000, exclude_paths=["/api/v1/payments/sse"]
|
|
||||||
)
|
|
||||||
|
|
||||||
app.add_middleware(AuditMiddleware, audit_queue=audit_queue)
|
app.add_middleware(AuditMiddleware, audit_queue=audit_queue)
|
||||||
|
|
||||||
@@ -394,16 +395,21 @@ def register_new_ratelimiter(app: FastAPI) -> Callable:
|
|||||||
return register_new_ratelimiter_fn
|
return register_new_ratelimiter_fn
|
||||||
|
|
||||||
|
|
||||||
|
def register_ext_tasks(ext: Extension) -> None:
|
||||||
|
"""Register extension async tasks."""
|
||||||
|
ext_module = importlib.import_module(ext.module_name)
|
||||||
|
|
||||||
|
if hasattr(ext_module, f"{ext.code}_start"):
|
||||||
|
ext_start_func = getattr(ext_module, f"{ext.code}_start")
|
||||||
|
ext_start_func()
|
||||||
|
|
||||||
|
|
||||||
def register_ext_routes(app: FastAPI, ext: Extension) -> None:
|
def register_ext_routes(app: FastAPI, ext: Extension) -> None:
|
||||||
"""Register FastAPI routes for extension."""
|
"""Register FastAPI routes for extension."""
|
||||||
ext_module = importlib.import_module(ext.module_name)
|
ext_module = importlib.import_module(ext.module_name)
|
||||||
|
|
||||||
ext_route = getattr(ext_module, f"{ext.code}_ext")
|
ext_route = getattr(ext_module, f"{ext.code}_ext")
|
||||||
|
|
||||||
if hasattr(ext_module, f"{ext.code}_start"):
|
|
||||||
ext_start_func = getattr(ext_module, f"{ext.code}_start")
|
|
||||||
ext_start_func()
|
|
||||||
|
|
||||||
if hasattr(ext_module, f"{ext.code}_static_files"):
|
if hasattr(ext_module, f"{ext.code}_static_files"):
|
||||||
ext_statics = getattr(ext_module, f"{ext.code}_static_files")
|
ext_statics = getattr(ext_module, f"{ext.code}_static_files")
|
||||||
for s in ext_statics:
|
for s in ext_statics:
|
||||||
@@ -430,15 +436,12 @@ async def check_and_register_extensions(app: FastAPI):
|
|||||||
for ext in await get_valid_extensions(False):
|
for ext in await get_valid_extensions(False):
|
||||||
try:
|
try:
|
||||||
register_ext_routes(app, ext)
|
register_ext_routes(app, ext)
|
||||||
|
register_ext_tasks(ext)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error(f"Could not load extension `{ext.code}`: {exc!s}")
|
logger.error(f"Could not load extension `{ext.code}`: {exc!s}")
|
||||||
|
|
||||||
|
|
||||||
def register_async_tasks(app: FastAPI):
|
def register_async_tasks():
|
||||||
|
|
||||||
# check extensions after restart
|
|
||||||
if not settings.lnbits_extensions_deactivate_all:
|
|
||||||
create_task(check_and_register_extensions(app))
|
|
||||||
|
|
||||||
create_permanent_task(wait_for_audit_data)
|
create_permanent_task(wait_for_audit_data)
|
||||||
create_permanent_task(check_pending_payments)
|
create_permanent_task(check_pending_payments)
|
||||||
@@ -447,7 +450,7 @@ def register_async_tasks(app: FastAPI):
|
|||||||
create_permanent_task(cache.invalidate_forever)
|
create_permanent_task(cache.invalidate_forever)
|
||||||
|
|
||||||
# core invoice listener
|
# core invoice listener
|
||||||
invoice_queue: asyncio.Queue = asyncio.Queue(5)
|
invoice_queue: asyncio.Queue = asyncio.Queue()
|
||||||
register_invoice_listener(invoice_queue, "core")
|
register_invoice_listener(invoice_queue, "core")
|
||||||
create_permanent_task(lambda: wait_for_paid_invoices(invoice_queue))
|
create_permanent_task(lambda: wait_for_paid_invoices(invoice_queue))
|
||||||
|
|
||||||
@@ -455,6 +458,7 @@ def register_async_tasks(app: FastAPI):
|
|||||||
# create_permanent_task(watchdog_task)
|
# create_permanent_task(watchdog_task)
|
||||||
create_permanent_task(killswitch_task)
|
create_permanent_task(killswitch_task)
|
||||||
create_permanent_task(purge_audit_data)
|
create_permanent_task(purge_audit_data)
|
||||||
|
create_permanent_task(collect_exchange_rates_data)
|
||||||
|
|
||||||
# server logs for websocket
|
# server logs for websocket
|
||||||
if settings.lnbits_admin_ui:
|
if settings.lnbits_admin_ui:
|
||||||
|
|||||||
+7
-1
@@ -1,5 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import importlib
|
import importlib
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -338,12 +339,17 @@ async def extensions_update(
|
|||||||
if not await _can_run_operation(url):
|
if not await _can_run_operation(url):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
upgrades_dir = Path(settings.lnbits_extensions_path, "upgrades")
|
||||||
|
Path(upgrades_dir).mkdir(parents=True, exist_ok=True)
|
||||||
|
sys.path.append(str(upgrades_dir))
|
||||||
|
|
||||||
if extension:
|
if extension:
|
||||||
await update_extension(extension, repo_index, source_repo, url, admin_user)
|
await update_extension(extension, repo_index, source_repo, url, admin_user)
|
||||||
return
|
return
|
||||||
|
|
||||||
click.echo("Updating all extensions...")
|
click.echo("Updating all extensions:")
|
||||||
installed_extensions = await get_installed_extensions()
|
installed_extensions = await get_installed_extensions()
|
||||||
|
click.echo(f" {[e.id for e in installed_extensions]}")
|
||||||
updated_extensions = []
|
updated_extensions = []
|
||||||
for e in installed_extensions:
|
for e in installed_extensions:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ from .views.extension_api import extension_router
|
|||||||
from .views.generic import generic_router
|
from .views.generic import generic_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.public_api import public_router
|
|
||||||
from .views.tinyurl_api import tinyurl_router
|
from .views.tinyurl_api import tinyurl_router
|
||||||
from .views.user_api import users_router
|
from .views.user_api import users_router
|
||||||
from .views.wallet_api import wallet_router
|
from .views.wallet_api import wallet_router
|
||||||
@@ -31,7 +30,6 @@ def init_core_routers(app: FastAPI):
|
|||||||
app.include_router(extension_router)
|
app.include_router(extension_router)
|
||||||
app.include_router(super_node_router)
|
app.include_router(super_node_router)
|
||||||
app.include_router(public_node_router)
|
app.include_router(public_node_router)
|
||||||
app.include_router(public_router)
|
|
||||||
app.include_router(payment_router)
|
app.include_router(payment_router)
|
||||||
app.include_router(wallet_router)
|
app.include_router(wallet_router)
|
||||||
app.include_router(api_router)
|
app.include_router(api_router)
|
||||||
|
|||||||
@@ -123,15 +123,20 @@ async def get_payments_paginated(
|
|||||||
clause.append("wallet_id = :wallet_id")
|
clause.append("wallet_id = :wallet_id")
|
||||||
|
|
||||||
if complete and pending:
|
if complete and pending:
|
||||||
pass
|
clause.append(
|
||||||
|
f"(status = '{PaymentState.SUCCESS}' OR status = '{PaymentState.PENDING}')"
|
||||||
|
)
|
||||||
elif complete:
|
elif complete:
|
||||||
clause.append(
|
clause.append(
|
||||||
f"((amount > 0 AND status = '{PaymentState.SUCCESS}') OR amount < 0)"
|
f"""
|
||||||
|
(
|
||||||
|
status = '{PaymentState.SUCCESS}'
|
||||||
|
OR (amount < 0 AND status = '{PaymentState.PENDING}')
|
||||||
|
)
|
||||||
|
"""
|
||||||
)
|
)
|
||||||
elif pending:
|
elif pending:
|
||||||
clause.append(f"status = '{PaymentState.PENDING}'")
|
clause.append(f"status = '{PaymentState.PENDING}'")
|
||||||
else:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if outgoing and incoming:
|
if outgoing and incoming:
|
||||||
pass
|
pass
|
||||||
@@ -289,8 +294,14 @@ async def get_payments_history(
|
|||||||
values = {
|
values = {
|
||||||
"wallet_id": wallet_id,
|
"wallet_id": wallet_id,
|
||||||
}
|
}
|
||||||
|
# count outgoing payments if they are still pending
|
||||||
where = [
|
where = [
|
||||||
f"wallet_id = :wallet_id AND (status = '{PaymentState.SUCCESS}' OR amount < 0)"
|
f"""
|
||||||
|
wallet_id = :wallet_id AND (
|
||||||
|
status = '{PaymentState.SUCCESS}'
|
||||||
|
OR (amount < 0 AND status = '{PaymentState.PENDING}')
|
||||||
|
)
|
||||||
|
"""
|
||||||
]
|
]
|
||||||
transactions: list[dict] = await db.fetchall(
|
transactions: list[dict] = await db.fetchall(
|
||||||
f"""
|
f"""
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from typing import Any, Optional
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.core.db import db
|
from lnbits.core.db import db
|
||||||
|
from lnbits.db import dict_to_model
|
||||||
from lnbits.settings import (
|
from lnbits.settings import (
|
||||||
AdminSettings,
|
AdminSettings,
|
||||||
EditableSettings,
|
EditableSettings,
|
||||||
@@ -18,7 +19,8 @@ async def get_super_settings() -> Optional[SuperSettings]:
|
|||||||
if data:
|
if data:
|
||||||
super_user = await get_settings_field("super_user")
|
super_user = await get_settings_field("super_user")
|
||||||
super_user_id = super_user.value if super_user else None
|
super_user_id = super_user.value if super_user else None
|
||||||
return SuperSettings(**{"super_user": super_user_id, **data})
|
settings_dict = {"super_user": super_user_id, **data}
|
||||||
|
return dict_to_model(settings_dict, SuperSettings)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 Optional
|
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
|
||||||
@@ -46,6 +46,20 @@ async def get_accounts(
|
|||||||
filters: Optional[Filters[AccountFilters]] = None,
|
filters: Optional[Filters[AccountFilters]] = None,
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> Page[AccountOverview]:
|
) -> Page[AccountOverview]:
|
||||||
|
where_clauses = []
|
||||||
|
values: dict[str, Any] = {}
|
||||||
|
|
||||||
|
# Make wallet filter explicit
|
||||||
|
wallet_filter = (
|
||||||
|
next((f for f in filters.filters if f.field == "wallet_id"), None)
|
||||||
|
if filters
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
if filters and wallet_filter and wallet_filter.values:
|
||||||
|
where_clauses.append("wallets.id = :wallet_id")
|
||||||
|
values = {**values, "wallet_id": next(iter(wallet_filter.values.values()))}
|
||||||
|
filters.filters = [f for f in filters.filters if f.field != "wallet_id"]
|
||||||
|
|
||||||
return await (conn or db).fetch_page(
|
return await (conn or db).fetch_page(
|
||||||
"""
|
"""
|
||||||
SELECT
|
SELECT
|
||||||
@@ -53,7 +67,6 @@ async def get_accounts(
|
|||||||
accounts.username,
|
accounts.username,
|
||||||
accounts.email,
|
accounts.email,
|
||||||
accounts.pubkey,
|
accounts.pubkey,
|
||||||
wallets.id as wallet_id,
|
|
||||||
SUM(COALESCE((
|
SUM(COALESCE((
|
||||||
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
||||||
), 0)) as balance_msat,
|
), 0)) as balance_msat,
|
||||||
@@ -69,8 +82,8 @@ async def get_accounts(
|
|||||||
)) as last_payment
|
)) as last_payment
|
||||||
FROM accounts LEFT JOIN wallets ON accounts.id = wallets.user
|
FROM accounts LEFT JOIN wallets ON accounts.id = wallets.user
|
||||||
""",
|
""",
|
||||||
[],
|
where_clauses,
|
||||||
{},
|
values,
|
||||||
filters=filters,
|
filters=filters,
|
||||||
model=AccountOverview,
|
model=AccountOverview,
|
||||||
group_by=["accounts.id"],
|
group_by=["accounts.id"],
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ async def create_wallet(
|
|||||||
user=user_id,
|
user=user_id,
|
||||||
adminkey=uuid4().hex,
|
adminkey=uuid4().hex,
|
||||||
inkey=uuid4().hex,
|
inkey=uuid4().hex,
|
||||||
|
currency="USD",
|
||||||
)
|
)
|
||||||
await (conn or db).insert("wallets", wallet)
|
await (conn or db).insert("wallets", wallet)
|
||||||
return wallet
|
return wallet
|
||||||
|
|||||||
+28
-28
@@ -9,7 +9,7 @@ from lnbits import bolt11
|
|||||||
from lnbits.db import Connection
|
from lnbits.db import Connection
|
||||||
|
|
||||||
|
|
||||||
async def m000_create_migrations_table(db):
|
async def m000_create_migrations_table(db: Connection):
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS dbversions (
|
CREATE TABLE IF NOT EXISTS dbversions (
|
||||||
@@ -20,7 +20,7 @@ async def m000_create_migrations_table(db):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def m001_initial(db):
|
async def m001_initial(db: Connection):
|
||||||
"""
|
"""
|
||||||
Initial LNbits tables.
|
Initial LNbits tables.
|
||||||
"""
|
"""
|
||||||
@@ -89,7 +89,7 @@ async def m001_initial(db):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def m002_add_fields_to_apipayments(db):
|
async def m002_add_fields_to_apipayments(db: Connection):
|
||||||
"""
|
"""
|
||||||
Adding fields to apipayments for better accounting,
|
Adding fields to apipayments for better accounting,
|
||||||
and renaming payhash to checking_id since that is what it really is.
|
and renaming payhash to checking_id since that is what it really is.
|
||||||
@@ -133,7 +133,7 @@ async def m002_add_fields_to_apipayments(db):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
async def m003_add_invoice_webhook(db):
|
async def m003_add_invoice_webhook(db: Connection):
|
||||||
"""
|
"""
|
||||||
Special column for webhook endpoints that can be assigned
|
Special column for webhook endpoints that can be assigned
|
||||||
to each different invoice.
|
to each different invoice.
|
||||||
@@ -143,7 +143,7 @@ async def m003_add_invoice_webhook(db):
|
|||||||
await db.execute("ALTER TABLE apipayments ADD COLUMN webhook_status TEXT")
|
await db.execute("ALTER TABLE apipayments ADD COLUMN webhook_status TEXT")
|
||||||
|
|
||||||
|
|
||||||
async def m004_ensure_fees_are_always_negative(db):
|
async def m004_ensure_fees_are_always_negative(db: Connection):
|
||||||
"""
|
"""
|
||||||
Use abs() so wallet backends don't have to care about the sign of the fees.
|
Use abs() so wallet backends don't have to care about the sign of the fees.
|
||||||
"""
|
"""
|
||||||
@@ -168,7 +168,7 @@ async def m004_ensure_fees_are_always_negative(db):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def m005_balance_check_balance_notify(db):
|
async def m005_balance_check_balance_notify(db: Connection):
|
||||||
"""
|
"""
|
||||||
Keep track of balanceCheck-enabled lnurl-withdrawals to be consumed by an
|
Keep track of balanceCheck-enabled lnurl-withdrawals to be consumed by an
|
||||||
LNbits wallet and of balanceNotify URLs supplied by users to empty their wallets.
|
LNbits wallet and of balanceNotify URLs supplied by users to empty their wallets.
|
||||||
@@ -198,7 +198,7 @@ async def m005_balance_check_balance_notify(db):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def m006_add_invoice_expiry_to_apipayments(db):
|
async def m006_add_invoice_expiry_to_apipayments(db: Connection):
|
||||||
"""
|
"""
|
||||||
Adds invoice expiry column to apipayments.
|
Adds invoice expiry column to apipayments.
|
||||||
"""
|
"""
|
||||||
@@ -208,7 +208,7 @@ async def m006_add_invoice_expiry_to_apipayments(db):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
async def m007_set_invoice_expiries(db):
|
async def m007_set_invoice_expiries(db: Connection):
|
||||||
"""
|
"""
|
||||||
Precomputes invoice expiry for existing pending incoming payments.
|
Precomputes invoice expiry for existing pending incoming payments.
|
||||||
"""
|
"""
|
||||||
@@ -258,7 +258,7 @@ async def m007_set_invoice_expiries(db):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
async def m008_create_admin_settings_table(db):
|
async def m008_create_admin_settings_table(db: Connection):
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS settings (
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
@@ -269,7 +269,7 @@ async def m008_create_admin_settings_table(db):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def m009_create_tinyurl_table(db):
|
async def m009_create_tinyurl_table(db: Connection):
|
||||||
await db.execute(
|
await db.execute(
|
||||||
f"""
|
f"""
|
||||||
CREATE TABLE IF NOT EXISTS tiny_url (
|
CREATE TABLE IF NOT EXISTS tiny_url (
|
||||||
@@ -283,7 +283,7 @@ async def m009_create_tinyurl_table(db):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def m010_create_installed_extensions_table(db):
|
async def m010_create_installed_extensions_table(db: Connection):
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS installed_extensions (
|
CREATE TABLE IF NOT EXISTS installed_extensions (
|
||||||
@@ -300,7 +300,7 @@ async def m010_create_installed_extensions_table(db):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def m011_optimize_balances_view(db):
|
async def m011_optimize_balances_view(db: Connection):
|
||||||
"""
|
"""
|
||||||
Make the calculation of the balance a single aggregation
|
Make the calculation of the balance a single aggregation
|
||||||
over the payments table instead of 2.
|
over the payments table instead of 2.
|
||||||
@@ -317,7 +317,7 @@ async def m011_optimize_balances_view(db):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def m012_add_currency_to_wallet(db):
|
async def m012_add_currency_to_wallet(db: Connection):
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
ALTER TABLE wallets ADD COLUMN currency TEXT
|
ALTER TABLE wallets ADD COLUMN currency TEXT
|
||||||
@@ -325,7 +325,7 @@ async def m012_add_currency_to_wallet(db):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def m013_add_deleted_to_wallets(db):
|
async def m013_add_deleted_to_wallets(db: Connection):
|
||||||
"""
|
"""
|
||||||
Adds deleted column to wallets.
|
Adds deleted column to wallets.
|
||||||
"""
|
"""
|
||||||
@@ -337,7 +337,7 @@ async def m013_add_deleted_to_wallets(db):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
async def m014_set_deleted_wallets(db):
|
async def m014_set_deleted_wallets(db: Connection):
|
||||||
"""
|
"""
|
||||||
Sets deleted column to wallets.
|
Sets deleted column to wallets.
|
||||||
"""
|
"""
|
||||||
@@ -381,7 +381,7 @@ async def m014_set_deleted_wallets(db):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
async def m015_create_push_notification_subscriptions_table(db):
|
async def m015_create_push_notification_subscriptions_table(db: Connection):
|
||||||
await db.execute(
|
await db.execute(
|
||||||
f"""
|
f"""
|
||||||
CREATE TABLE IF NOT EXISTS webpush_subscriptions (
|
CREATE TABLE IF NOT EXISTS webpush_subscriptions (
|
||||||
@@ -396,7 +396,7 @@ async def m015_create_push_notification_subscriptions_table(db):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def m016_add_username_column_to_accounts(db):
|
async def m016_add_username_column_to_accounts(db: Connection):
|
||||||
"""
|
"""
|
||||||
Adds username column to accounts.
|
Adds username column to accounts.
|
||||||
"""
|
"""
|
||||||
@@ -407,7 +407,7 @@ async def m016_add_username_column_to_accounts(db):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
async def m017_add_timestamp_columns_to_accounts_and_wallets(db):
|
async def m017_add_timestamp_columns_to_accounts_and_wallets(db: Connection):
|
||||||
"""
|
"""
|
||||||
Adds created_at and updated_at column to accounts and wallets.
|
Adds created_at and updated_at column to accounts and wallets.
|
||||||
"""
|
"""
|
||||||
@@ -473,7 +473,7 @@ async def m017_add_timestamp_columns_to_accounts_and_wallets(db):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
async def m018_balances_view_exclude_deleted(db):
|
async def m018_balances_view_exclude_deleted(db: Connection):
|
||||||
"""
|
"""
|
||||||
Make deleted wallets not show up in the balances view.
|
Make deleted wallets not show up in the balances view.
|
||||||
"""
|
"""
|
||||||
@@ -493,7 +493,7 @@ async def m018_balances_view_exclude_deleted(db):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def m019_balances_view_based_on_wallets(db):
|
async def m019_balances_view_based_on_wallets(db: Connection):
|
||||||
"""
|
"""
|
||||||
Make deleted wallets not show up in the balances view.
|
Make deleted wallets not show up in the balances view.
|
||||||
Important for querying whole lnbits balances.
|
Important for querying whole lnbits balances.
|
||||||
@@ -514,14 +514,14 @@ async def m019_balances_view_based_on_wallets(db):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def m020_add_column_column_to_user_extensions(db):
|
async def m020_add_column_column_to_user_extensions(db: Connection):
|
||||||
"""
|
"""
|
||||||
Adds extra column to user extensions.
|
Adds extra column to user extensions.
|
||||||
"""
|
"""
|
||||||
await db.execute("ALTER TABLE extensions ADD COLUMN extra TEXT")
|
await db.execute("ALTER TABLE extensions ADD COLUMN extra TEXT")
|
||||||
|
|
||||||
|
|
||||||
async def m021_add_success_failed_to_apipayments(db):
|
async def m021_add_success_failed_to_apipayments(db: Connection):
|
||||||
"""
|
"""
|
||||||
Adds success and failed columns to apipayments.
|
Adds success and failed columns to apipayments.
|
||||||
"""
|
"""
|
||||||
@@ -547,7 +547,7 @@ async def m021_add_success_failed_to_apipayments(db):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def m022_add_pubkey_to_accounts(db):
|
async def m022_add_pubkey_to_accounts(db: Connection):
|
||||||
"""
|
"""
|
||||||
Adds pubkey column to accounts.
|
Adds pubkey column to accounts.
|
||||||
"""
|
"""
|
||||||
@@ -557,7 +557,7 @@ async def m022_add_pubkey_to_accounts(db):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
async def m023_add_column_column_to_apipayments(db):
|
async def m023_add_column_column_to_apipayments(db: Connection):
|
||||||
"""
|
"""
|
||||||
renames hash to payment_hash and drops unused index
|
renames hash to payment_hash and drops unused index
|
||||||
"""
|
"""
|
||||||
@@ -569,11 +569,11 @@ async def m023_add_column_column_to_apipayments(db):
|
|||||||
await db.execute("CREATE INDEX by_hash ON apipayments (payment_hash)")
|
await db.execute("CREATE INDEX by_hash ON apipayments (payment_hash)")
|
||||||
|
|
||||||
|
|
||||||
async def m024_drop_pending(db):
|
async def m024_drop_pending(db: Connection):
|
||||||
await db.execute("ALTER TABLE apipayments DROP COLUMN pending")
|
await db.execute("ALTER TABLE apipayments DROP COLUMN pending")
|
||||||
|
|
||||||
|
|
||||||
async def m025_refresh_view(db):
|
async def m025_refresh_view(db: Connection):
|
||||||
await db.execute("DROP VIEW balances")
|
await db.execute("DROP VIEW balances")
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
@@ -592,7 +592,7 @@ async def m025_refresh_view(db):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def m026_update_payment_table(db):
|
async def m026_update_payment_table(db: Connection):
|
||||||
await db.execute("ALTER TABLE apipayments ADD COLUMN tag TEXT")
|
await db.execute("ALTER TABLE apipayments ADD COLUMN tag TEXT")
|
||||||
await db.execute("ALTER TABLE apipayments ADD COLUMN extension TEXT")
|
await db.execute("ALTER TABLE apipayments ADD COLUMN extension TEXT")
|
||||||
await db.execute("ALTER TABLE apipayments ADD COLUMN created_at TIMESTAMP")
|
await db.execute("ALTER TABLE apipayments ADD COLUMN created_at TIMESTAMP")
|
||||||
@@ -666,7 +666,7 @@ async def m028_update_settings(db: Connection):
|
|||||||
await db.execute("drop table settings")
|
await db.execute("drop table settings")
|
||||||
|
|
||||||
|
|
||||||
async def m029_create_audit_table(db):
|
async def m029_create_audit_table(db: Connection):
|
||||||
await db.execute(
|
await db.execute(
|
||||||
f"""
|
f"""
|
||||||
CREATE TABLE IF NOT EXISTS audit (
|
CREATE TABLE IF NOT EXISTS audit (
|
||||||
|
|||||||
@@ -25,12 +25,12 @@ from .users import (
|
|||||||
Account,
|
Account,
|
||||||
AccountFilters,
|
AccountFilters,
|
||||||
AccountOverview,
|
AccountOverview,
|
||||||
CreateTopup,
|
|
||||||
CreateUser,
|
CreateUser,
|
||||||
LoginUsernamePassword,
|
LoginUsernamePassword,
|
||||||
LoginUsr,
|
LoginUsr,
|
||||||
RegisterUser,
|
RegisterUser,
|
||||||
ResetUserPassword,
|
ResetUserPassword,
|
||||||
|
UpdateBalance,
|
||||||
UpdateSuperuserPassword,
|
UpdateSuperuserPassword,
|
||||||
UpdateUser,
|
UpdateUser,
|
||||||
UpdateUserPassword,
|
UpdateUserPassword,
|
||||||
@@ -73,12 +73,12 @@ __all__ = [
|
|||||||
"Account",
|
"Account",
|
||||||
"AccountFilters",
|
"AccountFilters",
|
||||||
"AccountOverview",
|
"AccountOverview",
|
||||||
"CreateTopup",
|
|
||||||
"CreateUser",
|
"CreateUser",
|
||||||
"RegisterUser",
|
"RegisterUser",
|
||||||
"LoginUsernamePassword",
|
"LoginUsernamePassword",
|
||||||
"LoginUsr",
|
"LoginUsr",
|
||||||
"ResetUserPassword",
|
"ResetUserPassword",
|
||||||
|
"UpdateBalance",
|
||||||
"UpdateSuperuserPassword",
|
"UpdateSuperuserPassword",
|
||||||
"UpdateUser",
|
"UpdateUser",
|
||||||
"UpdateUserPassword",
|
"UpdateUserPassword",
|
||||||
|
|||||||
@@ -51,8 +51,8 @@ class AuditFilters(FilterModel):
|
|||||||
|
|
||||||
|
|
||||||
class AuditCountStat(BaseModel):
|
class AuditCountStat(BaseModel):
|
||||||
field: str
|
field: str = ""
|
||||||
total: float
|
total: float = 0
|
||||||
|
|
||||||
|
|
||||||
class AuditStats(BaseModel):
|
class AuditStats(BaseModel):
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from pydantic import BaseModel
|
|||||||
from lnbits.helpers import (
|
from lnbits.helpers import (
|
||||||
download_url,
|
download_url,
|
||||||
file_hash,
|
file_hash,
|
||||||
|
is_lnbits_version_ok,
|
||||||
version_parse,
|
version_parse,
|
||||||
)
|
)
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
@@ -32,6 +33,7 @@ class ExplicitRelease(BaseModel):
|
|||||||
icon: Optional[str]
|
icon: Optional[str]
|
||||||
short_description: Optional[str]
|
short_description: Optional[str]
|
||||||
min_lnbits_version: Optional[str]
|
min_lnbits_version: Optional[str]
|
||||||
|
max_lnbits_version: Optional[str]
|
||||||
html_url: Optional[str] # todo: release_url
|
html_url: Optional[str] # todo: release_url
|
||||||
warning: Optional[str]
|
warning: Optional[str]
|
||||||
info_notification: Optional[str]
|
info_notification: Optional[str]
|
||||||
@@ -40,9 +42,7 @@ class ExplicitRelease(BaseModel):
|
|||||||
pay_link: Optional[str]
|
pay_link: Optional[str]
|
||||||
|
|
||||||
def is_version_compatible(self):
|
def is_version_compatible(self):
|
||||||
if not self.min_lnbits_version:
|
return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version)
|
||||||
return True
|
|
||||||
return version_parse(self.min_lnbits_version) <= version_parse(settings.version)
|
|
||||||
|
|
||||||
|
|
||||||
class GitHubRelease(BaseModel):
|
class GitHubRelease(BaseModel):
|
||||||
@@ -79,11 +79,10 @@ class ExtensionConfig(BaseModel):
|
|||||||
tile: str = ""
|
tile: str = ""
|
||||||
warning: Optional[str] = ""
|
warning: Optional[str] = ""
|
||||||
min_lnbits_version: Optional[str]
|
min_lnbits_version: Optional[str]
|
||||||
|
max_lnbits_version: Optional[str]
|
||||||
|
|
||||||
def is_version_compatible(self):
|
def is_version_compatible(self) -> bool:
|
||||||
if not self.min_lnbits_version:
|
return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version)
|
||||||
return True
|
|
||||||
return version_parse(self.min_lnbits_version) <= version_parse(settings.version)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def fetch_github_release_config(
|
async def fetch_github_release_config(
|
||||||
@@ -181,6 +180,7 @@ class ExtensionRelease(BaseModel):
|
|||||||
is_github_release: bool = False
|
is_github_release: bool = False
|
||||||
hash: Optional[str] = None
|
hash: Optional[str] = None
|
||||||
min_lnbits_version: Optional[str] = None
|
min_lnbits_version: Optional[str] = None
|
||||||
|
max_lnbits_version: Optional[str] = None
|
||||||
is_version_compatible: Optional[bool] = True
|
is_version_compatible: Optional[bool] = True
|
||||||
html_url: Optional[str] = None
|
html_url: Optional[str] = None
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
@@ -251,6 +251,7 @@ class ExtensionRelease(BaseModel):
|
|||||||
source_repo=source_repo,
|
source_repo=source_repo,
|
||||||
description=e.short_description,
|
description=e.short_description,
|
||||||
min_lnbits_version=e.min_lnbits_version,
|
min_lnbits_version=e.min_lnbits_version,
|
||||||
|
max_lnbits_version=e.max_lnbits_version,
|
||||||
is_version_compatible=e.is_version_compatible(),
|
is_version_compatible=e.is_version_compatible(),
|
||||||
warning=e.warning,
|
warning=e.warning,
|
||||||
html_url=e.html_url,
|
html_url=e.html_url,
|
||||||
@@ -348,6 +349,11 @@ class InstallableExtension(BaseModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def module_name(self) -> str:
|
def module_name(self) -> str:
|
||||||
|
if self.ext_upgrade_dir.is_dir():
|
||||||
|
if settings.has_default_extension_path:
|
||||||
|
return f"lnbits.upgrades.{self.id}-{self.hash}"
|
||||||
|
return f"{self.id}-{self.hash}"
|
||||||
|
|
||||||
if settings.has_default_extension_path:
|
if settings.has_default_extension_path:
|
||||||
return f"lnbits.extensions.{self.id}"
|
return f"lnbits.extensions.{self.id}"
|
||||||
return self.id
|
return self.id
|
||||||
@@ -573,6 +579,7 @@ class InstallableExtension(BaseModel):
|
|||||||
archive=f"{conf_path}",
|
archive=f"{conf_path}",
|
||||||
source_repo=f"{conf_path}",
|
source_repo=f"{conf_path}",
|
||||||
min_lnbits_version=config_json.get("min_lnbits_version"),
|
min_lnbits_version=config_json.get("min_lnbits_version"),
|
||||||
|
max_lnbits_version=config_json.get("max_lnbits_version"),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ class Payment(BaseModel):
|
|||||||
class PaymentFilters(FilterModel):
|
class PaymentFilters(FilterModel):
|
||||||
__search_fields__ = ["memo", "amount"]
|
__search_fields__ = ["memo", "amount"]
|
||||||
|
|
||||||
|
status: str
|
||||||
checking_id: str
|
checking_id: str
|
||||||
amount: int
|
amount: int
|
||||||
fee: int
|
fee: int
|
||||||
|
|||||||
@@ -195,6 +195,6 @@ class AccessTokenPayload(BaseModel):
|
|||||||
auth_time: Optional[int] = 0
|
auth_time: Optional[int] = 0
|
||||||
|
|
||||||
|
|
||||||
class CreateTopup(BaseModel):
|
class UpdateBalance(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
amount: int
|
amount: int
|
||||||
|
|||||||
@@ -23,18 +23,23 @@ from ..models.extensions import Extension, ExtensionMeta, InstallableExtension
|
|||||||
|
|
||||||
|
|
||||||
async def install_extension(ext_info: InstallableExtension) -> Extension:
|
async def install_extension(ext_info: InstallableExtension) -> Extension:
|
||||||
ext_id = ext_info.id
|
|
||||||
extension = Extension.from_installable_ext(ext_info)
|
ext_info.meta = ext_info.meta or ExtensionMeta()
|
||||||
installed_ext = await get_installed_extension(ext_id)
|
|
||||||
|
if ext_info.meta.installed_release:
|
||||||
|
assert (
|
||||||
|
ext_info.meta.installed_release.is_version_compatible
|
||||||
|
), "Incompatible extension version"
|
||||||
|
|
||||||
|
installed_ext = await get_installed_extension(ext_info.id)
|
||||||
if installed_ext and installed_ext.meta:
|
if installed_ext and installed_ext.meta:
|
||||||
ext_info.meta = ext_info.meta or ExtensionMeta()
|
|
||||||
ext_info.meta.payments = installed_ext.meta.payments
|
ext_info.meta.payments = installed_ext.meta.payments
|
||||||
|
|
||||||
await ext_info.download_archive()
|
await ext_info.download_archive()
|
||||||
|
|
||||||
ext_info.extract_archive()
|
ext_info.extract_archive()
|
||||||
|
|
||||||
db_version = await get_db_version(ext_id)
|
db_version = await get_db_version(ext_info.id)
|
||||||
await migrate_extension_database(ext_info, db_version)
|
await migrate_extension_database(ext_info, db_version)
|
||||||
|
|
||||||
# if the extensions does not exist in the installed extensions table, create it
|
# if the extensions does not exist in the installed extensions table, create it
|
||||||
@@ -44,9 +49,12 @@ async def install_extension(ext_info: InstallableExtension) -> Extension:
|
|||||||
else:
|
else:
|
||||||
await update_installed_extension(ext_info)
|
await update_installed_extension(ext_info)
|
||||||
|
|
||||||
|
extension = Extension.from_installable_ext(ext_info)
|
||||||
if extension.is_upgrade_extension:
|
if extension.is_upgrade_extension:
|
||||||
# call stop while the old routes are still active
|
# call stop while the old routes are still active
|
||||||
await stop_extension_background_work(ext_id)
|
await stop_extension_background_work(ext_info.id)
|
||||||
|
|
||||||
|
await start_extension_background_work(ext_info.id)
|
||||||
|
|
||||||
return extension
|
return extension
|
||||||
|
|
||||||
@@ -75,7 +83,7 @@ async def deactivate_extension(ext_id: str):
|
|||||||
async def stop_extension_background_work(ext_id: str) -> bool:
|
async def stop_extension_background_work(ext_id: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Stop background work for extension (like asyncio.Tasks, WebSockets, etc).
|
Stop background work for extension (like asyncio.Tasks, WebSockets, etc).
|
||||||
Extensions SHOULD expose a `api_stop()` function.
|
Extension must expose a `myextension_stop()` function if it is starting tasks.
|
||||||
"""
|
"""
|
||||||
upgrade_hash = settings.extension_upgrade_hash(ext_id)
|
upgrade_hash = settings.extension_upgrade_hash(ext_id)
|
||||||
ext = Extension(code=ext_id, is_valid=True, upgrade_hash=upgrade_hash)
|
ext = Extension(code=ext_id, is_valid=True, upgrade_hash=upgrade_hash)
|
||||||
@@ -84,11 +92,10 @@ async def stop_extension_background_work(ext_id: str) -> bool:
|
|||||||
logger.info(f"Stopping background work for extension '{ext.module_name}'.")
|
logger.info(f"Stopping background work for extension '{ext.module_name}'.")
|
||||||
old_module = importlib.import_module(ext.module_name)
|
old_module = importlib.import_module(ext.module_name)
|
||||||
|
|
||||||
# Extensions must expose an `{ext_id}_stop()` function at the module level
|
stop_fn_name = f"{ext_id}_stop"
|
||||||
# The `api_stop()` function is for backwards compatibility (will be deprecated)
|
assert hasattr(
|
||||||
stop_fns = [f"{ext_id}_stop", "api_stop"]
|
old_module, stop_fn_name
|
||||||
stop_fn_name = next((fn for fn in stop_fns if hasattr(old_module, fn)), None)
|
), f"No stop function found for '{ext.module_name}'."
|
||||||
assert stop_fn_name, f"No stop function found for '{ext.module_name}'."
|
|
||||||
|
|
||||||
stop_fn = getattr(old_module, stop_fn_name)
|
stop_fn = getattr(old_module, stop_fn_name)
|
||||||
if stop_fn:
|
if stop_fn:
|
||||||
@@ -96,7 +103,6 @@ async def stop_extension_background_work(ext_id: str) -> bool:
|
|||||||
await stop_fn()
|
await stop_fn()
|
||||||
else:
|
else:
|
||||||
stop_fn()
|
stop_fn()
|
||||||
|
|
||||||
logger.info(f"Stopped background work for extension '{ext.module_name}'.")
|
logger.info(f"Stopped background work for extension '{ext.module_name}'.")
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning(f"Failed to stop background work for '{ext.module_name}'.")
|
logger.warning(f"Failed to stop background work for '{ext.module_name}'.")
|
||||||
@@ -106,6 +112,38 @@ async def stop_extension_background_work(ext_id: str) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def start_extension_background_work(ext_id: str) -> bool:
|
||||||
|
"""
|
||||||
|
Start background work for extension (like asyncio.Tasks, WebSockets, etc).
|
||||||
|
Extension CAN expose a `myextension_start()` function if it is starting tasks.
|
||||||
|
Extension MUST expose a `myextension_stop()` in that case.
|
||||||
|
"""
|
||||||
|
upgrade_hash = settings.extension_upgrade_hash(ext_id)
|
||||||
|
ext = Extension(code=ext_id, is_valid=True, upgrade_hash=upgrade_hash)
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info(f"Starting background work for extension '{ext.module_name}'.")
|
||||||
|
new_module = importlib.import_module(ext.module_name)
|
||||||
|
start_fn_name = f"{ext_id}_start"
|
||||||
|
|
||||||
|
# start function is optional, return False if not found
|
||||||
|
if not hasattr(new_module, start_fn_name):
|
||||||
|
return False
|
||||||
|
|
||||||
|
start_fn = getattr(new_module, start_fn_name)
|
||||||
|
if start_fn:
|
||||||
|
if asyncio.iscoroutinefunction(start_fn):
|
||||||
|
await start_fn()
|
||||||
|
else:
|
||||||
|
start_fn()
|
||||||
|
logger.info(f"Started background work for extension '{ext.module_name}'.")
|
||||||
|
return True
|
||||||
|
except Exception as ex:
|
||||||
|
logger.warning(f"Failed to start background work for '{ext.module_name}'.")
|
||||||
|
logger.warning(ex)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
async def get_valid_extensions(
|
async def get_valid_extensions(
|
||||||
include_deactivated: Optional[bool] = True,
|
include_deactivated: Optional[bool] = True,
|
||||||
) -> list[Extension]:
|
) -> list[Extension]:
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import json
|
|||||||
import time
|
import time
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
from bolt11 import Bolt11, MilliSatoshi, Tags
|
||||||
from bolt11 import decode as bolt11_decode
|
from bolt11 import decode as bolt11_decode
|
||||||
from bolt11.types import Bolt11
|
from bolt11 import encode as bolt11_encode
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.core.db import db
|
from lnbits.core.db import db
|
||||||
@@ -11,6 +12,7 @@ from lnbits.db import Connection
|
|||||||
from lnbits.decorators import check_user_extension_access
|
from lnbits.decorators import check_user_extension_access
|
||||||
from lnbits.exceptions import InvoiceError, PaymentError
|
from lnbits.exceptions import InvoiceError, PaymentError
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
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 (
|
||||||
@@ -195,12 +197,59 @@ def service_fee(amount_msat: int, internal: bool = False) -> int:
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
async def update_wallet_balance(wallet_id: str, amount: int):
|
async def update_wallet_balance(
|
||||||
async with db.connect() as conn:
|
wallet: Wallet,
|
||||||
|
amount: int,
|
||||||
|
conn: Optional[Connection] = None,
|
||||||
|
):
|
||||||
|
if amount == 0:
|
||||||
|
raise ValueError("Amount cannot be 0.")
|
||||||
|
|
||||||
|
# negative balance change
|
||||||
|
if amount < 0:
|
||||||
|
if wallet.balance + amount < 0:
|
||||||
|
raise ValueError("Balance change failed, can not go into negative balance.")
|
||||||
|
async with db.reuse_conn(conn) if conn else db.connect() as conn:
|
||||||
|
payment_secret, payment_hash = random_secret_and_hash()
|
||||||
|
invoice = Bolt11(
|
||||||
|
currency="bc",
|
||||||
|
amount_msat=MilliSatoshi(abs(amount) * 1000),
|
||||||
|
date=int(time.time()),
|
||||||
|
tags=Tags.from_dict(
|
||||||
|
{
|
||||||
|
"payment_hash": payment_hash,
|
||||||
|
"payment_secret": payment_secret,
|
||||||
|
"description": "Admin debit",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
privkey = fake_privkey(settings.fake_wallet_secret)
|
||||||
|
bolt11 = bolt11_encode(invoice, privkey)
|
||||||
|
await create_payment(
|
||||||
|
checking_id=f"internal_{payment_hash}",
|
||||||
|
data=CreatePayment(
|
||||||
|
wallet_id=wallet.id,
|
||||||
|
bolt11=bolt11,
|
||||||
|
payment_hash=payment_hash,
|
||||||
|
amount_msat=amount * 1000,
|
||||||
|
memo="Admin debit",
|
||||||
|
),
|
||||||
|
status=PaymentState.SUCCESS,
|
||||||
|
conn=conn,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# positive balance change
|
||||||
|
if (
|
||||||
|
settings.lnbits_wallet_limit_max_balance > 0
|
||||||
|
and wallet.balance + amount > settings.lnbits_wallet_limit_max_balance
|
||||||
|
):
|
||||||
|
raise ValueError("Balance change failed, amount exceeds maximum balance.")
|
||||||
|
async with db.reuse_conn(conn) if conn else db.connect() as conn:
|
||||||
payment = await create_invoice(
|
payment = await create_invoice(
|
||||||
wallet_id=wallet_id,
|
wallet_id=wallet.id,
|
||||||
amount=amount,
|
amount=amount,
|
||||||
memo="Admin top up",
|
memo="Admin credit",
|
||||||
internal=True,
|
internal=True,
|
||||||
conn=conn,
|
conn=conn,
|
||||||
)
|
)
|
||||||
@@ -218,16 +267,16 @@ async def send_payment_notification(wallet: Wallet, payment: Payment):
|
|||||||
# 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
|
||||||
await websocket_manager.send_data(
|
payment_notification = json.dumps(
|
||||||
json.dumps(
|
{
|
||||||
{
|
"wallet_balance": wallet.balance,
|
||||||
"wallet_balance": wallet.balance,
|
# use pydantic json serialization to get the correct datetime format
|
||||||
# use pydantic json serialization to get the correct datetime format
|
"payment": json.loads(payment.json()),
|
||||||
"payment": json.loads(payment.json()),
|
},
|
||||||
},
|
|
||||||
),
|
|
||||||
wallet.inkey,
|
|
||||||
)
|
)
|
||||||
|
await websocket_manager.send_data(payment_notification, wallet.inkey)
|
||||||
|
await websocket_manager.send_data(payment_notification, wallet.adminkey)
|
||||||
|
|
||||||
await websocket_manager.send_data(
|
await websocket_manager.send_data(
|
||||||
json.dumps({"pending": payment.pending}), payment.payment_hash
|
json.dumps({"pending": payment.pending}), payment.payment_hash
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from loguru import logger
|
|||||||
from py_vapid import Vapid
|
from py_vapid import Vapid
|
||||||
from py_vapid.utils import b64urlencode
|
from py_vapid.utils import b64urlencode
|
||||||
|
|
||||||
|
from lnbits.db import dict_to_model
|
||||||
from lnbits.settings import (
|
from lnbits.settings import (
|
||||||
EditableSettings,
|
EditableSettings,
|
||||||
readonly_variables,
|
readonly_variables,
|
||||||
@@ -37,14 +38,16 @@ async def check_webpush_settings():
|
|||||||
|
|
||||||
|
|
||||||
def update_cached_settings(sets_dict: dict):
|
def update_cached_settings(sets_dict: dict):
|
||||||
for key, value in sets_dict.items():
|
editable_settings = dict_to_model(sets_dict, EditableSettings)
|
||||||
|
for key in sets_dict.keys():
|
||||||
if key in readonly_variables:
|
if key in readonly_variables:
|
||||||
continue
|
continue
|
||||||
if key not in settings.dict().keys():
|
if key not in settings.dict().keys():
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
|
value = getattr(editable_settings, key)
|
||||||
setattr(settings, key, value)
|
setattr(settings, key, value)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning(f"Failed overriding setting: {key}, value: {value}")
|
logger.warning(f"Failed overriding setting: {key}.")
|
||||||
if "super_user" in sets_dict:
|
if "super_user" in sets_dict:
|
||||||
settings.super_user = sets_dict["super_user"]
|
settings.super_user = sets_dict["super_user"]
|
||||||
|
|||||||
+25
-19
@@ -1,5 +1,4 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -19,8 +18,8 @@ from lnbits.core.services import (
|
|||||||
)
|
)
|
||||||
from lnbits.settings import get_funding_source, settings
|
from lnbits.settings import get_funding_source, settings
|
||||||
from lnbits.tasks import send_push_notification
|
from lnbits.tasks import send_push_notification
|
||||||
|
from lnbits.utils.exchange_rates import btc_rates
|
||||||
|
|
||||||
api_invoice_listeners: Dict[str, asyncio.Queue] = {}
|
|
||||||
audit_queue: asyncio.Queue = asyncio.Queue()
|
audit_queue: asyncio.Queue = asyncio.Queue()
|
||||||
|
|
||||||
|
|
||||||
@@ -84,8 +83,6 @@ async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
|
|||||||
while settings.lnbits_running:
|
while settings.lnbits_running:
|
||||||
payment = await invoice_paid_queue.get()
|
payment = await invoice_paid_queue.get()
|
||||||
logger.trace("received invoice paid event")
|
logger.trace("received invoice paid event")
|
||||||
# dispatch api_invoice_listeners
|
|
||||||
await dispatch_api_invoice_listeners(payment)
|
|
||||||
# payment notification
|
# payment notification
|
||||||
wallet = await get_wallet(payment.wallet_id)
|
wallet = await get_wallet(payment.wallet_id)
|
||||||
if wallet:
|
if wallet:
|
||||||
@@ -97,21 +94,6 @@ async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
|
|||||||
await send_payment_push_notification(payment)
|
await send_payment_push_notification(payment)
|
||||||
|
|
||||||
|
|
||||||
async def dispatch_api_invoice_listeners(payment: Payment):
|
|
||||||
"""
|
|
||||||
Emits events to invoice listener subscribed from the API.
|
|
||||||
"""
|
|
||||||
for chan_name, send_channel in api_invoice_listeners.items():
|
|
||||||
try:
|
|
||||||
logger.debug(f"api invoice listener: sending paid event to {chan_name}")
|
|
||||||
send_channel.put_nowait(payment)
|
|
||||||
except asyncio.QueueFull:
|
|
||||||
logger.error(
|
|
||||||
f"api invoice listener: QueueFull, removing {send_channel}:{chan_name}"
|
|
||||||
)
|
|
||||||
api_invoice_listeners.pop(chan_name)
|
|
||||||
|
|
||||||
|
|
||||||
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.
|
||||||
@@ -188,3 +170,27 @@ async def purge_audit_data():
|
|||||||
|
|
||||||
# clean every hour
|
# clean every hour
|
||||||
await asyncio.sleep(60 * 60)
|
await asyncio.sleep(60 * 60)
|
||||||
|
|
||||||
|
|
||||||
|
async def collect_exchange_rates_data():
|
||||||
|
"""
|
||||||
|
Collect exchange rates data. Used for monitoring only.
|
||||||
|
"""
|
||||||
|
while settings.lnbits_running:
|
||||||
|
currency = settings.lnbits_default_accounting_currency or "USD"
|
||||||
|
max_history_size = settings.lnbits_exchange_history_size
|
||||||
|
sleep_time = settings.lnbits_exchange_history_refresh_interval_seconds
|
||||||
|
|
||||||
|
if sleep_time > 0:
|
||||||
|
try:
|
||||||
|
rates = await btc_rates(currency)
|
||||||
|
if rates:
|
||||||
|
rates_values = [r[1] for r in rates]
|
||||||
|
lnbits_rate = sum(rates_values) / len(rates_values)
|
||||||
|
rates.append(("LNbits", lnbits_rate))
|
||||||
|
settings.append_exchange_rate_datapoint(dict(rates), max_history_size)
|
||||||
|
except Exception as ex:
|
||||||
|
logger.warning(ex)
|
||||||
|
else:
|
||||||
|
sleep_time = 60
|
||||||
|
await asyncio.sleep(sleep_time)
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
<q-tab-panel name="exchange_providers">
|
||||||
|
<h6 class="q-my-none q-mb-sm">Exchange Providers</h6>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8 col-sm-12">
|
||||||
|
<div class="q-pa-sm">
|
||||||
|
<canvas
|
||||||
|
style="
|
||||||
|
width: 100% !important;
|
||||||
|
height: auto !important;
|
||||||
|
min-height: 350px;
|
||||||
|
max-height: 50vh;
|
||||||
|
"
|
||||||
|
ref="exchangeRatesChart"
|
||||||
|
></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 col-sm-12">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
v-model="formData.lnbits_exchange_history_refresh_interval_seconds"
|
||||||
|
type="number"
|
||||||
|
label="Refresh Interval (seconds)"
|
||||||
|
hint="How often should the exchange rates be fetched. Set to zero to disable."
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
v-model="formData.lnbits_exchange_history_size"
|
||||||
|
type="number"
|
||||||
|
label="History Size"
|
||||||
|
hint="How many data points should be kept in memory."
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<code>Refresh Interval</code> and <code> History Size </code>are for
|
||||||
|
historical purposes only.
|
||||||
|
</li>
|
||||||
|
<li>These two settings do not affect the live price computation.</li>
|
||||||
|
<li>
|
||||||
|
Chart currency:
|
||||||
|
<strong
|
||||||
|
><span
|
||||||
|
v-text="formData.lnbits_default_accounting_currency || 'USD'"
|
||||||
|
></span
|
||||||
|
></strong>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-mt-md">
|
||||||
|
<div class="col-6">
|
||||||
|
<q-btn
|
||||||
|
@click="addExchangeProvider()"
|
||||||
|
label="Add Exchange Provider"
|
||||||
|
color="primary"
|
||||||
|
class="q-mb-md"
|
||||||
|
>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<q-btn
|
||||||
|
@click="getDefaultSetting('lnbits_exchange_rate_providers')"
|
||||||
|
flat
|
||||||
|
:label="$t('reset_defaults')"
|
||||||
|
color="primary"
|
||||||
|
class="float-right"
|
||||||
|
>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-table
|
||||||
|
row-key="name"
|
||||||
|
:rows="formData.lnbits_exchange_rate_providers"
|
||||||
|
:columns="exchangesTable.columns"
|
||||||
|
v-model:pagination="exchangesTable.pagination"
|
||||||
|
>
|
||||||
|
<template v-slot:header="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-th auto-width></q-th>
|
||||||
|
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||||
|
<span v-text="col.label"></span>
|
||||||
|
</q-th>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
<template v-slot:body="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-td>
|
||||||
|
<q-btn
|
||||||
|
@click="removeExchangeProvider(props.row)"
|
||||||
|
round
|
||||||
|
icon="delete"
|
||||||
|
size="sm"
|
||||||
|
color="negative"
|
||||||
|
class="q-ml-xs"
|
||||||
|
>
|
||||||
|
</q-btn>
|
||||||
|
</q-td>
|
||||||
|
<q-td>
|
||||||
|
<q-input
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
v-model="props.row.name"
|
||||||
|
@update:model-value="touchSettings()"
|
||||||
|
type="text"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
</q-td>
|
||||||
|
<q-td full-width>
|
||||||
|
<q-input
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
v-model="props.row.api_url"
|
||||||
|
@update:model-value="touchSettings()"
|
||||||
|
type="text"
|
||||||
|
>
|
||||||
|
</q-input
|
||||||
|
></q-td>
|
||||||
|
<q-td>
|
||||||
|
<q-input
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
v-model="props.row.path"
|
||||||
|
@update:model-value="touchSettings()"
|
||||||
|
type="text"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
</q-td>
|
||||||
|
<q-td>
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model="props.row.exclude_to"
|
||||||
|
@update:model-value="touchSettings()"
|
||||||
|
multiple
|
||||||
|
:options="{{ currencies | safe }}"
|
||||||
|
></q-select>
|
||||||
|
</q-td>
|
||||||
|
<q-td>
|
||||||
|
<q-btn
|
||||||
|
@click="showTickerConversionDialog(props.row)"
|
||||||
|
round
|
||||||
|
icon="add"
|
||||||
|
size="sm"
|
||||||
|
color="gray"
|
||||||
|
class="q-ml-xs"
|
||||||
|
>
|
||||||
|
</q-btn>
|
||||||
|
<q-chip
|
||||||
|
v-for="ticker, index in props.row.ticker_conversion"
|
||||||
|
:key="ticker"
|
||||||
|
removable
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
@remove="removeExchangeTickerConversion(props.row, ticker)"
|
||||||
|
color="primary"
|
||||||
|
text-color="white"
|
||||||
|
:label="ticker"
|
||||||
|
class="ellipsis"
|
||||||
|
>
|
||||||
|
</q-chip>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<code>API URL</code> and <code>JSON Path</code> fields can use the
|
||||||
|
<code>{to}</code> and <code>{TO}</code> placeholders for the code of the
|
||||||
|
currency
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>{TO}</code> is the uppercase code and <code>{to}</code> is the
|
||||||
|
lowercase code
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<q-separator class="q-ma-md"></q-separator>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4 col-sm-12">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
v-model="formData.lnbits_exchange_rate_cache_seconds"
|
||||||
|
type="number"
|
||||||
|
label="Exchange rate cache (seconds)"
|
||||||
|
hint="For how many seconds should the exchange rate be cached."
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-8 col-sm-12"></div>
|
||||||
|
</div>
|
||||||
|
</q-tab-panel>
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
filled
|
filled
|
||||||
type="text"
|
type="text"
|
||||||
tip="Custom Badge"
|
tip="Custom Badge"
|
||||||
v-model="formData.lnbits_custom_badge"
|
v-model.trim="formData.lnbits_custom_badge"
|
||||||
label="Custom Badge 'USE WITH CAUTION - LNbits wallet is still in BETA'"
|
label="Custom Badge 'USE WITH CAUTION - LNbits wallet is still in BETA'"
|
||||||
></q-input>
|
></q-input>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||||
%} {% block page %}
|
%} {% block page %}
|
||||||
<div class="row q-col-gutter-md justify-center">
|
<div class="row q-col-gutter-md justify-center">
|
||||||
<div class="col q-my-md">
|
<div class="col q-mb-md">
|
||||||
<q-btn
|
<q-btn
|
||||||
:label="$t('save')"
|
:label="$t('save')"
|
||||||
color="primary"
|
color="primary"
|
||||||
@@ -63,7 +63,12 @@
|
|||||||
<q-card>
|
<q-card>
|
||||||
<q-splitter>
|
<q-splitter>
|
||||||
<template v-slot:before>
|
<template v-slot:before>
|
||||||
<q-tabs v-model="tab" vertical active-color="primary">
|
<q-tabs
|
||||||
|
@update:model-value="showExchangeProvidersTab"
|
||||||
|
v-model="tab"
|
||||||
|
vertical
|
||||||
|
active-color="primary"
|
||||||
|
>
|
||||||
<q-tab
|
<q-tab
|
||||||
name="funding"
|
name="funding"
|
||||||
icon="account_balance_wallet"
|
icon="account_balance_wallet"
|
||||||
@@ -80,23 +85,6 @@
|
|||||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||||
><span v-text="$t('security')"></span></q-tooltip
|
><span v-text="$t('security')"></span></q-tooltip
|
||||||
></q-tab>
|
></q-tab>
|
||||||
|
|
||||||
<q-tab
|
|
||||||
name="users"
|
|
||||||
icon="group"
|
|
||||||
:label="$q.screen.gt.sm ? $t('users') : null"
|
|
||||||
@update="val => tab = val.name"
|
|
||||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
|
||||||
><span v-text="$t('users')"></span></q-tooltip
|
|
||||||
></q-tab>
|
|
||||||
<q-tab
|
|
||||||
name="extensions"
|
|
||||||
icon="extension"
|
|
||||||
:label="$q.screen.gt.sm ? $t('extensions') : null"
|
|
||||||
@update="val => tab = val.name"
|
|
||||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
|
||||||
><span v-text="$t('extensions')"></span></q-tooltip
|
|
||||||
></q-tab>
|
|
||||||
<q-tab
|
<q-tab
|
||||||
name="server"
|
name="server"
|
||||||
icon="price_change"
|
icon="price_change"
|
||||||
@@ -105,6 +93,31 @@
|
|||||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||||
><span v-text="$t('payments')"></span></q-tooltip
|
><span v-text="$t('payments')"></span></q-tooltip
|
||||||
></q-tab>
|
></q-tab>
|
||||||
|
<q-tab
|
||||||
|
name="exchange_providers"
|
||||||
|
icon="show_chart"
|
||||||
|
:label="$q.screen.gt.sm ? $t('exchanges') : null"
|
||||||
|
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||||
|
><span v-text="$t('exchanges')"></span></q-tooltip
|
||||||
|
></q-tab>
|
||||||
|
<q-tab
|
||||||
|
name="users"
|
||||||
|
icon="group"
|
||||||
|
:label="$q.screen.gt.sm ? $t('users') : null"
|
||||||
|
@update="val => tab = val.name"
|
||||||
|
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||||
|
><span v-text="$t('users')"></span></q-tooltip
|
||||||
|
></q-tab>
|
||||||
|
|
||||||
|
<q-tab
|
||||||
|
name="extensions"
|
||||||
|
icon="extension"
|
||||||
|
:label="$q.screen.gt.sm ? $t('extensions') : null"
|
||||||
|
@update="val => tab = val.name"
|
||||||
|
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||||
|
><span v-text="$t('extensions')"></span></q-tooltip
|
||||||
|
></q-tab>
|
||||||
|
|
||||||
<q-tab
|
<q-tab
|
||||||
name="notifications"
|
name="notifications"
|
||||||
icon="notifications"
|
icon="notifications"
|
||||||
@@ -145,7 +158,8 @@
|
|||||||
>
|
>
|
||||||
{% include "admin/_tab_funding.html" %} {% include
|
{% include "admin/_tab_funding.html" %} {% include
|
||||||
"admin/_tab_users.html" %} {% include "admin/_tab_server.html" %}
|
"admin/_tab_users.html" %} {% include "admin/_tab_server.html" %}
|
||||||
{% include "admin/_tab_extensions.html" %} {% include
|
{% include "admin/_tab_exchange_providers.html" %} {% include
|
||||||
|
"admin/_tab_extensions.html" %} {% include
|
||||||
"admin/_tab_notifications.html" %} {% include
|
"admin/_tab_notifications.html" %} {% include
|
||||||
"admin/_tab_security.html" %} {% include "admin/_tab_theme.html"
|
"admin/_tab_security.html" %} {% include "admin/_tab_theme.html"
|
||||||
%}{% include "admin/_tab_audit.html"%}
|
%}{% include "admin/_tab_audit.html"%}
|
||||||
@@ -157,6 +171,47 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<q-dialog v-model="exchangeData.showTickerConversion" position="top">
|
||||||
|
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
|
||||||
|
<strong>Create Currecny Ticker Converter</strong>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-md">
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model="exchangeData.convertFromTicker"
|
||||||
|
label="From Currency"
|
||||||
|
:options="{{ currencies | safe }}"
|
||||||
|
></q-select>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<q-input
|
||||||
|
v-model="exchangeData.convertToTicker"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
label="New Ticker"
|
||||||
|
hint="This ticker will be used for the exchange API calls."
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn
|
||||||
|
@click="addExchangeTickerConversion()"
|
||||||
|
label="Add Ticker Conversion"
|
||||||
|
color="primary"
|
||||||
|
></q-btn>
|
||||||
|
<q-btn
|
||||||
|
v-close-popup
|
||||||
|
flat
|
||||||
|
color="grey"
|
||||||
|
class="q-ml-auto"
|
||||||
|
v-text="$t('close')"
|
||||||
|
></q-btn>
|
||||||
|
</div>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
|
||||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||||
<script src="{{ static_url_for('static', 'js/admin.js') }}"></script>
|
<script src="{{ static_url_for('static', 'js/admin.js') }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||||
%} {% block page %}
|
%} {% block page %}
|
||||||
|
|
||||||
<div class="row q-col-gutter-md justify-center q-mb-xl">
|
<div class="row q-col-gutter-md justify-center q-mb-lg">
|
||||||
<div class="col-lg-3 col-md-6 col-sm-12 text-center">
|
<div class="col-lg-3 col-md-6 col-sm-12 text-center">
|
||||||
<q-card class="q-pt-sm">
|
<q-card class="q-pt-sm">
|
||||||
<strong>Components</strong>
|
<strong>Components</strong>
|
||||||
@@ -70,6 +70,7 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</q-input>
|
</q-input>
|
||||||
|
|
||||||
<q-select
|
<q-select
|
||||||
v-else-if="['component', 'response_code','request_method'].includes(col.name)"
|
v-else-if="['component', 'response_code','request_method'].includes(col.name)"
|
||||||
v-model="searchData[col.name]"
|
v-model="searchData[col.name]"
|
||||||
@@ -82,6 +83,11 @@
|
|||||||
|
|
||||||
<span v-else v-text="col.label"></span>
|
<span v-else v-text="col.label"></span>
|
||||||
</q-th>
|
</q-th>
|
||||||
|
<q-th>
|
||||||
|
<q-btn flat round icon="settings" tag="a" href="/admin#audit"
|
||||||
|
><q-tooltip v-text="$t('admin_settings')"></q-tooltip
|
||||||
|
></q-btn>
|
||||||
|
</q-th>
|
||||||
</q-tr>
|
</q-tr>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:body="props">
|
<template v-slot:body="props">
|
||||||
|
|||||||
@@ -286,6 +286,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
<q-tab-panel name="theme">
|
<q-tab-panel name="theme">
|
||||||
|
<q-btn
|
||||||
|
v-if="g.user.admin"
|
||||||
|
class="absolute-top-right"
|
||||||
|
flat
|
||||||
|
round
|
||||||
|
icon="settings"
|
||||||
|
tag="a"
|
||||||
|
href="/admin#site_customisation"
|
||||||
|
><q-tooltip v-text="$t('admin_settings')"></q-tooltip
|
||||||
|
></q-btn>
|
||||||
<div class="row q-mb-md">
|
<div class="row q-mb-md">
|
||||||
<div class="col-4"><span v-text="$t('language')"></span></div>
|
<div class="col-4"><span v-text="$t('language')"></span></div>
|
||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
@@ -412,6 +422,22 @@
|
|||||||
</q-btn>
|
</q-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row q-mb-md">
|
||||||
|
<div class="col-4">
|
||||||
|
<span v-text="$t('background_image')"></span>
|
||||||
|
</div>
|
||||||
|
<div class="col-8">
|
||||||
|
<q-input
|
||||||
|
v-model="backgroundImage"
|
||||||
|
:label="$t('background_image')"
|
||||||
|
@update:model-value="backgroundImageFunc"
|
||||||
|
>
|
||||||
|
<q-tooltip
|
||||||
|
><span v-text="$t('background_image')"></span
|
||||||
|
></q-tooltip>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="row q-mb-md">
|
<div class="row q-mb-md">
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<span v-text="$t('gradient_background')"></span>
|
<span v-text="$t('gradient_background')"></span>
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||||
%} {{ window_vars(user, extensions) }}{% block page %}
|
%} {{ window_vars(user, extensions) }}{% block page %}
|
||||||
<div class="row q-col-gutter-md q-mb-md">
|
|
||||||
<div class="col-sm-9 col-xs-12">
|
|
||||||
<p class="text-h4 gt-sm" v-text="$t('extensions')"></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-sm-3 col-xs-12 q-ml-auto">
|
|
||||||
<q-input v-model="searchTerm" :label="$t('search_extensions')">
|
|
||||||
<q-icon
|
|
||||||
v-if="searchTerm !== ''"
|
|
||||||
name="close"
|
|
||||||
@click="searchTerm = ''"
|
|
||||||
class="cursor-pointer"
|
|
||||||
/>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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">
|
||||||
@@ -35,6 +19,45 @@
|
|||||||
v-if="!g.user.admin && tab != 'installed'"
|
v-if="!g.user.admin && tab != 'installed'"
|
||||||
v-text="$t('only_admins_can_install')"
|
v-text="$t('only_admins_can_install')"
|
||||||
></i>
|
></i>
|
||||||
|
<q-space></q-space>
|
||||||
|
<q-input
|
||||||
|
:label="$t('search_extensions')"
|
||||||
|
:dense="dense"
|
||||||
|
class="float-right q-pr-xl"
|
||||||
|
v-model="searchTerm"
|
||||||
|
>
|
||||||
|
<template v-slot:before>
|
||||||
|
<q-icon name="search"> </q-icon>
|
||||||
|
</template>
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-icon
|
||||||
|
v-if="searchTerm !== ''"
|
||||||
|
name="close"
|
||||||
|
@click="searchTerm = ''"
|
||||||
|
class="cursor-pointer"
|
||||||
|
>
|
||||||
|
</q-icon>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
<q-badge
|
||||||
|
v-if="g.user.admin && updatableExtensions?.length"
|
||||||
|
@click="showUpdateAllDialog = true"
|
||||||
|
color="primary"
|
||||||
|
class="float-right q-pa-sm q-mr-md cursor-pointer"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-text="$t('new_version') + ` (${updatableExtensions?.length})`"
|
||||||
|
></span>
|
||||||
|
</q-badge>
|
||||||
|
<q-btn
|
||||||
|
v-if="g.user.admin"
|
||||||
|
flat
|
||||||
|
round
|
||||||
|
icon="settings"
|
||||||
|
tag="a"
|
||||||
|
href="/admin#extensions"
|
||||||
|
><q-tooltip v-text="$t('admin_settings')"></q-tooltip
|
||||||
|
></q-btn>
|
||||||
</q-tabs>
|
</q-tabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -229,7 +252,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-dialog v-model="showUninstallDialog">
|
<q-dialog v-model="showUninstallDialog" position="top">
|
||||||
<q-card class="q-pa-lg">
|
<q-card class="q-pa-lg">
|
||||||
<h6 class="q-my-md text-primary" v-text="$t('warning')"></h6>
|
<h6 class="q-my-md text-primary" v-text="$t('warning')"></h6>
|
||||||
<p>
|
<p>
|
||||||
@@ -266,7 +289,7 @@
|
|||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
||||||
<q-dialog v-model="showDropDbDialog">
|
<q-dialog v-model="showDropDbDialog" position="top">
|
||||||
<q-card v-if="selectedExtension" class="q-pa-lg">
|
<q-card v-if="selectedExtension" class="q-pa-lg">
|
||||||
<h6 class="q-my-md text-primary" v-text="$t('warning')"></h6>
|
<h6 class="q-my-md text-primary" v-text="$t('warning')"></h6>
|
||||||
<p><span v-text="$t('extension_db_drop_warning')"></span><br /></p>
|
<p><span v-text="$t('extension_db_drop_warning')"></span><br /></p>
|
||||||
@@ -296,7 +319,7 @@
|
|||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
||||||
<q-dialog v-model="showManageExtensionDialog">
|
<q-dialog v-model="showManageExtensionDialog" position="top">
|
||||||
<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">
|
||||||
@@ -527,10 +550,26 @@
|
|||||||
></a>
|
></a>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-section v-else>
|
<q-card-section v-else>
|
||||||
<span v-text="$t('extension_min_lnbits_version')"></span>
|
<span
|
||||||
<strong>
|
v-text="$t('extension_required_lnbits_version')"
|
||||||
<span v-text="release.min_lnbits_version"></span>
|
></span>
|
||||||
</strong>
|
<span class="q-mr-sm">:</span>
|
||||||
|
<ul>
|
||||||
|
<li v-if="release.min_lnbits_version">
|
||||||
|
<span v-text="$t('min_version')"></span>
|
||||||
|
<span class="q-mr-sm">:</span>
|
||||||
|
<strong>
|
||||||
|
<span v-text="release.min_lnbits_version"></span>
|
||||||
|
</strong>
|
||||||
|
</li>
|
||||||
|
<li v-if="release.max_lnbits_version">
|
||||||
|
<span v-text="$t('max_version')"></span>
|
||||||
|
<span class="q-mr-sm">:</span>
|
||||||
|
<strong>
|
||||||
|
<span v-text="release.max_lnbits_version"></span>
|
||||||
|
</strong>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card v-if="release.warning">
|
<q-card v-if="release.warning">
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
@@ -632,7 +671,7 @@
|
|||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
||||||
<q-dialog v-model="showPayToEnableDialog">
|
<q-dialog v-model="showPayToEnableDialog" position="top">
|
||||||
<q-card v-if="selectedExtension" class="q-pa-md">
|
<q-card v-if="selectedExtension" class="q-pa-md">
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<p>
|
<p>
|
||||||
@@ -738,7 +777,7 @@
|
|||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
||||||
<q-dialog v-model="showExtensionDetailsDialog">
|
<q-dialog v-model="showExtensionDetailsDialog" position="top">
|
||||||
<q-card
|
<q-card
|
||||||
v-if="selectedExtensionDetails"
|
v-if="selectedExtensionDetails"
|
||||||
class="q-pa-sm"
|
class="q-pa-sm"
|
||||||
@@ -884,6 +923,63 @@
|
|||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
<q-dialog v-model="showUpdateAllDialog" position="top">
|
||||||
|
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<h6 class="q-my-md" v-text="$t('update')"></h6>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="updatableExtensions?.length > 1" class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<q-btn
|
||||||
|
outline
|
||||||
|
color="grey"
|
||||||
|
@click="selectAllUpdatableExtensionss()"
|
||||||
|
v-text="$t('select_all')"
|
||||||
|
></q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-virtual-scroll :items="updatableExtensions" style="max-height: 400px">
|
||||||
|
<template v-slot="{ item, index }">
|
||||||
|
<div class="row">
|
||||||
|
<div class="q-col">
|
||||||
|
<q-checkbox
|
||||||
|
v-model="item.selectedForUpdate"
|
||||||
|
:disable="item.isUpgraded"
|
||||||
|
value="false"
|
||||||
|
:label="item.name + ` (v${item.latestRelease?.version})`"
|
||||||
|
>
|
||||||
|
<q-spinner
|
||||||
|
v-if="item.inProgress"
|
||||||
|
color="primary"
|
||||||
|
size="1.5em"
|
||||||
|
class="q-ml-md"
|
||||||
|
></q-spinner>
|
||||||
|
</q-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</q-virtual-scroll>
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn
|
||||||
|
@click="updateSelectedExtensions()"
|
||||||
|
outline
|
||||||
|
color="grey"
|
||||||
|
v-text="$t('update')"
|
||||||
|
></q-btn>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
v-close-popup
|
||||||
|
flat
|
||||||
|
color="grey"
|
||||||
|
class="q-ml-auto"
|
||||||
|
v-text="$t('cancel')"
|
||||||
|
></q-btn>
|
||||||
|
</div>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||||
<script>
|
<script>
|
||||||
window.app = Vue.createApp({
|
window.app = Vue.createApp({
|
||||||
@@ -898,11 +994,13 @@
|
|||||||
tab: 'all',
|
tab: 'all',
|
||||||
manageExtensionTab: 'releases',
|
manageExtensionTab: 'releases',
|
||||||
filteredExtensions: null,
|
filteredExtensions: null,
|
||||||
|
updatableExtensions: [],
|
||||||
showUninstallDialog: false,
|
showUninstallDialog: false,
|
||||||
showManageExtensionDialog: false,
|
showManageExtensionDialog: false,
|
||||||
showExtensionDetailsDialog: false,
|
showExtensionDetailsDialog: false,
|
||||||
showDropDbDialog: false,
|
showDropDbDialog: false,
|
||||||
showPayToEnableDialog: false,
|
showPayToEnableDialog: false,
|
||||||
|
showUpdateAllDialog: false,
|
||||||
dropDbExtensionId: '',
|
dropDbExtensionId: '',
|
||||||
selectedExtension: null,
|
selectedExtension: null,
|
||||||
selectedImage: null,
|
selectedImage: null,
|
||||||
@@ -1074,6 +1172,7 @@
|
|||||||
)
|
)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
|
timeout: 2000,
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: `Extension '${extension.id}' ${action}d!`
|
message: `Extension '${extension.id}' ${action}d!`
|
||||||
})
|
})
|
||||||
@@ -1476,12 +1575,63 @@
|
|||||||
} finally {
|
} finally {
|
||||||
release.inProgress = false
|
release.inProgress = false
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
selectAllUpdatableExtensionss: async function () {
|
||||||
|
this.updatableExtensions.forEach(e => (e.selectedForUpdate = true))
|
||||||
|
},
|
||||||
|
updateSelectedExtensions: async function () {
|
||||||
|
let count = 0
|
||||||
|
for (const ext of this.updatableExtensions) {
|
||||||
|
try {
|
||||||
|
if (!ext.selectedForUpdate) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ext.inProgress = true
|
||||||
|
await LNbits.api.request(
|
||||||
|
'POST',
|
||||||
|
`/api/v1/extension`,
|
||||||
|
this.g.user.wallets[0].adminkey,
|
||||||
|
{
|
||||||
|
ext_id: ext.id,
|
||||||
|
archive: ext.latestRelease.archive,
|
||||||
|
source_repo: ext.latestRelease.source_repo,
|
||||||
|
payment_hash: ext.latestRelease.payment_hash,
|
||||||
|
version: ext.latestRelease.version
|
||||||
|
}
|
||||||
|
)
|
||||||
|
count++
|
||||||
|
ext.isAvailable = true
|
||||||
|
ext.isInstalled = true
|
||||||
|
ext.isUpgraded = true
|
||||||
|
ext.inProgress = false
|
||||||
|
ext.installedRelease = ext.latestRelease
|
||||||
|
this.toggleExtension(ext)
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(err)
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: 'negative',
|
||||||
|
message: `Failed to update ${ext.code}!`
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
ext.inProgress = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: 'positive',
|
||||||
|
message: `${count} extensions updated!`
|
||||||
|
})
|
||||||
|
this.showUpdateAllDialog = false
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload()
|
||||||
|
}, 2000)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
created: function () {
|
created: function () {
|
||||||
this.extensions = JSON.parse('{{extensions | tojson | safe}}').map(e => ({
|
this.extensions = JSON.parse('{{extensions | tojson | safe}}').map(e => ({
|
||||||
...e,
|
...e,
|
||||||
inProgress: false
|
inProgress: false,
|
||||||
|
selectedForUpdate: false
|
||||||
}))
|
}))
|
||||||
this.filteredExtensions = this.extensions.concat([])
|
this.filteredExtensions = this.extensions.concat([])
|
||||||
for (let i = 0; i < this.filteredExtensions.length; i++) {
|
for (let i = 0; i < this.filteredExtensions.length; i++) {
|
||||||
@@ -1493,6 +1643,9 @@
|
|||||||
if (window.user) {
|
if (window.user) {
|
||||||
this.user = LNbits.map.user(window.user)
|
this.user = LNbits.map.user(window.user)
|
||||||
}
|
}
|
||||||
|
this.updatableExtensions = this.extensions.filter(ext =>
|
||||||
|
this.hasNewVersion(ext)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
mixins: [windowMixin]
|
mixins: [windowMixin]
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -25,41 +25,124 @@
|
|||||||
} : ''"
|
} : ''"
|
||||||
>
|
>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-9 col-12">
|
||||||
<q-card
|
<q-card
|
||||||
:style="$q.screen.lt.md ? {
|
:style="$q.screen.lt.md ? {
|
||||||
background: $q.screen.lt.md ? 'none !important': ''
|
background: $q.screen.lt.md ? 'none !important': ''
|
||||||
, boxShadow: $q.screen.lt.md ? 'none !important': ''
|
, boxShadow: $q.screen.lt.md ? 'none !important': ''
|
||||||
, margin: $q.screen.lt.md && mobileSimple ? 'auto !important': ''
|
, width: $q.screen.lt.md && mobileSimple ? '90% !important': ''
|
||||||
, width: $q.screen.lt.md && mobileSimple ? '90% !important': ''
|
} : ''"
|
||||||
} : ''"
|
|
||||||
>
|
>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<h3 class="q-my-none text-no-wrap">
|
|
||||||
<strong v-text="formattedBalance"></strong>
|
|
||||||
<small> {{LNBITS_DENOMINATION}}</small>
|
|
||||||
<lnbits-update-balance
|
|
||||||
:wallet_id="this.g.wallet.id"
|
|
||||||
flat
|
|
||||||
:callback="updateBalanceCallback"
|
|
||||||
round
|
|
||||||
/>
|
|
||||||
</h3>
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col-1" style="max-width: 30px">
|
||||||
<div v-if="g.wallet.currency">
|
<q-btn
|
||||||
|
v-if="update.currency"
|
||||||
|
@click="swapBalancePriority"
|
||||||
|
style="height: 50px"
|
||||||
|
class="q-mt-lg"
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
icon="swap_vert"
|
||||||
|
></q-btn>
|
||||||
|
</div>
|
||||||
|
<div class="col-11">
|
||||||
|
<div
|
||||||
|
class="column"
|
||||||
|
:class="{
|
||||||
|
'q-pt-sm': update.currency,
|
||||||
|
'q-pt-lg': !update.currency
|
||||||
|
}"
|
||||||
|
v-if="!isPrioritySwapped"
|
||||||
|
style="height: 90px"
|
||||||
|
>
|
||||||
|
<div class="col-7">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-auto">
|
||||||
|
<div class="text-h3 q-my-none text-no-wrap">
|
||||||
|
<strong v-text="formattedBalance"></strong>
|
||||||
|
<small> {{LNBITS_DENOMINATION}}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<lnbits-update-balance
|
||||||
|
v-if="$q.screen.lt.lg"
|
||||||
|
:wallet_id="this.g.wallet.id"
|
||||||
|
:callback="updateBalanceCallback"
|
||||||
|
:small_btn="true"
|
||||||
|
></lnbits-update-balance>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-2">
|
||||||
|
<div v-if="update.currency">
|
||||||
|
<span
|
||||||
|
class="text-h5 text-italic"
|
||||||
|
v-text="formattedFiatAmount"
|
||||||
|
style="opacity: 0.75"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="column"
|
||||||
|
v-if="isPrioritySwapped"
|
||||||
|
:class="{
|
||||||
|
'q-pt-sm': update.currency,
|
||||||
|
'q-pt-lg': !update.currency
|
||||||
|
}"
|
||||||
|
style="height: 100px"
|
||||||
|
>
|
||||||
|
<div class="col-7">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-auto">
|
||||||
|
<div
|
||||||
|
v-if="update.currency"
|
||||||
|
class="text-h3 q-my-none text-no-wrap"
|
||||||
|
>
|
||||||
|
<strong v-text="formattedFiatAmount"></strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<lnbits-update-balance
|
||||||
|
v-if="$q.screen.lt.lg"
|
||||||
|
:wallet_id="this.g.wallet.id"
|
||||||
|
:callback="updateBalanceCallback"
|
||||||
|
:small_btn="true"
|
||||||
|
></lnbits-update-balance>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-2">
|
||||||
|
<span
|
||||||
|
class="text-h5 text-italic"
|
||||||
|
style="opacity: 0.75"
|
||||||
|
v-text="formattedBalance + ' {{LNBITS_DENOMINATION}}'"
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="absolute-right q-pa-md"
|
||||||
|
v-if="$q.screen.gt.md && update.currency"
|
||||||
|
>
|
||||||
|
<div class="text-bold text-italic">BTC Price</div>
|
||||||
<span
|
<span
|
||||||
class="text-h5 text-italic"
|
class="text-bold text-italic"
|
||||||
v-text="formattedFiatBalance"
|
v-text="formattedExchange"
|
||||||
style="opacity: 0.75"
|
|
||||||
></span>
|
></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<q-btn
|
<q-btn
|
||||||
|
v-if="$q.screen.lt.md"
|
||||||
@click="mobileSimple = !mobileSimple"
|
@click="mobileSimple = !mobileSimple"
|
||||||
color="primary"
|
color="primary"
|
||||||
class="float-right lt-md"
|
class="q-ml-xl absolute-right"
|
||||||
|
dense
|
||||||
size="sm"
|
size="sm"
|
||||||
|
style="height: 20px; margin-top: 75px"
|
||||||
flat
|
flat
|
||||||
:icon="mobileSimple ? 'unfold_more' : 'unfold_less'"
|
:icon="mobileSimple ? 'unfold_more' : 'unfold_less'"
|
||||||
:label="mobileSimple ? $t('more') : $t('less')"
|
:label="mobileSimple ? $t('more') : $t('less')"
|
||||||
@@ -72,35 +155,49 @@
|
|||||||
<q-btn
|
<q-btn
|
||||||
unelevated
|
unelevated
|
||||||
color="primary"
|
color="primary"
|
||||||
class="full-width"
|
class="q-mr-md"
|
||||||
@click="showParseDialog"
|
@click="showParseDialog"
|
||||||
:label="$t('paste_request')"
|
:label="$t('paste_request')"
|
||||||
></q-btn>
|
></q-btn>
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<q-btn
|
<q-btn
|
||||||
unelevated
|
unelevated
|
||||||
color="primary"
|
color="primary"
|
||||||
class="full-width"
|
class="q-mr-md"
|
||||||
@click="showReceiveDialog"
|
@click="showReceiveDialog"
|
||||||
:label="$t('create_invoice')"
|
:label="$t('create_invoice')"
|
||||||
></q-btn>
|
></q-btn>
|
||||||
</div>
|
<q-btn unelevated color="secondary" icon="qr_code_scanner">
|
||||||
<div class="col">
|
|
||||||
<q-btn
|
|
||||||
unelevated
|
|
||||||
color="secondary"
|
|
||||||
icon="photo_camera"
|
|
||||||
@click="showCamera"
|
|
||||||
:label="$t('scan')"
|
|
||||||
>
|
|
||||||
<q-tooltip
|
<q-tooltip
|
||||||
><span v-text="$t('camera_tooltip')"></span
|
><span v-text="$t('camera_tooltip')"></span
|
||||||
></q-tooltip>
|
></q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
|
<lnbits-update-balance
|
||||||
|
v-if="$q.screen.gt.md"
|
||||||
|
:wallet_id="this.g.wallet.id"
|
||||||
|
:callback="updateBalanceCallback"
|
||||||
|
:small_btn="false"
|
||||||
|
></lnbits-update-balance>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
</div>
|
||||||
|
<div class="col-3" v-show="$q.screen.gt.md">
|
||||||
|
<q-card class="q-ml-md q-pa-none" style="height: 172px">
|
||||||
|
<q-card-section style="padding: 0">
|
||||||
|
<canvas
|
||||||
|
ref="transactionChart"
|
||||||
|
style="
|
||||||
|
height: 172px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
transform: translate(-2%, 4%);
|
||||||
|
"
|
||||||
|
></canvas>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<q-card
|
<q-card
|
||||||
:style="
|
:style="
|
||||||
$q.screen.lt.md
|
$q.screen.lt.md
|
||||||
@@ -117,17 +214,14 @@
|
|||||||
:update="updatePayments"
|
:update="updatePayments"
|
||||||
:wallet="this.g.wallet"
|
:wallet="this.g.wallet"
|
||||||
:mobile-simple="mobileSimple"
|
:mobile-simple="mobileSimple"
|
||||||
/>
|
></payment-list>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</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">
|
||||||
{% else %}
|
{% else %}
|
||||||
<div
|
<div v-if="!mobileSimple || $q.screen.gt.sm" class="col-12 col-md-5 q-gutter-y-md">
|
||||||
v-if="!mobileSimple || $q.screen.gt.sm"
|
|
||||||
class="col-12 col-md-5 q-gutter-y-md"
|
|
||||||
>
|
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<h6 class="text-subtitle1 q-mt-none q-mb-sm">
|
<h6 class="text-subtitle1 q-mt-none q-mb-sm">
|
||||||
@@ -216,24 +310,27 @@
|
|||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div style="max-width: 360px">
|
<div style="max-width: 360px">
|
||||||
<q-select
|
<div class="row">
|
||||||
filled
|
<div class="col-auto">
|
||||||
dense
|
<q-toggle
|
||||||
clearable
|
v-model="fiatTracking"
|
||||||
v-model="update.currency"
|
color="primary"
|
||||||
type="text"
|
@update:model-value="handleFiatTracking"
|
||||||
:label="$t('currency')"
|
></q-toggle>
|
||||||
:options="receive.units.filter((u) => u !== 'sat')"
|
</div>
|
||||||
></q-select>
|
<div class="col">
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model="update.currency"
|
||||||
|
type="text"
|
||||||
|
:label="!fiatTracking ? 'Disabled' : $t('currency')"
|
||||||
|
:options="receive.units.filter((u) => u !== 'sat')"
|
||||||
|
:disable="!fiatTracking"
|
||||||
|
></q-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<q-btn
|
|
||||||
:disable="!update.name.length"
|
|
||||||
unelevated
|
|
||||||
class="q-mt-sm"
|
|
||||||
color="primary"
|
|
||||||
:label="$t('update_currency')"
|
|
||||||
@click="updateWallet({ currency: update.currency || '' })"
|
|
||||||
></q-btn>
|
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-expansion-item>
|
</q-expansion-item>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<q-tab-panel name="channels">
|
<q-tab-panel name="channels">
|
||||||
<q-dialog v-model="connectPeerDialog.show">
|
<q-dialog v-model="connectPeerDialog.show" position="top">
|
||||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||||
<q-form class="q-gutter-md">
|
<q-form class="q-gutter-md">
|
||||||
<q-input
|
<q-input
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
||||||
<q-dialog v-model="openChannelDialog.show">
|
<q-dialog v-model="openChannelDialog.show" position="top">
|
||||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||||
<q-form class="q-gutter-md">
|
<q-form class="q-gutter-md">
|
||||||
<q-input
|
<q-input
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
||||||
<q-dialog v-model="closeChannelDialog.show">
|
<q-dialog v-model="closeChannelDialog.show" position="top">
|
||||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||||
<q-form class="q-gutter-md">
|
<q-form class="q-gutter-md">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<q-tab-panel name="transactions">
|
<q-tab-panel name="transactions">
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<q-dialog v-model="transactionDetailsDialog.show">
|
<q-dialog v-model="transactionDetailsDialog.show" position="top">
|
||||||
<q-card class="my-card">
|
<q-card class="my-card">
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div class="text-center q-mb-lg">
|
<div class="text-center q-mb-lg">
|
||||||
@@ -120,7 +120,11 @@
|
|||||||
>
|
>
|
||||||
<q-tooltip>Pending</q-tooltip>
|
<q-tooltip>Pending</q-tooltip>
|
||||||
</q-icon>
|
</q-icon>
|
||||||
<q-dialog v-model="props.row.expand" :props="props">
|
<q-dialog
|
||||||
|
v-model="props.row.expand"
|
||||||
|
:props="props"
|
||||||
|
position="top"
|
||||||
|
>
|
||||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||||
<div class="text-center q-mb-lg">
|
<div class="text-center q-mb-lg">
|
||||||
<div v-if="props.row.isIn && props.row.pending">
|
<div v-if="props.row.isIn && props.row.pending">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||||
%} {% block page %}
|
%} {% block page %}
|
||||||
|
|
||||||
<q-dialog v-model="nodeInfoDialog.show">
|
<q-dialog v-model="nodeInfoDialog.show" position="top">
|
||||||
<lnbits-node-qrcode :info="nodeInfoDialog.data"></lnbits-node-qrcode>
|
<lnbits-node-qrcode :info="nodeInfoDialog.data"></lnbits-node-qrcode>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
||||||
|
|||||||
@@ -30,9 +30,6 @@ self.addEventListener('activate', evt =>
|
|||||||
// from the network before returning it to the page.
|
// from the network before returning it to the page.
|
||||||
self.addEventListener('fetch', event => {
|
self.addEventListener('fetch', event => {
|
||||||
if (
|
if (
|
||||||
!event.request.url.startsWith(
|
|
||||||
self.location.origin + '/api/v1/payments/sse'
|
|
||||||
) &&
|
|
||||||
event.request.url.startsWith(self.location.origin) &&
|
event.request.url.startsWith(self.location.origin) &&
|
||||||
event.request.method == 'GET'
|
event.request.method == 'GET'
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -48,13 +48,11 @@
|
|||||||
<template v-slot:body="props">
|
<template v-slot:body="props">
|
||||||
<q-tr :props="props">
|
<q-tr :props="props">
|
||||||
<q-td auto-width>
|
<q-td auto-width>
|
||||||
<q-btn
|
<lnbits-update-balance
|
||||||
:label="$t('topup')"
|
:wallet_id="props.row.id"
|
||||||
@click="showTopupDialog(props.row.id)"
|
@credit-value="handleBalanceUpdate"
|
||||||
color="secondary"
|
|
||||||
size="sm"
|
|
||||||
class="q-mr-md"
|
class="q-mr-md"
|
||||||
></q-btn>
|
></lnbits-update-balance>
|
||||||
<q-btn
|
<q-btn
|
||||||
round
|
round
|
||||||
icon="menu"
|
icon="menu"
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
<q-dialog v-model="topupDialog.show" position="top">
|
|
||||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
|
||||||
<q-form class="q-gutter-md">
|
|
||||||
<p v-text="$t('topup_wallet')"></p>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<q-input
|
|
||||||
dense
|
|
||||||
type="text"
|
|
||||||
filled
|
|
||||||
v-model="wallet.id"
|
|
||||||
label="Wallet ID"
|
|
||||||
:hint="$t('topup_hint')"
|
|
||||||
></q-input>
|
|
||||||
|
|
||||||
<br />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12">
|
|
||||||
<q-input
|
|
||||||
dense
|
|
||||||
type="number"
|
|
||||||
filled
|
|
||||||
v-model="wallet.amount"
|
|
||||||
:label="$t('amount')"
|
|
||||||
></q-input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-btn
|
|
||||||
:label="$t('topup')"
|
|
||||||
color="primary"
|
|
||||||
@click="topupWallet"
|
|
||||||
v-close-popup
|
|
||||||
></q-btn>
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
v-close-popup
|
|
||||||
flat
|
|
||||||
color="grey"
|
|
||||||
class="q-ml-auto"
|
|
||||||
:label="$t('cancel')"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
</q-form>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||||
%} {% block page %}{% include "users/_topupDialog.html" %}
|
%} {% block page %}
|
||||||
|
|
||||||
<div class="row q-col-gutter-md justify-center">
|
<div class="row q-col-gutter-md justify-center">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
|
|||||||
@@ -14,10 +14,9 @@ from lnbits.core.services import (
|
|||||||
get_balance_delta,
|
get_balance_delta,
|
||||||
update_cached_settings,
|
update_cached_settings,
|
||||||
)
|
)
|
||||||
from lnbits.core.tasks import api_invoice_listeners
|
|
||||||
from lnbits.decorators import check_admin, check_super_user
|
from lnbits.decorators import check_admin, check_super_user
|
||||||
from lnbits.server import server_restart
|
from lnbits.server import server_restart
|
||||||
from lnbits.settings import AdminSettings, UpdateSettings, settings
|
from lnbits.settings import AdminSettings, Settings, UpdateSettings, settings
|
||||||
from lnbits.tasks import invoice_listeners
|
from lnbits.tasks import invoice_listeners
|
||||||
|
|
||||||
from .. import core_app_extra
|
from .. import core_app_extra
|
||||||
@@ -45,7 +44,6 @@ async def api_auditor():
|
|||||||
async def api_monitor():
|
async def api_monitor():
|
||||||
return {
|
return {
|
||||||
"invoice_listeners": list(invoice_listeners.keys()),
|
"invoice_listeners": list(invoice_listeners.keys()),
|
||||||
"api_invoice_listeners": list(api_invoice_listeners.keys()),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -70,6 +68,16 @@ async def api_update_settings(data: UpdateSettings, user: User = Depends(check_a
|
|||||||
return {"status": "Success"}
|
return {"status": "Success"}
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.get(
|
||||||
|
"/api/v1/settings/default",
|
||||||
|
status_code=HTTPStatus.OK,
|
||||||
|
dependencies=[Depends(check_admin)],
|
||||||
|
)
|
||||||
|
async def api_reset_settings(field_name: str):
|
||||||
|
default_settings = Settings()
|
||||||
|
return {"default_value": getattr(default_settings, field_name)}
|
||||||
|
|
||||||
|
|
||||||
@admin_router.delete(
|
@admin_router.delete(
|
||||||
"/api/v1/settings",
|
"/api/v1/settings",
|
||||||
status_code=HTTPStatus.OK,
|
status_code=HTTPStatus.OK,
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ from lnbits.settings import settings
|
|||||||
from lnbits.utils.exchange_rates import (
|
from lnbits.utils.exchange_rates import (
|
||||||
allowed_currencies,
|
allowed_currencies,
|
||||||
fiat_amount_as_satoshis,
|
fiat_amount_as_satoshis,
|
||||||
get_fiat_rate_satoshis,
|
get_fiat_rate_and_price_satoshis,
|
||||||
satoshis_amount_as_fiat,
|
satoshis_amount_as_fiat,
|
||||||
)
|
)
|
||||||
from lnbits.wallets import get_funding_source
|
from lnbits.wallets import get_funding_source
|
||||||
@@ -225,10 +225,18 @@ async def api_perform_lnurlauth(
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.get(
|
||||||
|
"/api/v1/rate/history",
|
||||||
|
dependencies=[Depends(require_invoice_key)],
|
||||||
|
)
|
||||||
|
async def api_exchange_rate_history() -> list[dict]:
|
||||||
|
return settings.lnbits_exchange_rate_history
|
||||||
|
|
||||||
|
|
||||||
@api_router.get("/api/v1/rate/{currency}")
|
@api_router.get("/api/v1/rate/{currency}")
|
||||||
async def api_check_fiat_rate(currency: str) -> dict[str, float]:
|
async def api_check_fiat_rate(currency: str) -> dict[str, float]:
|
||||||
rate = await get_fiat_rate_satoshis(currency)
|
rate, price = await get_fiat_rate_and_price_satoshis(currency)
|
||||||
return {"rate": rate}
|
return {"rate": rate, "price": price}
|
||||||
|
|
||||||
|
|
||||||
@api_router.get("/api/v1/currencies")
|
@api_router.get("/api/v1/currencies")
|
||||||
|
|||||||
@@ -69,11 +69,6 @@ async def api_install_extension(data: CreateExtension):
|
|||||||
status_code=HTTPStatus.NOT_FOUND, detail="Release not found"
|
status_code=HTTPStatus.NOT_FOUND, detail="Release not found"
|
||||||
)
|
)
|
||||||
|
|
||||||
if not release.is_version_compatible:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST, detail="Incompatible extension version"
|
|
||||||
)
|
|
||||||
|
|
||||||
release.payment_hash = data.payment_hash
|
release.payment_hash = data.payment_hash
|
||||||
ext_meta = ExtensionMeta(installed_release=release)
|
ext_meta = ExtensionMeta(installed_release=release)
|
||||||
ext_info = InstallableExtension(
|
ext_info = InstallableExtension(
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ public_node_router = APIRouter(
|
|||||||
@node_router.get(
|
@node_router.get(
|
||||||
"/ok",
|
"/ok",
|
||||||
description="Check if node api can be enabled",
|
description="Check if node api can be enabled",
|
||||||
status_code=200,
|
status_code=HTTPStatus.OK,
|
||||||
dependencies=[Depends(require_node)],
|
dependencies=[Depends(require_node)],
|
||||||
)
|
)
|
||||||
async def api_get_ok():
|
async def api_get_ok():
|
||||||
@@ -197,5 +197,5 @@ async def api_get_1ml_stats(node: Node = Depends(require_node)) -> Optional[Node
|
|||||||
return r.json()["noderank"]
|
return r.json()["noderank"]
|
||||||
except httpx.HTTPStatusError as exc:
|
except httpx.HTTPStatusError as exc:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=404, detail="Node not found on 1ml.com"
|
status_code=HTTPStatus.NOT_FOUND, detail="Node not found on 1ml.com"
|
||||||
) from exc
|
) from exc
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import asyncio
|
|
||||||
import json
|
import json
|
||||||
import uuid
|
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from math import ceil
|
from math import ceil
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
@@ -13,11 +11,9 @@ from fastapi import (
|
|||||||
Header,
|
Header,
|
||||||
HTTPException,
|
HTTPException,
|
||||||
Query,
|
Query,
|
||||||
Request,
|
|
||||||
)
|
)
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from sse_starlette.sse import EventSourceResponse
|
|
||||||
|
|
||||||
from lnbits import bolt11
|
from lnbits import bolt11
|
||||||
from lnbits.core.models import (
|
from lnbits.core.models import (
|
||||||
@@ -57,7 +53,6 @@ from ..services import (
|
|||||||
pay_invoice,
|
pay_invoice,
|
||||||
update_pending_payments,
|
update_pending_payments,
|
||||||
)
|
)
|
||||||
from ..tasks import api_invoice_listeners
|
|
||||||
|
|
||||||
payment_router = APIRouter(prefix="/api/v1/payments", tags=["Payments"])
|
payment_router = APIRouter(prefix="/api/v1/payments", tags=["Payments"])
|
||||||
|
|
||||||
@@ -113,8 +108,6 @@ async def api_payments_paginated(
|
|||||||
await update_pending_payments(key_info.wallet.id)
|
await update_pending_payments(key_info.wallet.id)
|
||||||
page = await get_payments_paginated(
|
page = await get_payments_paginated(
|
||||||
wallet_id=key_info.wallet.id,
|
wallet_id=key_info.wallet.id,
|
||||||
pending=True,
|
|
||||||
complete=True,
|
|
||||||
filters=filters,
|
filters=filters,
|
||||||
)
|
)
|
||||||
return page
|
return page
|
||||||
@@ -313,47 +306,6 @@ async def api_payments_pay_lnurl(
|
|||||||
return payment
|
return payment
|
||||||
|
|
||||||
|
|
||||||
async def subscribe_wallet_invoices(request: Request, wallet: Wallet):
|
|
||||||
"""
|
|
||||||
Subscribe to new invoices for a wallet. Can be wrapped in EventSourceResponse.
|
|
||||||
Listenes invoming payments for a wallet and yields jsons with payment details.
|
|
||||||
"""
|
|
||||||
this_wallet_id = wallet.id
|
|
||||||
|
|
||||||
payment_queue: asyncio.Queue[Payment] = asyncio.Queue(0)
|
|
||||||
|
|
||||||
uid = f"{this_wallet_id}_{str(uuid.uuid4())[:8]}"
|
|
||||||
logger.debug(f"adding sse listener for wallet: {uid}")
|
|
||||||
api_invoice_listeners[uid] = payment_queue
|
|
||||||
|
|
||||||
try:
|
|
||||||
while settings.lnbits_running:
|
|
||||||
if await request.is_disconnected():
|
|
||||||
await request.close()
|
|
||||||
break
|
|
||||||
payment: Payment = await payment_queue.get()
|
|
||||||
if payment.wallet_id == this_wallet_id:
|
|
||||||
logger.debug("sse listener: payment received", payment)
|
|
||||||
yield {"data": payment.json(), "event": "payment-received"}
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
logger.debug(f"removing listener for wallet {uid}")
|
|
||||||
except Exception as exc:
|
|
||||||
logger.error(f"Error in sse: {exc}")
|
|
||||||
finally:
|
|
||||||
api_invoice_listeners.pop(uid)
|
|
||||||
|
|
||||||
|
|
||||||
@payment_router.get("/sse")
|
|
||||||
async def api_payments_sse(
|
|
||||||
request: Request, key_info: WalletTypeInfo = Depends(require_invoice_key)
|
|
||||||
):
|
|
||||||
return EventSourceResponse(
|
|
||||||
subscribe_wallet_invoices(request, key_info.wallet),
|
|
||||||
ping=20,
|
|
||||||
media_type="text/event-stream",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# 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: Optional[str] = Header(None)):
|
async def api_payment(payment_hash, x_api_key: Optional[str] = Header(None)):
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
from http import HTTPStatus
|
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from lnbits import bolt11
|
|
||||||
|
|
||||||
from ..crud import get_standalone_payment
|
|
||||||
from ..tasks import api_invoice_listeners
|
|
||||||
|
|
||||||
public_router = APIRouter(tags=["Core"])
|
|
||||||
|
|
||||||
|
|
||||||
@public_router.get("/public/v1/payment/{payment_hash}")
|
|
||||||
async def api_public_payment_longpolling(payment_hash):
|
|
||||||
payment = await get_standalone_payment(payment_hash)
|
|
||||||
|
|
||||||
if not payment:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Payment does not exist."
|
|
||||||
)
|
|
||||||
# TODO: refactor to use PaymentState
|
|
||||||
if payment.success:
|
|
||||||
return {"status": "paid"}
|
|
||||||
|
|
||||||
try:
|
|
||||||
invoice = bolt11.decode(payment.bolt11)
|
|
||||||
if invoice.has_expired():
|
|
||||||
return {"status": "expired"}
|
|
||||||
except Exception as exc:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST, detail="Invalid bolt11 invoice."
|
|
||||||
) from exc
|
|
||||||
|
|
||||||
payment_queue = asyncio.Queue(0)
|
|
||||||
|
|
||||||
logger.debug(f"adding standalone invoice listener for hash: {payment_hash}")
|
|
||||||
api_invoice_listeners[payment_hash] = payment_queue
|
|
||||||
|
|
||||||
response = None
|
|
||||||
|
|
||||||
async def payment_info_receiver():
|
|
||||||
for payment in await payment_queue.get():
|
|
||||||
if payment.payment_hash == payment_hash:
|
|
||||||
nonlocal response
|
|
||||||
response = {"status": "paid"}
|
|
||||||
|
|
||||||
async def timeouter(cancel_scope):
|
|
||||||
await asyncio.sleep(45)
|
|
||||||
cancel_scope.cancel()
|
|
||||||
|
|
||||||
cancel_scope = asyncio.create_task(payment_info_receiver())
|
|
||||||
asyncio.create_task(timeouter(cancel_scope)) # noqa: RUF006
|
|
||||||
|
|
||||||
if response:
|
|
||||||
return response
|
|
||||||
else:
|
|
||||||
raise HTTPException(status_code=HTTPStatus.REQUEST_TIMEOUT, detail="timeout")
|
|
||||||
@@ -24,9 +24,9 @@ from lnbits.core.crud import (
|
|||||||
from lnbits.core.models import (
|
from lnbits.core.models import (
|
||||||
AccountFilters,
|
AccountFilters,
|
||||||
AccountOverview,
|
AccountOverview,
|
||||||
CreateTopup,
|
|
||||||
CreateUser,
|
CreateUser,
|
||||||
SimpleStatus,
|
SimpleStatus,
|
||||||
|
UpdateBalance,
|
||||||
User,
|
User,
|
||||||
UserExtra,
|
UserExtra,
|
||||||
Wallet,
|
Wallet,
|
||||||
@@ -267,16 +267,14 @@ async def api_users_delete_user_wallet(user_id: str, wallet: str) -> SimpleStatu
|
|||||||
|
|
||||||
|
|
||||||
@users_router.put(
|
@users_router.put(
|
||||||
"/topup",
|
"/balance",
|
||||||
name="Topup",
|
name="UpdateBalance",
|
||||||
summary="Update balance for a particular wallet.",
|
summary="Update balance for a particular wallet.",
|
||||||
status_code=HTTPStatus.OK,
|
|
||||||
dependencies=[Depends(check_super_user)],
|
dependencies=[Depends(check_super_user)],
|
||||||
)
|
)
|
||||||
async def api_topup_balance(data: CreateTopup) -> SimpleStatus:
|
async def api_update_balance(data: UpdateBalance) -> SimpleStatus:
|
||||||
await get_wallet(data.id)
|
wallet = await get_wallet(data.id)
|
||||||
if settings.lnbits_backend_wallet_class == "VoidWallet":
|
if not wallet:
|
||||||
raise Exception("VoidWallet active")
|
raise HTTPException(HTTPStatus.NOT_FOUND, "Wallet not found.")
|
||||||
|
await update_wallet_balance(wallet=wallet, amount=int(data.amount))
|
||||||
await update_wallet_balance(wallet_id=data.id, amount=int(data.amount))
|
|
||||||
return SimpleStatus(success=True, message="Balance updated.")
|
return SimpleStatus(success=True, message="Balance updated.")
|
||||||
|
|||||||
@@ -241,6 +241,21 @@ def version_parse(v: str):
|
|||||||
return version.parse("0.0.0")
|
return version.parse("0.0.0")
|
||||||
|
|
||||||
|
|
||||||
|
def is_lnbits_version_ok(
|
||||||
|
min_lnbits_version: Optional[str], max_lnbits_version: Optional[str]
|
||||||
|
) -> bool:
|
||||||
|
if min_lnbits_version and (
|
||||||
|
version_parse(min_lnbits_version) > version_parse(settings.version)
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
if max_lnbits_version and (
|
||||||
|
version_parse(max_lnbits_version) <= version_parse(settings.version)
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def download_url(url, save_path):
|
def download_url(url, save_path):
|
||||||
with request.urlopen(url, timeout=60) as dl_file:
|
with request.urlopen(url, timeout=60) as dl_file:
|
||||||
with open(save_path, "wb") as out_file:
|
with open(save_path, "wb") as out_file:
|
||||||
|
|||||||
+4
-3
@@ -1,3 +1,4 @@
|
|||||||
|
from http import HTTPStatus
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
from fastapi import HTTPException, Request, Response
|
from fastapi import HTTPException, Request, Response
|
||||||
@@ -31,21 +32,21 @@ class LnurlErrorResponseHandler(APIRoute):
|
|||||||
except (InvoiceError, PaymentError) as exc:
|
except (InvoiceError, PaymentError) as exc:
|
||||||
logger.debug(f"Wallet Error: {exc}")
|
logger.debug(f"Wallet Error: {exc}")
|
||||||
response = JSONResponse(
|
response = JSONResponse(
|
||||||
status_code=200,
|
status_code=HTTPStatus.OK,
|
||||||
content={"status": "ERROR", "reason": f"{exc.message}"},
|
content={"status": "ERROR", "reason": f"{exc.message}"},
|
||||||
)
|
)
|
||||||
return response
|
return response
|
||||||
except HTTPException as exc:
|
except HTTPException as exc:
|
||||||
logger.debug(f"HTTPException: {exc}")
|
logger.debug(f"HTTPException: {exc}")
|
||||||
response = JSONResponse(
|
response = JSONResponse(
|
||||||
status_code=200,
|
status_code=HTTPStatus.OK,
|
||||||
content={"status": "ERROR", "reason": f"{exc.detail}"},
|
content={"status": "ERROR", "reason": f"{exc.detail}"},
|
||||||
)
|
)
|
||||||
return response
|
return response
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error("Unknown Error:", exc)
|
logger.error("Unknown Error:", exc)
|
||||||
response = JSONResponse(
|
response = JSONResponse(
|
||||||
status_code=200,
|
status_code=HTTPStatus.OK,
|
||||||
content={
|
content={
|
||||||
"status": "ERROR",
|
"status": "ERROR",
|
||||||
"reason": f"UNKNOWN ERROR: {exc!s}",
|
"reason": f"UNKNOWN ERROR: {exc!s}",
|
||||||
|
|||||||
+2
-15
@@ -11,7 +11,6 @@ from slowapi import _rate_limit_exceeded_handler
|
|||||||
from slowapi.errors import RateLimitExceeded
|
from slowapi.errors import RateLimitExceeded
|
||||||
from slowapi.middleware import SlowAPIMiddleware
|
from slowapi.middleware import SlowAPIMiddleware
|
||||||
from starlette.middleware.base import BaseHTTPMiddleware
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
from starlette.middleware.gzip import GZipMiddleware
|
|
||||||
from starlette.types import ASGIApp, Receive, Scope, Send
|
from starlette.types import ASGIApp, Receive, Scope, Send
|
||||||
|
|
||||||
from lnbits.core.db import core_app_extra
|
from lnbits.core.db import core_app_extra
|
||||||
@@ -93,18 +92,6 @@ class InstalledExtensionMiddleware:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CustomGZipMiddleware(GZipMiddleware):
|
|
||||||
def __init__(self, *args, exclude_paths=None, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.exclude_paths = exclude_paths or []
|
|
||||||
|
|
||||||
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
||||||
if "path" in scope and scope["path"] in self.exclude_paths:
|
|
||||||
await self.app(scope, receive, send)
|
|
||||||
return
|
|
||||||
await super().__call__(scope, receive, send)
|
|
||||||
|
|
||||||
|
|
||||||
class ExtensionsRedirectMiddleware:
|
class ExtensionsRedirectMiddleware:
|
||||||
# Extensions are allowed to specify redirect paths. A call to a path outside the
|
# Extensions are allowed to specify redirect paths. A call to a path outside the
|
||||||
# scope of the extension can be redirected to one of the extension's endpoints.
|
# scope of the extension can be redirected to one of the extension's endpoints.
|
||||||
@@ -230,7 +217,7 @@ def add_ip_block_middleware(app: FastAPI):
|
|||||||
async def block_allow_ip_middleware(request: Request, call_next):
|
async def block_allow_ip_middleware(request: Request, call_next):
|
||||||
if not request.client:
|
if not request.client:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=403, # Forbidden
|
status_code=HTTPStatus.FORBIDDEN,
|
||||||
content={"detail": "No request client"},
|
content={"detail": "No request client"},
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
@@ -238,7 +225,7 @@ def add_ip_block_middleware(app: FastAPI):
|
|||||||
and request.client.host not in settings.lnbits_allowed_ips
|
and request.client.host not in settings.lnbits_allowed_ips
|
||||||
):
|
):
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=403, # Forbidden
|
status_code=HTTPStatus.FORBIDDEN,
|
||||||
content={"detail": "IP is blocked"},
|
content={"detail": "IP is blocked"},
|
||||||
)
|
)
|
||||||
return await call_next(request)
|
return await call_next(request)
|
||||||
|
|||||||
+9
-3
@@ -47,9 +47,13 @@ def catch_rpc_errors(f):
|
|||||||
except RpcError as exc:
|
except RpcError as exc:
|
||||||
msg = exc.error["message"]
|
msg = exc.error["message"]
|
||||||
if exc.error["code"] == -32602:
|
if exc.error["code"] == -32602:
|
||||||
raise HTTPException(status_code=400, detail=msg) from exc
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST, detail=msg
|
||||||
|
) from exc
|
||||||
else:
|
else:
|
||||||
raise HTTPException(status_code=500, detail=msg) from exc
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=msg
|
||||||
|
) from exc
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
@@ -152,7 +156,9 @@ class CoreLightningNode(Node):
|
|||||||
force: bool = False,
|
force: bool = False,
|
||||||
):
|
):
|
||||||
if not short_id:
|
if not short_id:
|
||||||
raise HTTPException(status_code=400, detail="Short id required")
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST, detail="Short id required"
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
await self.ln_rpc("close", short_id)
|
await self.ln_rpc("close", short_id)
|
||||||
except RpcError as exc:
|
except RpcError as exc:
|
||||||
|
|||||||
@@ -159,9 +159,15 @@ class LndRestNode(Node):
|
|||||||
timeout=None,
|
timeout=None,
|
||||||
) as stream:
|
) as stream:
|
||||||
async for chunk in stream.aiter_text():
|
async for chunk in stream.aiter_text():
|
||||||
if chunk:
|
if not chunk:
|
||||||
chunk = json.loads(chunk)
|
continue
|
||||||
logger.info(f"LND Channel close update: {chunk['result']}")
|
chunk = json.loads(chunk)
|
||||||
|
if "error" in chunk:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
detail=chunk["error"].get("message"),
|
||||||
|
)
|
||||||
|
logger.info(f"LND Channel close update: {chunk.get('result')}")
|
||||||
|
|
||||||
async def close_channel(
|
async def close_channel(
|
||||||
self,
|
self,
|
||||||
|
|||||||
+111
-1
@@ -5,6 +5,7 @@ import importlib.metadata
|
|||||||
import inspect
|
import inspect
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
from datetime import datetime, timezone
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from os import path
|
from os import path
|
||||||
@@ -118,6 +119,26 @@ class RedirectPath(BaseModel):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class ExchangeRateProvider(BaseModel):
|
||||||
|
name: str
|
||||||
|
api_url: str
|
||||||
|
path: str
|
||||||
|
exclude_to: list[str] = []
|
||||||
|
ticker_conversion: list[str] = []
|
||||||
|
|
||||||
|
def convert_ticker(self, currency: str) -> str:
|
||||||
|
if not self.ticker_conversion:
|
||||||
|
return currency
|
||||||
|
try:
|
||||||
|
for t in self.ticker_conversion:
|
||||||
|
_from, _to = t.split(":")
|
||||||
|
if _from == currency:
|
||||||
|
return _to
|
||||||
|
except Exception as err:
|
||||||
|
logger.warning(err)
|
||||||
|
return currency
|
||||||
|
|
||||||
|
|
||||||
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=[])
|
lnbits_deactivated_extensions: set[str] = Field(default=[])
|
||||||
@@ -193,6 +214,20 @@ class InstalledExtensionsSettings(LNbitsSettings):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ExchangeHistorySettings(LNbitsSettings):
|
||||||
|
|
||||||
|
lnbits_exchange_rate_history: list[dict] = Field(default=[])
|
||||||
|
|
||||||
|
def append_exchange_rate_datapoint(self, rates: dict, max_size: int):
|
||||||
|
data = {
|
||||||
|
"timestamp": int(datetime.now(timezone.utc).timestamp()),
|
||||||
|
"rates": rates,
|
||||||
|
}
|
||||||
|
self.lnbits_exchange_rate_history.append(data)
|
||||||
|
if len(self.lnbits_exchange_rate_history) > max_size:
|
||||||
|
self.lnbits_exchange_rate_history.pop(0)
|
||||||
|
|
||||||
|
|
||||||
class ThemesSettings(LNbitsSettings):
|
class ThemesSettings(LNbitsSettings):
|
||||||
lnbits_site_title: str = Field(default="LNbits")
|
lnbits_site_title: str = Field(default="LNbits")
|
||||||
lnbits_site_tagline: str = Field(default="free and open-source lightning wallet")
|
lnbits_site_tagline: str = Field(default="free and open-source lightning wallet")
|
||||||
@@ -250,6 +285,80 @@ class FeeSettings(LNbitsSettings):
|
|||||||
return max(int(reserve_min), int(amount_msat * reserve_percent / 100.0))
|
return max(int(reserve_min), int(amount_msat * reserve_percent / 100.0))
|
||||||
|
|
||||||
|
|
||||||
|
class ExchangeProvidersSettings(LNbitsSettings):
|
||||||
|
lnbits_exchange_rate_cache_seconds: int = Field(default=30)
|
||||||
|
lnbits_exchange_history_size: int = Field(default=60)
|
||||||
|
lnbits_exchange_history_refresh_interval_seconds: int = Field(default=300)
|
||||||
|
|
||||||
|
lnbits_exchange_rate_providers: list[ExchangeRateProvider] = Field(
|
||||||
|
default=[
|
||||||
|
ExchangeRateProvider(
|
||||||
|
name="Binance",
|
||||||
|
api_url="https://api.binance.com/api/v3/ticker/price?symbol=BTC{TO}",
|
||||||
|
path="$.price",
|
||||||
|
exclude_to=["czk"],
|
||||||
|
ticker_conversion=["USD:USDT"],
|
||||||
|
),
|
||||||
|
ExchangeRateProvider(
|
||||||
|
name="Blockchain",
|
||||||
|
api_url="https://blockchain.info/frombtc?currency={TO}&value=100000000",
|
||||||
|
path="",
|
||||||
|
exclude_to=[],
|
||||||
|
ticker_conversion=[],
|
||||||
|
),
|
||||||
|
ExchangeRateProvider(
|
||||||
|
name="Exir",
|
||||||
|
api_url="https://api.exir.io/v1/ticker?symbol=btc-{to}",
|
||||||
|
path="$.last",
|
||||||
|
exclude_to=["czk", "eur"],
|
||||||
|
ticker_conversion=["USD:USDT"],
|
||||||
|
),
|
||||||
|
ExchangeRateProvider(
|
||||||
|
name="Bitfinex",
|
||||||
|
api_url="https://api.bitfinex.com/v1/pubticker/btc{to}",
|
||||||
|
path="$.last_price",
|
||||||
|
exclude_to=["czk"],
|
||||||
|
ticker_conversion=[],
|
||||||
|
),
|
||||||
|
ExchangeRateProvider(
|
||||||
|
name="Bitstamp",
|
||||||
|
api_url="https://www.bitstamp.net/api/v2/ticker/btc{to}/",
|
||||||
|
path="$.last",
|
||||||
|
exclude_to=["czk"],
|
||||||
|
ticker_conversion=[],
|
||||||
|
),
|
||||||
|
ExchangeRateProvider(
|
||||||
|
name="Coinbase",
|
||||||
|
api_url="https://api.coinbase.com/v2/exchange-rates?currency=BTC",
|
||||||
|
path="$.data.rates.{TO}",
|
||||||
|
exclude_to=[],
|
||||||
|
ticker_conversion=[],
|
||||||
|
),
|
||||||
|
ExchangeRateProvider(
|
||||||
|
name="CoinMate",
|
||||||
|
api_url="https://coinmate.io/api/ticker?currencyPair=BTC_{TO}",
|
||||||
|
path="$.data.last",
|
||||||
|
exclude_to=[],
|
||||||
|
ticker_conversion=["USD:USDT"],
|
||||||
|
),
|
||||||
|
ExchangeRateProvider(
|
||||||
|
name="Kraken",
|
||||||
|
api_url="https://api.kraken.com/0/public/Ticker?pair=XBT{TO}",
|
||||||
|
path="$.result.XXBTZ{TO}.c[0]",
|
||||||
|
exclude_to=["czk"],
|
||||||
|
ticker_conversion=[],
|
||||||
|
),
|
||||||
|
ExchangeRateProvider(
|
||||||
|
name="yadio",
|
||||||
|
api_url="https://api.yadio.io/exrates/BTC",
|
||||||
|
path="$.BTC.{TO}",
|
||||||
|
exclude_to=[],
|
||||||
|
ticker_conversion=[],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SecuritySettings(LNbitsSettings):
|
class SecuritySettings(LNbitsSettings):
|
||||||
lnbits_rate_limit_no: str = Field(default="200")
|
lnbits_rate_limit_no: str = Field(default="200")
|
||||||
lnbits_rate_limit_unit: str = Field(default="minute")
|
lnbits_rate_limit_unit: str = Field(default="minute")
|
||||||
@@ -594,6 +703,7 @@ class EditableSettings(
|
|||||||
ThemesSettings,
|
ThemesSettings,
|
||||||
OpsSettings,
|
OpsSettings,
|
||||||
FeeSettings,
|
FeeSettings,
|
||||||
|
ExchangeProvidersSettings,
|
||||||
SecuritySettings,
|
SecuritySettings,
|
||||||
FundingSourcesSettings,
|
FundingSourcesSettings,
|
||||||
LightningSettings,
|
LightningSettings,
|
||||||
@@ -698,7 +808,7 @@ class SuperUserSettings(LNbitsSettings):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TransientSettings(InstalledExtensionsSettings):
|
class TransientSettings(InstalledExtensionsSettings, ExchangeHistorySettings):
|
||||||
# Transient Settings:
|
# Transient Settings:
|
||||||
# - are initialized, updated and used at runtime
|
# - are initialized, updated and used at runtime
|
||||||
# - are not read from a file or from the `settings` table
|
# - are not read from a file or from the `settings` table
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+19
-9
File diff suppressed because one or more lines are too long
@@ -143,7 +143,7 @@ window.localisation.br = {
|
|||||||
'Todos os dados da extensão serão permanentemente excluídos. Não há como desfazer essa operação!',
|
'Todos os dados da extensão serão permanentemente excluídos. Não há como desfazer essa operação!',
|
||||||
extension_db_drop_warning:
|
extension_db_drop_warning:
|
||||||
'Você está prestes a remover todos os dados para a extensão. Por favor, digite o nome da extensão para continuar:',
|
'Você está prestes a remover todos os dados para a extensão. Por favor, digite o nome da extensão para continuar:',
|
||||||
extension_min_lnbits_version:
|
extension_required_lnbits_version:
|
||||||
'Esta versão requer no mínimo a versão do LNbits',
|
'Esta versão requer no mínimo a versão do LNbits',
|
||||||
payment_hash: 'Hash de pagamento',
|
payment_hash: 'Hash de pagamento',
|
||||||
fee: 'Taxa',
|
fee: 'Taxa',
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ window.localisation.cn = {
|
|||||||
extension_db_drop_info: '该扩展程序的所有数据将被永久删除。此操作无法撤销!',
|
extension_db_drop_info: '该扩展程序的所有数据将被永久删除。此操作无法撤销!',
|
||||||
extension_db_drop_warning:
|
extension_db_drop_warning:
|
||||||
'您即将删除该扩展的所有数据。请继续输入扩展程序名称以确认操作:',
|
'您即将删除该扩展的所有数据。请继续输入扩展程序名称以确认操作:',
|
||||||
extension_min_lnbits_version: '此版本要求最低的 LNbits 版本为',
|
extension_required_lnbits_version: '此版本要求最低的 LNbits 版本为',
|
||||||
payment_hash: '付款哈希',
|
payment_hash: '付款哈希',
|
||||||
fee: '费',
|
fee: '费',
|
||||||
amount: '金额',
|
amount: '金额',
|
||||||
|
|||||||
@@ -143,7 +143,8 @@ window.localisation.cs = {
|
|||||||
'Všechna data pro rozšíření budou trvale odstraněna. Tuto operaci nelze vrátit zpět!',
|
'Všechna data pro rozšíření budou trvale odstraněna. Tuto operaci nelze vrátit zpět!',
|
||||||
extension_db_drop_warning:
|
extension_db_drop_warning:
|
||||||
'Chystáte se odstranit všechna data pro rozšíření. Prosím, pokračujte zadáním názvu rozšíření:',
|
'Chystáte se odstranit všechna data pro rozšíření. Prosím, pokračujte zadáním názvu rozšíření:',
|
||||||
extension_min_lnbits_version: 'Toto vydání vyžaduje alespoň verzi LNbits',
|
extension_required_lnbits_version:
|
||||||
|
'Toto vydání vyžaduje alespoň verzi LNbits',
|
||||||
payment_hash: 'Hash platby',
|
payment_hash: 'Hash platby',
|
||||||
fee: 'Poplatek',
|
fee: 'Poplatek',
|
||||||
amount: 'Částka',
|
amount: 'Částka',
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ window.localisation.de = {
|
|||||||
'Alle Daten für die Erweiterung werden dauerhaft gelöscht. Es gibt keine Möglichkeit, diesen Vorgang rückgängig zu machen!',
|
'Alle Daten für die Erweiterung werden dauerhaft gelöscht. Es gibt keine Möglichkeit, diesen Vorgang rückgängig zu machen!',
|
||||||
extension_db_drop_warning:
|
extension_db_drop_warning:
|
||||||
'Sie sind dabei, alle Daten für die Erweiterung zu entfernen. Bitte geben Sie den Namen der Erweiterung ein, um fortzufahren:',
|
'Sie sind dabei, alle Daten für die Erweiterung zu entfernen. Bitte geben Sie den Namen der Erweiterung ein, um fortzufahren:',
|
||||||
extension_min_lnbits_version:
|
extension_required_lnbits_version:
|
||||||
'Diese Version erfordert mindestens die LNbits-Version',
|
'Diese Version erfordert mindestens die LNbits-Version',
|
||||||
payment_hash: 'Zahlungs-Hash',
|
payment_hash: 'Zahlungs-Hash',
|
||||||
fee: 'Gebühr',
|
fee: 'Gebühr',
|
||||||
|
|||||||
@@ -26,19 +26,20 @@ window.localisation.en = {
|
|||||||
close_channel: 'Close Channel',
|
close_channel: 'Close Channel',
|
||||||
close: 'Close',
|
close: 'Close',
|
||||||
restart: 'Restart server',
|
restart: 'Restart server',
|
||||||
|
background_image: 'Background Image',
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
save_tooltip: 'Save your changes',
|
save_tooltip: 'Save your changes',
|
||||||
topup: 'Topup',
|
credit_debit: 'Credit / Debit',
|
||||||
topup_wallet: 'Topup a wallet',
|
credit_hint: 'Press Enter to credit/debit wallet (negative values allowed)',
|
||||||
topup_hint: 'Use the wallet ID to topup any wallet',
|
credit_label: '{denomination} to credit/debit',
|
||||||
|
credit_ok:
|
||||||
|
'Success crediting/debiting virtual funds ({amount} sats). Payments depend on actual funds on funding source.',
|
||||||
restart_tooltip: 'Restart the server for changes to take effect',
|
restart_tooltip: 'Restart the server for changes to take effect',
|
||||||
add_funds_tooltip: 'Add funds to a wallet.',
|
add_funds_tooltip: 'Add funds to a wallet.',
|
||||||
reset_defaults: 'Reset to defaults',
|
reset_defaults: 'Reset to defaults',
|
||||||
reset_defaults_tooltip: 'Delete all settings and reset to defaults.',
|
reset_defaults_tooltip: 'Delete all settings and reset to defaults.',
|
||||||
download_backup: 'Download database backup',
|
download_backup: 'Download database backup',
|
||||||
name_your_wallet: 'Name your {name} wallet',
|
name_your_wallet: 'Name your {name} wallet',
|
||||||
wallet_topup_ok:
|
|
||||||
'Success creating virtual funds ({amount} sats). Payments depend on actual funds on funding source.',
|
|
||||||
paste_invoice_label: 'Paste an invoice, payment request or lnurl code *',
|
paste_invoice_label: 'Paste an invoice, payment request or lnurl code *',
|
||||||
lnbits_description:
|
lnbits_description:
|
||||||
'Easy to set up and lightweight, LNbits can run on any Lightning Network funding source and even LNbits itself! You can run LNbits for yourself, or easily offer a custodian solution for others. Each wallet has its own API keys and there is no limit to the number of wallets you can make. Being able to partition funds makes LNbits a useful tool for money management and as a development tool. Extensions add extra functionality to LNbits so you can experiment with a range of cutting-edge technologies on the lightning network. We have made developing extensions as easy as possible, and as a free and open-source project, we encourage people to develop and submit their own.',
|
'Easy to set up and lightweight, LNbits can run on any Lightning Network funding source and even LNbits itself! You can run LNbits for yourself, or easily offer a custodian solution for others. Each wallet has its own API keys and there is no limit to the number of wallets you can make. Being able to partition funds makes LNbits a useful tool for money management and as a development tool. Extensions add extra functionality to LNbits so you can experiment with a range of cutting-edge technologies on the lightning network. We have made developing extensions as easy as possible, and as a free and open-source project, we encourage people to develop and submit their own.',
|
||||||
@@ -71,8 +72,6 @@ window.localisation.en = {
|
|||||||
api_keys_api_docs: 'Node URL, API keys and API docs',
|
api_keys_api_docs: 'Node URL, API keys and API docs',
|
||||||
lnbits_version: 'LNbits version',
|
lnbits_version: 'LNbits version',
|
||||||
runs_on: 'Runs on',
|
runs_on: 'Runs on',
|
||||||
credit_hint: 'Press Enter to credit account',
|
|
||||||
credit_label: '{denomination} to credit',
|
|
||||||
paste: 'Paste',
|
paste: 'Paste',
|
||||||
paste_from_clipboard: 'Paste from clipboard',
|
paste_from_clipboard: 'Paste from clipboard',
|
||||||
paste_request: 'Paste Request',
|
paste_request: 'Paste Request',
|
||||||
@@ -108,6 +107,7 @@ window.localisation.en = {
|
|||||||
'You *must* save your login credentials to be able to access your wallet again. If you lose them, you will lose access to your wallet and funds.\n\nFind your login credentials on your account settings page.\n\nThis service is in BETA. LNbits holds no responsibility for loss of access to funds.',
|
'You *must* save your login credentials to be able to access your wallet again. If you lose them, you will lose access to your wallet and funds.\n\nFind your login credentials on your account settings page.\n\nThis service is in BETA. LNbits holds no responsibility for loss of access to funds.',
|
||||||
no_transactions: 'No transactions made yet',
|
no_transactions: 'No transactions made yet',
|
||||||
manage: 'Manage',
|
manage: 'Manage',
|
||||||
|
exchanges: 'Exchanges',
|
||||||
extensions: 'Extensions',
|
extensions: 'Extensions',
|
||||||
no_extensions: "You don't have any extensions installed :(",
|
no_extensions: "You don't have any extensions installed :(",
|
||||||
created: 'Created',
|
created: 'Created',
|
||||||
@@ -144,7 +144,9 @@ window.localisation.en = {
|
|||||||
'All data for the extension will be permanently deleted. There is no way to undo this operation!',
|
'All data for the extension will be permanently deleted. There is no way to undo this operation!',
|
||||||
extension_db_drop_warning:
|
extension_db_drop_warning:
|
||||||
'You are about to remove all data for the extension. Please type the extension name to continue:',
|
'You are about to remove all data for the extension. Please type the extension name to continue:',
|
||||||
extension_min_lnbits_version: 'This release requires at least LNbits version',
|
extension_required_lnbits_version: 'This release requires LNbits version',
|
||||||
|
min_version: 'Minimum (included)',
|
||||||
|
max_version: 'Maximum (excluded)',
|
||||||
payment_hash: 'Payment Hash',
|
payment_hash: 'Payment Hash',
|
||||||
fee: 'Fee',
|
fee: 'Fee',
|
||||||
amount: 'Amount',
|
amount: 'Amount',
|
||||||
@@ -155,6 +157,7 @@ window.localisation.en = {
|
|||||||
expiry: 'Expiry',
|
expiry: 'Expiry',
|
||||||
webhook: 'Webhook',
|
webhook: 'Webhook',
|
||||||
payment_proof: 'Payment Proof',
|
payment_proof: 'Payment Proof',
|
||||||
|
update: 'Update',
|
||||||
update_available: 'Update {version} available!',
|
update_available: 'Update {version} available!',
|
||||||
latest_update: 'You are on the latest version {version}.',
|
latest_update: 'You are on the latest version {version}.',
|
||||||
notifications: 'Notifications',
|
notifications: 'Notifications',
|
||||||
@@ -252,6 +255,7 @@ window.localisation.en = {
|
|||||||
gradient_background: 'Gradient Background',
|
gradient_background: 'Gradient Background',
|
||||||
language: 'Language',
|
language: 'Language',
|
||||||
color_scheme: 'Color Scheme',
|
color_scheme: 'Color Scheme',
|
||||||
|
admin_settings: 'Admin Settings',
|
||||||
extension_cost: 'This release requires a payment of minimum {cost} sats.',
|
extension_cost: 'This release requires a payment of minimum {cost} sats.',
|
||||||
extension_paid_sats: 'You have already paid {paid_sats} sats.',
|
extension_paid_sats: 'You have already paid {paid_sats} sats.',
|
||||||
release_details_error: 'Cannot get the release details.',
|
release_details_error: 'Cannot get the release details.',
|
||||||
@@ -272,5 +276,6 @@ window.localisation.en = {
|
|||||||
license: 'License',
|
license: 'License',
|
||||||
reset_key: 'Reset Key',
|
reset_key: 'Reset Key',
|
||||||
reset_password: 'Reset Password',
|
reset_password: 'Reset Password',
|
||||||
border_choices: 'Border Choices'
|
border_choices: 'Border Choices',
|
||||||
|
select_all: 'Select All'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ window.localisation.es = {
|
|||||||
'Todos los datos para la extensión se eliminarán permanentemente. ¡No hay manera de deshacer esta operación!',
|
'Todos los datos para la extensión se eliminarán permanentemente. ¡No hay manera de deshacer esta operación!',
|
||||||
extension_db_drop_warning:
|
extension_db_drop_warning:
|
||||||
'Está a punto de eliminar todos los datos para la extensión. Por favor, escriba el nombre de la extensión para continuar:',
|
'Está a punto de eliminar todos los datos para la extensión. Por favor, escriba el nombre de la extensión para continuar:',
|
||||||
extension_min_lnbits_version:
|
extension_required_lnbits_version:
|
||||||
'Esta versión requiere al menos una versión de LNbits',
|
'Esta versión requiere al menos una versión de LNbits',
|
||||||
payment_hash: 'Hash de pago',
|
payment_hash: 'Hash de pago',
|
||||||
fee: 'Cuota',
|
fee: 'Cuota',
|
||||||
|
|||||||
@@ -144,7 +144,8 @@ window.localisation.fi = {
|
|||||||
'Kaikki laajennuksen tallettama tieto poistetaan pysyvästi. Poistoa ei voi jälkikäteen peruuttaa!',
|
'Kaikki laajennuksen tallettama tieto poistetaan pysyvästi. Poistoa ei voi jälkikäteen peruuttaa!',
|
||||||
extension_db_drop_warning:
|
extension_db_drop_warning:
|
||||||
'Olet tuhoamassa laajennuksen tallettamat tiedot. Vahvista poisto kirjoittamalla viivalle seuraavassa näkyvä laajennuksen nimi:',
|
'Olet tuhoamassa laajennuksen tallettamat tiedot. Vahvista poisto kirjoittamalla viivalle seuraavassa näkyvä laajennuksen nimi:',
|
||||||
extension_min_lnbits_version: 'Tämä julkaisu vaatii vähintään LNbits-version',
|
extension_required_lnbits_version:
|
||||||
|
'Tämä julkaisu vaatii vähintään LNbits-version',
|
||||||
payment_hash: 'Maksun tiiviste',
|
payment_hash: 'Maksun tiiviste',
|
||||||
fee: 'Kulu',
|
fee: 'Kulu',
|
||||||
amount: 'Määrä',
|
amount: 'Määrä',
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ window.localisation.fr = {
|
|||||||
"Toutes les données pour l'extension seront supprimées de manière permanente. Il n'est pas possible d'annuler cette opération !",
|
"Toutes les données pour l'extension seront supprimées de manière permanente. Il n'est pas possible d'annuler cette opération !",
|
||||||
extension_db_drop_warning:
|
extension_db_drop_warning:
|
||||||
"Vous êtes sur le point de supprimer toutes les données de l'extension. Veuillez taper le nom de l'extension pour continuer :",
|
"Vous êtes sur le point de supprimer toutes les données de l'extension. Veuillez taper le nom de l'extension pour continuer :",
|
||||||
extension_min_lnbits_version:
|
extension_required_lnbits_version:
|
||||||
'Cette version nécessite au moins LNbits version',
|
'Cette version nécessite au moins LNbits version',
|
||||||
payment_hash: 'Hash de paiement',
|
payment_hash: 'Hash de paiement',
|
||||||
fee: 'Frais',
|
fee: 'Frais',
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ window.localisation.it = {
|
|||||||
"Tutti i dati relativi all'estensione saranno cancellati permanentemente. Non c'è modo di annullare questa operazione!",
|
"Tutti i dati relativi all'estensione saranno cancellati permanentemente. Non c'è modo di annullare questa operazione!",
|
||||||
extension_db_drop_warning:
|
extension_db_drop_warning:
|
||||||
"Stai per rimuovere tutti i dati per l'estensione. Digita il nome dell'estensione per continuare:",
|
"Stai per rimuovere tutti i dati per l'estensione. Digita il nome dell'estensione per continuare:",
|
||||||
extension_min_lnbits_version:
|
extension_required_lnbits_version:
|
||||||
'Questa versione richiede almeno la versione LNbits',
|
'Questa versione richiede almeno la versione LNbits',
|
||||||
payment_hash: 'Hash del pagamento',
|
payment_hash: 'Hash del pagamento',
|
||||||
fee: 'Tariffa',
|
fee: 'Tariffa',
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ window.localisation.jp = {
|
|||||||
'エクステンションのすべてのデータが完全に削除されます。この操作を元に戻す方法はありません!',
|
'エクステンションのすべてのデータが完全に削除されます。この操作を元に戻す方法はありません!',
|
||||||
extension_db_drop_warning:
|
extension_db_drop_warning:
|
||||||
'エクステンションのすべてのデータを削除しようとしています。続行するには、エクステンションの名前を入力してください:',
|
'エクステンションのすべてのデータを削除しようとしています。続行するには、エクステンションの名前を入力してください:',
|
||||||
extension_min_lnbits_version:
|
extension_required_lnbits_version:
|
||||||
'このリリースには少なくとも LNbits バージョンが必要です',
|
'このリリースには少なくとも LNbits バージョンが必要です',
|
||||||
payment_hash: '支払いハッシュ',
|
payment_hash: '支払いハッシュ',
|
||||||
fee: '料金',
|
fee: '料金',
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ window.localisation.kr = {
|
|||||||
'해당 확장 기능의 모든 데이터가 영구적으로 삭제됩니다. 작업 수행 후에는 되돌릴 수 없습니다!',
|
'해당 확장 기능의 모든 데이터가 영구적으로 삭제됩니다. 작업 수행 후에는 되돌릴 수 없습니다!',
|
||||||
extension_db_drop_warning:
|
extension_db_drop_warning:
|
||||||
'해당 확장 기능의 모든 데이터가 영구적으로 삭제될 겁니다. 계속하려면 확장 기능의 이름을 입력해주세요:',
|
'해당 확장 기능의 모든 데이터가 영구적으로 삭제될 겁니다. 계속하려면 확장 기능의 이름을 입력해주세요:',
|
||||||
extension_min_lnbits_version:
|
extension_required_lnbits_version:
|
||||||
'이 배포 버전은 더 높은 버전의 lnbits가 설치되어 있어야 합니다.',
|
'이 배포 버전은 더 높은 버전의 lnbits가 설치되어 있어야 합니다.',
|
||||||
payment_hash: '결제 해쉬값',
|
payment_hash: '결제 해쉬값',
|
||||||
fee: '수수료',
|
fee: '수수료',
|
||||||
|
|||||||
@@ -145,7 +145,8 @@ window.localisation.nl = {
|
|||||||
'Alle gegevens voor de extensie zullen permanent worden verwijderd. Er is geen manier om deze bewerking ongedaan te maken!',
|
'Alle gegevens voor de extensie zullen permanent worden verwijderd. Er is geen manier om deze bewerking ongedaan te maken!',
|
||||||
extension_db_drop_warning:
|
extension_db_drop_warning:
|
||||||
'U staat op het punt alle gegevens voor de extensie te verwijderen. Typ de naam van de extensie om door te gaan:',
|
'U staat op het punt alle gegevens voor de extensie te verwijderen. Typ de naam van de extensie om door te gaan:',
|
||||||
extension_min_lnbits_version: 'Deze release vereist ten minste LNbits-versie',
|
extension_required_lnbits_version:
|
||||||
|
'Deze release vereist ten minste LNbits-versie',
|
||||||
payment_hash: 'Betalings-hash',
|
payment_hash: 'Betalings-hash',
|
||||||
fee: 'Kosten',
|
fee: 'Kosten',
|
||||||
amount: 'Bedrag',
|
amount: 'Bedrag',
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ window.localisation.pi = {
|
|||||||
"All data fer th' extension will be permanently deleted. There be no way to undo this operation!",
|
"All data fer th' extension will be permanently deleted. There be no way to undo this operation!",
|
||||||
extension_db_drop_warning:
|
extension_db_drop_warning:
|
||||||
"Ye be about to scuttle all data fer th' extension. Please scribble th' extension name to continue:",
|
"Ye be about to scuttle all data fer th' extension. Please scribble th' extension name to continue:",
|
||||||
extension_min_lnbits_version:
|
extension_required_lnbits_version:
|
||||||
"This release be needin' at least LNbits version",
|
"This release be needin' at least LNbits version",
|
||||||
payment_hash: 'Payment Hash like a treasure map, arrr',
|
payment_hash: 'Payment Hash like a treasure map, arrr',
|
||||||
fee: 'Fee like a toll to cross a strait, matey',
|
fee: 'Fee like a toll to cross a strait, matey',
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ window.localisation.pl = {
|
|||||||
'Wszystkie dane dla rozszerzenia zostaną trwale usunięte. Nie ma sposobu, aby cofnąć tę operację!',
|
'Wszystkie dane dla rozszerzenia zostaną trwale usunięte. Nie ma sposobu, aby cofnąć tę operację!',
|
||||||
extension_db_drop_warning:
|
extension_db_drop_warning:
|
||||||
'Za chwilę usuniesz wszystkie dane dla rozszerzenia. Proszę wpisz nazwę rozszerzenia, aby kontynuować:',
|
'Za chwilę usuniesz wszystkie dane dla rozszerzenia. Proszę wpisz nazwę rozszerzenia, aby kontynuować:',
|
||||||
extension_min_lnbits_version: 'To wymaga przynajmniej wersji LNbits',
|
extension_required_lnbits_version: 'To wymaga przynajmniej wersji LNbits',
|
||||||
payment_hash: 'Hash Płatności',
|
payment_hash: 'Hash Płatności',
|
||||||
fee: 'Opłata',
|
fee: 'Opłata',
|
||||||
amount: 'Wartość',
|
amount: 'Wartość',
|
||||||
|
|||||||
@@ -144,7 +144,8 @@ window.localisation.pt = {
|
|||||||
'Todos os dados da extensão serão permanentemente excluídos. Não há como desfazer essa operação!',
|
'Todos os dados da extensão serão permanentemente excluídos. Não há como desfazer essa operação!',
|
||||||
extension_db_drop_warning:
|
extension_db_drop_warning:
|
||||||
'Você está prestes a remover todos os dados para a extensão. Por favor, digite o nome da extensão para continuar:',
|
'Você está prestes a remover todos os dados para a extensão. Por favor, digite o nome da extensão para continuar:',
|
||||||
extension_min_lnbits_version: 'Esta versão requer pelo menos a versão LNbits',
|
extension_required_lnbits_version:
|
||||||
|
'Esta versão requer pelo menos a versão LNbits',
|
||||||
payment_hash: 'Hash de pagamento',
|
payment_hash: 'Hash de pagamento',
|
||||||
fee: 'Taxa',
|
fee: 'Taxa',
|
||||||
amount: 'Quantidade',
|
amount: 'Quantidade',
|
||||||
|
|||||||
@@ -143,7 +143,8 @@ window.localisation.sk = {
|
|||||||
'Všetky údaje pre rozšírenie budú trvalo vymazané. Túto operáciu nie je možné vrátiť!',
|
'Všetky údaje pre rozšírenie budú trvalo vymazané. Túto operáciu nie je možné vrátiť!',
|
||||||
extension_db_drop_warning:
|
extension_db_drop_warning:
|
||||||
'Chystáte sa odstrániť všetky údaje pre rozšírenie. Pre pokračovanie prosím napíšte názov rozšírenia:',
|
'Chystáte sa odstrániť všetky údaje pre rozšírenie. Pre pokračovanie prosím napíšte názov rozšírenia:',
|
||||||
extension_min_lnbits_version: 'Toto vydanie vyžaduje aspoň verziu LNbits',
|
extension_required_lnbits_version:
|
||||||
|
'Toto vydanie vyžaduje aspoň verziu LNbits',
|
||||||
payment_hash: 'Hash platby',
|
payment_hash: 'Hash platby',
|
||||||
fee: 'Poplatok',
|
fee: 'Poplatok',
|
||||||
amount: 'Suma',
|
amount: 'Suma',
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ window.localisation.we = {
|
|||||||
"Bydd yr holl ddata ar gyfer yr estyniad yn cael ei ddileu'n barhaol. Does dim ffordd o dadwneud y weithrediad hwn!",
|
"Bydd yr holl ddata ar gyfer yr estyniad yn cael ei ddileu'n barhaol. Does dim ffordd o dadwneud y weithrediad hwn!",
|
||||||
extension_db_drop_warning:
|
extension_db_drop_warning:
|
||||||
"Rydych chi ar fin dileu'r holl ddata ar gyfer yr estyniad. Teipiwch enw'r estyniad i barhau:",
|
"Rydych chi ar fin dileu'r holl ddata ar gyfer yr estyniad. Teipiwch enw'r estyniad i barhau:",
|
||||||
extension_min_lnbits_version:
|
extension_required_lnbits_version:
|
||||||
"Mae'r rhyddhau hwn yn gofyn o leiaf am fersiwn LNbits",
|
"Mae'r rhyddhau hwn yn gofyn o leiaf am fersiwn LNbits",
|
||||||
payment_hash: 'Hais Taliad',
|
payment_hash: 'Hais Taliad',
|
||||||
fee: 'Fee',
|
fee: 'Fee',
|
||||||
|
|||||||
+93
-35
@@ -1,7 +1,7 @@
|
|||||||
window.app = Vue.createApp({
|
window.app = Vue.createApp({
|
||||||
el: '#vue',
|
el: '#vue',
|
||||||
mixins: [window.windowMixin],
|
mixins: [window.windowMixin],
|
||||||
data: function () {
|
data() {
|
||||||
return {
|
return {
|
||||||
user: null,
|
user: null,
|
||||||
hasUsername: false,
|
hasUsername: false,
|
||||||
@@ -12,7 +12,12 @@ window.app = Vue.createApp({
|
|||||||
'confettiFireworks',
|
'confettiFireworks',
|
||||||
'confettiStars'
|
'confettiStars'
|
||||||
],
|
],
|
||||||
borderOptions: ['retro-border', 'hard-border', 'no-border'],
|
borderOptions: [
|
||||||
|
'retro-border',
|
||||||
|
'hard-border',
|
||||||
|
'neon-border',
|
||||||
|
'no-border'
|
||||||
|
],
|
||||||
tab: 'user',
|
tab: 'user',
|
||||||
credentialsData: {
|
credentialsData: {
|
||||||
show: false,
|
show: false,
|
||||||
@@ -25,47 +30,90 @@ window.app = Vue.createApp({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
activeLanguage: function (lang) {
|
activeLanguage(lang) {
|
||||||
return window.i18n.global.locale === lang
|
return window.i18n.global.locale === lang
|
||||||
},
|
},
|
||||||
changeLanguage: function (newValue) {
|
changeLanguage(newValue) {
|
||||||
window.i18n.global.locale = newValue
|
window.i18n.global.locale = newValue
|
||||||
this.$q.localStorage.set('lnbits.lang', newValue)
|
this.$q.localStorage.set('lnbits.lang', newValue)
|
||||||
},
|
},
|
||||||
toggleDarkMode: function () {
|
toggleDarkMode() {
|
||||||
this.$q.dark.toggle()
|
this.$q.dark.toggle()
|
||||||
this.$q.localStorage.set('lnbits.darkMode', this.$q.dark.isActive)
|
this.$q.localStorage.set('lnbits.darkMode', this.$q.dark.isActive)
|
||||||
if (!this.$q.dark.isActive && this.gradientChoice) {
|
if (!this.$q.dark.isActive && this.gradientChoice) {
|
||||||
this.toggleGradient()
|
this.toggleGradient()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
applyGradient: function () {
|
applyGradient() {
|
||||||
darkBgColor = this.$q.localStorage.getItem('lnbits.darkBgColor')
|
this.$q.localStorage.set('lnbits.gradientBg', this.gradientChoice)
|
||||||
primaryColor = this.$q.localStorage.getItem('lnbits.primaryColor')
|
if (this.$q.localStorage.getItem('lnbits.gradientBg')) {
|
||||||
if (this.gradientChoice) {
|
|
||||||
if (!this.$q.dark.isActive) {
|
if (!this.$q.dark.isActive) {
|
||||||
this.toggleDarkMode()
|
this.toggleDarkMode()
|
||||||
}
|
}
|
||||||
|
this.setColors()
|
||||||
|
darkBgColor = this.$q.localStorage.getItem('lnbits.darkBgColor')
|
||||||
|
primaryColor = this.$q.localStorage.getItem('lnbits.primaryColor')
|
||||||
const gradientStyle = `linear-gradient(to bottom right, ${LNbits.utils.hexDarken(String(primaryColor), -70)}, #0a0a0a)`
|
const gradientStyle = `linear-gradient(to bottom right, ${LNbits.utils.hexDarken(String(primaryColor), -70)}, #0a0a0a)`
|
||||||
document.body.style.setProperty(
|
document.body.style.setProperty(
|
||||||
'background-image',
|
'background-image',
|
||||||
gradientStyle,
|
gradientStyle,
|
||||||
'important'
|
'important'
|
||||||
)
|
)
|
||||||
const gradientStyleCards = `background-color: ${LNbits.utils.hexAlpha(String(darkBgColor), 0.4)} !important`
|
const gradientStyleCards = `background-color: ${LNbits.utils.hexAlpha(String(darkBgColor), 0.55)} !important`
|
||||||
const style = document.createElement('style')
|
const style = document.createElement('style')
|
||||||
style.innerHTML =
|
style.innerHTML = `
|
||||||
`body[data-theme="${this.$q.localStorage.getItem('lnbits.theme')}"] .q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark), body.body${this.$q.dark.isActive ? '--dark' : ''} .q-header, body.body${this.$q.dark.isActive ? '--dark' : ''} .q-drawer { ${gradientStyleCards} }` +
|
body[data-theme="${this.$q.localStorage.getItem('lnbits.theme')}"] .q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark),
|
||||||
`body[data-theme="${this.$q.localStorage.getItem('lnbits.theme')}"].body--dark{background: ${LNbits.utils.hexDarken(String(primaryColor), -88)} !important; }` +
|
body.body${this.$q.dark.isActive ? '--dark' : ''} .q-header,
|
||||||
`[data-theme="${this.$q.localStorage.getItem('lnbits.theme')}"] .q-card--dark{background: ${String(darkBgColor)} !important;} }`
|
body.body${this.$q.dark.isActive ? '--dark' : ''} .q-drawer,
|
||||||
|
body.body${this.$q.dark.isActive ? '--dark' : ''} .q-tab-panels {
|
||||||
|
${gradientStyleCards}
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme="${this.$q.localStorage.getItem('lnbits.theme')}"].body--dark {
|
||||||
|
background: ${LNbits.utils.hexDarken(String(primaryColor), -88)} !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="${this.$q.localStorage.getItem('lnbits.theme')}"] .q-card--dark {
|
||||||
|
background: ${String(darkBgColor)} !important;
|
||||||
|
}
|
||||||
|
`
|
||||||
document.head.appendChild(style)
|
document.head.appendChild(style)
|
||||||
this.$q.localStorage.set('lnbits.gradientBg', true)
|
|
||||||
} else {
|
|
||||||
this.$q.localStorage.set('lnbits.gradientBg', false)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
applyBorder: function () {
|
applyBackgroundImage() {
|
||||||
slef = this
|
if (this.backgroundImage) {
|
||||||
|
this.$q.localStorage.set('lnbits.backgroundImage', this.backgroundImage)
|
||||||
|
this.gradientChoice = true
|
||||||
|
this.applyGradient()
|
||||||
|
}
|
||||||
|
let bgImage = this.$q.localStorage.getItem('lnbits.backgroundImage')
|
||||||
|
if (bgImage) {
|
||||||
|
this.backgroundImage = bgImage
|
||||||
|
const style = document.createElement('style')
|
||||||
|
style.innerHTML = `
|
||||||
|
body[data-theme="${this.$q.localStorage.getItem('lnbits.theme')}"]::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: url(${bgImage});
|
||||||
|
background-size: cover;
|
||||||
|
filter: blur(8px);
|
||||||
|
z-index: -1;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme="${this.$q.localStorage.getItem('lnbits.theme')}"] .q-page-container {
|
||||||
|
backdrop-filter: none; /* Ensure the page content is not affected */
|
||||||
|
}`
|
||||||
|
document.head.appendChild(style)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
applyBorder() {
|
||||||
if (this.borderChoice) {
|
if (this.borderChoice) {
|
||||||
this.$q.localStorage.setItem('lnbits.border', this.borderChoice)
|
this.$q.localStorage.setItem('lnbits.border', this.borderChoice)
|
||||||
}
|
}
|
||||||
@@ -75,6 +123,9 @@ window.app = Vue.createApp({
|
|||||||
if (borderStyle == 'hard-border') {
|
if (borderStyle == 'hard-border') {
|
||||||
borderStyleCSS = `box-shadow: 0 0 0 1px rgba(0,0,0,.12), 0 0 0 1px #ffffff47; border: none;`
|
borderStyleCSS = `box-shadow: 0 0 0 1px rgba(0,0,0,.12), 0 0 0 1px #ffffff47; border: none;`
|
||||||
}
|
}
|
||||||
|
if (borderStyle == 'neon-border') {
|
||||||
|
borderStyleCSS = `border: 2px solid ${this.$q.localStorage.getItem('lnbits.primaryColor')}; box-shadow: none;`
|
||||||
|
}
|
||||||
if (borderStyle == 'no-border') {
|
if (borderStyle == 'no-border') {
|
||||||
borderStyleCSS = `box-shadow: none; border: none;`
|
borderStyleCSS = `box-shadow: none; border: none;`
|
||||||
}
|
}
|
||||||
@@ -85,26 +136,32 @@ window.app = Vue.createApp({
|
|||||||
style.innerHTML = `body[data-theme="${this.$q.localStorage.getItem('lnbits.theme')}"] .q-card.q-card--dark, .q-date--dark { ${borderStyleCSS} }`
|
style.innerHTML = `body[data-theme="${this.$q.localStorage.getItem('lnbits.theme')}"] .q-card.q-card--dark, .q-date--dark { ${borderStyleCSS} }`
|
||||||
document.head.appendChild(style)
|
document.head.appendChild(style)
|
||||||
},
|
},
|
||||||
toggleGradient: function () {
|
toggleGradient() {
|
||||||
this.gradientChoice = !this.gradientChoice
|
this.gradientChoice = !this.gradientChoice
|
||||||
this.applyGradient()
|
this.applyGradient()
|
||||||
|
this.$q.localStorage.set('lnbits.backgroundImage', '')
|
||||||
|
this.applyBorder()
|
||||||
if (!this.gradientChoice) {
|
if (!this.gradientChoice) {
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
}
|
}
|
||||||
this.gradientChoice = this.$q.localStorage.getItem('lnbits.gradientBg')
|
this.gradientChoice = this.$q.localStorage.getItem('lnbits.gradientBg')
|
||||||
},
|
},
|
||||||
reactionChoiceFunc: function () {
|
reactionChoiceFunc() {
|
||||||
this.$q.localStorage.set('lnbits.reactions', this.reactionChoice)
|
this.$q.localStorage.set('lnbits.reactions', this.reactionChoice)
|
||||||
},
|
},
|
||||||
|
backgroundImageFunc() {
|
||||||
|
this.$q.localStorage.set('lnbits.backgroundImage', this.backgroundImage)
|
||||||
|
this.applyBackgroundImage()
|
||||||
|
},
|
||||||
changeColor: function (newValue) {
|
changeColor: function (newValue) {
|
||||||
document.body.setAttribute('data-theme', newValue)
|
document.body.setAttribute('data-theme', newValue)
|
||||||
this.$q.localStorage.set('lnbits.theme', newValue)
|
this.$q.localStorage.set('lnbits.theme', newValue)
|
||||||
this.setColors()
|
this.setColors()
|
||||||
if (this.$q.localStorage.getItem('lnbits.gradientBg')) {
|
this.applyGradient()
|
||||||
this.applyGradient()
|
this.applyBackgroundImage()
|
||||||
}
|
this.applyBorder()
|
||||||
},
|
},
|
||||||
updateAccount: async function () {
|
async updateAccount() {
|
||||||
try {
|
try {
|
||||||
const {data} = await LNbits.api.request(
|
const {data} = await LNbits.api.request(
|
||||||
'PUT',
|
'PUT',
|
||||||
@@ -127,7 +184,7 @@ window.app = Vue.createApp({
|
|||||||
LNbits.utils.notifyApiError(e)
|
LNbits.utils.notifyApiError(e)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
disableUpdatePassword: function () {
|
disableUpdatePassword() {
|
||||||
return (
|
return (
|
||||||
!this.credentialsData.newPassword ||
|
!this.credentialsData.newPassword ||
|
||||||
!this.credentialsData.newPasswordRepeat ||
|
!this.credentialsData.newPasswordRepeat ||
|
||||||
@@ -135,7 +192,7 @@ window.app = Vue.createApp({
|
|||||||
this.credentialsData.newPasswordRepeat
|
this.credentialsData.newPasswordRepeat
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
updatePassword: async function () {
|
async updatePassword() {
|
||||||
if (!this.credentialsData.username) {
|
if (!this.credentialsData.username) {
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
@@ -167,7 +224,7 @@ window.app = Vue.createApp({
|
|||||||
LNbits.utils.notifyApiError(e)
|
LNbits.utils.notifyApiError(e)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updatePubkey: async function () {
|
async updatePubkey() {
|
||||||
try {
|
try {
|
||||||
const {data} = await LNbits.api.request(
|
const {data} = await LNbits.api.request(
|
||||||
'PUT',
|
'PUT',
|
||||||
@@ -189,7 +246,7 @@ window.app = Vue.createApp({
|
|||||||
LNbits.utils.notifyApiError(e)
|
LNbits.utils.notifyApiError(e)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
showUpdateCredentials: function () {
|
showUpdateCredentials() {
|
||||||
this.credentialsData = {
|
this.credentialsData = {
|
||||||
show: true,
|
show: true,
|
||||||
oldPassword: null,
|
oldPassword: null,
|
||||||
@@ -200,7 +257,7 @@ window.app = Vue.createApp({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created: async function () {
|
async created() {
|
||||||
try {
|
try {
|
||||||
const {data} = await LNbits.api.getAuthenticatedUser()
|
const {data} = await LNbits.api.getAuthenticatedUser()
|
||||||
this.user = data
|
this.user = data
|
||||||
@@ -209,11 +266,12 @@ window.app = Vue.createApp({
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
LNbits.utils.notifyApiError(e)
|
LNbits.utils.notifyApiError(e)
|
||||||
}
|
}
|
||||||
if (this.$q.localStorage.getItem('lnbits.gradientBg')) {
|
const hash = window.location.hash.replace('#', '')
|
||||||
this.applyGradient()
|
if (hash) {
|
||||||
}
|
this.tab = hash
|
||||||
if (this.$q.localStorage.getItem('lnbits.border')) {
|
|
||||||
this.applyBorder()
|
|
||||||
}
|
}
|
||||||
|
this.applyGradient()
|
||||||
|
this.applyBackgroundImage()
|
||||||
|
this.applyBorder()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
+167
-16
@@ -1,7 +1,7 @@
|
|||||||
window.app = Vue.createApp({
|
window.app = Vue.createApp({
|
||||||
el: '#vue',
|
el: '#vue',
|
||||||
mixins: [windowMixin],
|
mixins: [windowMixin],
|
||||||
data: function () {
|
data() {
|
||||||
return {
|
return {
|
||||||
settings: {},
|
settings: {},
|
||||||
logs: [],
|
logs: [],
|
||||||
@@ -61,13 +61,71 @@ window.app = Vue.createApp({
|
|||||||
'orange'
|
'orange'
|
||||||
],
|
],
|
||||||
tab: 'funding',
|
tab: 'funding',
|
||||||
needsRestart: false
|
needsRestart: false,
|
||||||
|
exchangesTable: {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Exchange Name',
|
||||||
|
field: 'name',
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'api_url',
|
||||||
|
align: 'left',
|
||||||
|
label: 'URL',
|
||||||
|
field: 'api_url',
|
||||||
|
sortable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'path',
|
||||||
|
align: 'left',
|
||||||
|
label: 'JSON Path',
|
||||||
|
field: 'path',
|
||||||
|
sortable: false
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'exclude_to',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Exclude Currencies',
|
||||||
|
field: 'exclude_to',
|
||||||
|
sortable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ticker_conversion',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Ticker Conversion',
|
||||||
|
field: 'ticker_conversion',
|
||||||
|
sortable: false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
pagination: {
|
||||||
|
sortBy: 'name',
|
||||||
|
rowsPerPage: 100,
|
||||||
|
page: 1,
|
||||||
|
rowsNumber: 100
|
||||||
|
},
|
||||||
|
search: null,
|
||||||
|
hideEmpty: true
|
||||||
|
},
|
||||||
|
exchangeData: {
|
||||||
|
selectedProvider: null,
|
||||||
|
showTickerConversion: false,
|
||||||
|
convertFromTicker: null,
|
||||||
|
convertToTicker: null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.getSettings()
|
this.getSettings()
|
||||||
this.getAudit()
|
this.getAudit()
|
||||||
this.balance = +'{{ balance|safe }}'
|
this.balance = +'{{ balance|safe }}'
|
||||||
|
const hash = window.location.hash.replace('#', '')
|
||||||
|
if (hash) {
|
||||||
|
this.tab = hash
|
||||||
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
lnbitsVersion() {
|
lnbitsVersion() {
|
||||||
@@ -247,6 +305,57 @@ window.app = Vue.createApp({
|
|||||||
this.formData.nostr_absolute_request_urls =
|
this.formData.nostr_absolute_request_urls =
|
||||||
this.formData.nostr_absolute_request_urls.filter(b => b !== url)
|
this.formData.nostr_absolute_request_urls.filter(b => b !== url)
|
||||||
},
|
},
|
||||||
|
addExchangeProvider() {
|
||||||
|
this.formData.lnbits_exchange_rate_providers = [
|
||||||
|
{
|
||||||
|
name: '',
|
||||||
|
api_url: '',
|
||||||
|
path: '',
|
||||||
|
exclude_to: []
|
||||||
|
},
|
||||||
|
...this.formData.lnbits_exchange_rate_providers
|
||||||
|
]
|
||||||
|
},
|
||||||
|
removeExchangeProvider(provider) {
|
||||||
|
this.formData.lnbits_exchange_rate_providers =
|
||||||
|
this.formData.lnbits_exchange_rate_providers.filter(p => p !== provider)
|
||||||
|
},
|
||||||
|
removeExchangeTickerConversion(provider, ticker) {
|
||||||
|
provider.ticker_conversion = provider.ticker_conversion.filter(
|
||||||
|
t => t !== ticker
|
||||||
|
)
|
||||||
|
this.touchSettings()
|
||||||
|
},
|
||||||
|
addExchangeTickerConversion() {
|
||||||
|
if (!this.exchangeData.selectedProvider) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.exchangeData.selectedProvider.ticker_conversion.push(
|
||||||
|
`${this.exchangeData.convertFromTicker}:${this.exchangeData.convertToTicker}`
|
||||||
|
)
|
||||||
|
this.touchSettings()
|
||||||
|
this.exchangeData.showTickerConversion = false
|
||||||
|
},
|
||||||
|
showTickerConversionDialog(provider) {
|
||||||
|
this.exchangeData.convertFromTicker = null
|
||||||
|
this.exchangeData.convertToTicker = null
|
||||||
|
this.exchangeData.selectedProvider = provider
|
||||||
|
this.exchangeData.showTickerConversion = true
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultSetting(fieldName) {
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'GET',
|
||||||
|
`/admin/api/v1/settings/default?field_name=${fieldName}`
|
||||||
|
)
|
||||||
|
.then(response => {
|
||||||
|
this.formData[fieldName] = response.data.default_value
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
restartServer() {
|
restartServer() {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request('GET', '/admin/api/v1/restart/')
|
.request('GET', '/admin/api/v1/restart/')
|
||||||
@@ -258,9 +367,7 @@ window.app = Vue.createApp({
|
|||||||
})
|
})
|
||||||
this.needsRestart = false
|
this.needsRestart = false
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(LNbits.utils.notifyApiError)
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
formatDate(date) {
|
formatDate(date) {
|
||||||
return moment(date * 1000).fromNow()
|
return moment(date * 1000).fromNow()
|
||||||
@@ -286,6 +393,14 @@ window.app = Vue.createApp({
|
|||||||
.then(response => {
|
.then(response => {
|
||||||
this.auditData = response.data
|
this.auditData = response.data
|
||||||
})
|
})
|
||||||
|
.catch(LNbits.utils.notifyApiError)
|
||||||
|
},
|
||||||
|
getExchangeRateHistory() {
|
||||||
|
LNbits.api
|
||||||
|
.request('GET', '/api/v1/rate/history', this.g.user.wallets[0].inkey)
|
||||||
|
.then(response => {
|
||||||
|
this.initExchangeChart(response.data)
|
||||||
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
})
|
})
|
||||||
@@ -303,14 +418,13 @@ window.app = Vue.createApp({
|
|||||||
this.formData = {...this.settings}
|
this.formData = {...this.settings}
|
||||||
this.getNotifications()
|
this.getNotifications()
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(LNbits.utils.notifyApiError)
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
updateSettings() {
|
updateSettings() {
|
||||||
let data = _.omit(this.formData, [
|
const data = _.omit(this.formData, [
|
||||||
'is_super_user',
|
'is_super_user',
|
||||||
'lnbits_allowed_funding_sources'
|
'lnbits_allowed_funding_sources',
|
||||||
|
'touch'
|
||||||
])
|
])
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request(
|
.request(
|
||||||
@@ -334,9 +448,7 @@ window.app = Vue.createApp({
|
|||||||
icon: null
|
icon: null
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(LNbits.utils.notifyApiError)
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
deleteSettings() {
|
deleteSettings() {
|
||||||
LNbits.utils
|
LNbits.utils
|
||||||
@@ -353,13 +465,52 @@ window.app = Vue.createApp({
|
|||||||
})
|
})
|
||||||
this.needsRestart = true
|
this.needsRestart = true
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(LNbits.utils.notifyApiError)
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
downloadBackup() {
|
downloadBackup() {
|
||||||
window.open('/admin/api/v1/backup', '_blank')
|
window.open('/admin/api/v1/backup', '_blank')
|
||||||
|
},
|
||||||
|
showExchangeProvidersTab(tabName) {
|
||||||
|
if (tabName === 'exchange_providers') {
|
||||||
|
this.getExchangeRateHistory()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
touchSettings() {
|
||||||
|
this.formData.touch = null
|
||||||
|
},
|
||||||
|
initExchangeChart(data) {
|
||||||
|
const xValues = data.map(d =>
|
||||||
|
Quasar.date.formatDate(new Date(d.timestamp * 1000), 'HH:mm')
|
||||||
|
)
|
||||||
|
const exchanges = [
|
||||||
|
...this.formData.lnbits_exchange_rate_providers,
|
||||||
|
{name: 'LNbits'}
|
||||||
|
]
|
||||||
|
const datasets = exchanges.map(exchange => ({
|
||||||
|
label: exchange.name,
|
||||||
|
data: data.map(d => d.rates[exchange.name]),
|
||||||
|
pointStyle: true,
|
||||||
|
borderWidth: exchange.name === 'LNbits' ? 4 : 1,
|
||||||
|
tension: 0.4
|
||||||
|
}))
|
||||||
|
this.exchangeRatesChart = new Chart(
|
||||||
|
this.$refs.exchangeRatesChart.getContext('2d'),
|
||||||
|
{
|
||||||
|
type: 'line',
|
||||||
|
options: {
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
labels: xValues,
|
||||||
|
datasets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
window.app = Vue.createApp({
|
window.app = Vue.createApp({
|
||||||
el: '#vue',
|
el: '#vue',
|
||||||
mixins: [window.windowMixin],
|
mixins: [window.windowMixin],
|
||||||
data: function () {
|
data() {
|
||||||
return {
|
return {
|
||||||
auditEntries: [],
|
auditEntries: [],
|
||||||
searchData: {
|
searchData: {
|
||||||
@@ -104,6 +104,9 @@ window.app = Vue.createApp({
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
formatDate(dateString) {
|
||||||
|
return LNbits.utils.formatDateString(dateString)
|
||||||
|
},
|
||||||
async fetchAudit(props) {
|
async fetchAudit(props) {
|
||||||
try {
|
try {
|
||||||
const params = LNbits.utils.prepareFilterQuery(this.auditTable, props)
|
const params = LNbits.utils.prepareFilterQuery(this.auditTable, props)
|
||||||
@@ -198,9 +201,6 @@ window.app = Vue.createApp({
|
|||||||
this.auditDetailsDialog.data = JSON.stringify(details, null, 4)
|
this.auditDetailsDialog.data = JSON.stringify(details, null, 4)
|
||||||
this.auditDetailsDialog.show = true
|
this.auditDetailsDialog.show = true
|
||||||
},
|
},
|
||||||
formatDate: function (value) {
|
|
||||||
return Quasar.date.formatDate(new Date(value), 'YYYY-MM-DD HH:mm')
|
|
||||||
},
|
|
||||||
shortify(value) {
|
shortify(value) {
|
||||||
valueLength = (value || '').length
|
valueLength = (value || '').length
|
||||||
if (valueLength <= 10) {
|
if (valueLength <= 10) {
|
||||||
|
|||||||
+145
-97
@@ -1,13 +1,18 @@
|
|||||||
window.LOCALE = 'en'
|
window.LOCALE = 'en'
|
||||||
|
window.dateFormat = 'YYYY-MM-DD HH:mm'
|
||||||
window.i18n = new VueI18n.createI18n({
|
window.i18n = new VueI18n.createI18n({
|
||||||
locale: window.LOCALE,
|
locale: window.LOCALE,
|
||||||
fallbackLocale: window.LOCALE,
|
fallbackLocale: window.LOCALE,
|
||||||
messages: window.localisation
|
messages: window.localisation
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const websocketPrefix =
|
||||||
|
window.location.protocol === 'http:' ? 'ws://' : 'wss://'
|
||||||
|
const websocketUrl = `${websocketPrefix}${window.location.host}/api/v1/ws`
|
||||||
|
|
||||||
window.LNbits = {
|
window.LNbits = {
|
||||||
api: {
|
api: {
|
||||||
request: function (method, url, apiKey, data) {
|
request(method, url, apiKey, data) {
|
||||||
return axios({
|
return axios({
|
||||||
method: method,
|
method: method,
|
||||||
url: url,
|
url: url,
|
||||||
@@ -17,10 +22,10 @@ window.LNbits = {
|
|||||||
data: data
|
data: data
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
getServerHealth: function () {
|
getServerHealth() {
|
||||||
return this.request('get', '/api/v1/health')
|
return this.request('get', '/api/v1/health')
|
||||||
},
|
},
|
||||||
createInvoice: async function (
|
async createInvoice(
|
||||||
wallet,
|
wallet,
|
||||||
amount,
|
amount,
|
||||||
memo,
|
memo,
|
||||||
@@ -35,13 +40,13 @@ window.LNbits = {
|
|||||||
unit: unit
|
unit: unit
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
payInvoice: function (wallet, bolt11) {
|
payInvoice(wallet, bolt11) {
|
||||||
return this.request('post', '/api/v1/payments', wallet.adminkey, {
|
return this.request('post', '/api/v1/payments', wallet.adminkey, {
|
||||||
out: true,
|
out: true,
|
||||||
bolt11: bolt11
|
bolt11: bolt11
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
payLnurl: function (
|
payLnurl(
|
||||||
wallet,
|
wallet,
|
||||||
callback,
|
callback,
|
||||||
description_hash,
|
description_hash,
|
||||||
@@ -59,17 +64,17 @@ window.LNbits = {
|
|||||||
unit
|
unit
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
authLnurl: function (wallet, callback) {
|
authLnurl(wallet, callback) {
|
||||||
return this.request('post', '/api/v1/lnurlauth', wallet.adminkey, {
|
return this.request('post', '/api/v1/lnurlauth', wallet.adminkey, {
|
||||||
callback
|
callback
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
createAccount: function (name) {
|
createAccount(name) {
|
||||||
return this.request('post', '/api/v1/account', null, {
|
return this.request('post', '/api/v1/account', null, {
|
||||||
name: name
|
name: name
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
register: function (username, email, password, password_repeat) {
|
register(username, email, password, password_repeat) {
|
||||||
return axios({
|
return axios({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/api/v1/auth/register',
|
url: '/api/v1/auth/register',
|
||||||
@@ -81,7 +86,7 @@ window.LNbits = {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
reset: function (reset_key, password, password_repeat) {
|
reset(reset_key, password, password_repeat) {
|
||||||
return axios({
|
return axios({
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
url: '/api/v1/auth/reset',
|
url: '/api/v1/auth/reset',
|
||||||
@@ -92,14 +97,14 @@ window.LNbits = {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
login: function (username, password) {
|
login(username, password) {
|
||||||
return axios({
|
return axios({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/api/v1/auth',
|
url: '/api/v1/auth',
|
||||||
data: {username, password}
|
data: {username, password}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
loginByProvider: function (provider, headers, data) {
|
loginByProvider(provider, headers, data) {
|
||||||
return axios({
|
return axios({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: `/api/v1/auth/${provider}`,
|
url: `/api/v1/auth/${provider}`,
|
||||||
@@ -107,38 +112,38 @@ window.LNbits = {
|
|||||||
data
|
data
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
loginUsr: function (usr) {
|
loginUsr(usr) {
|
||||||
return axios({
|
return axios({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/api/v1/auth/usr',
|
url: '/api/v1/auth/usr',
|
||||||
data: {usr}
|
data: {usr}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
logout: function () {
|
logout() {
|
||||||
return axios({
|
return axios({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/api/v1/auth/logout'
|
url: '/api/v1/auth/logout'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
getAuthenticatedUser: function () {
|
getAuthenticatedUser() {
|
||||||
return this.request('get', '/api/v1/auth')
|
return this.request('get', '/api/v1/auth')
|
||||||
},
|
},
|
||||||
getWallet: function (wallet) {
|
getWallet(wallet) {
|
||||||
return this.request('get', '/api/v1/wallet', wallet.inkey)
|
return this.request('get', '/api/v1/wallet', wallet.inkey)
|
||||||
},
|
},
|
||||||
createWallet: function (wallet, name) {
|
createWallet(wallet, name) {
|
||||||
return this.request('post', '/api/v1/wallet', wallet.adminkey, {
|
return this.request('post', '/api/v1/wallet', wallet.adminkey, {
|
||||||
name: name
|
name: name
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
window.location = '/wallet?wal=' + res.data.id
|
window.location = '/wallet?wal=' + res.data.id
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
updateWallet: function (name, wallet) {
|
updateWallet(name, wallet) {
|
||||||
return this.request('patch', '/api/v1/wallet', wallet.adminkey, {
|
return this.request('patch', '/api/v1/wallet', wallet.adminkey, {
|
||||||
name: name
|
name: name
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
deleteWallet: function (wallet) {
|
deleteWallet(wallet) {
|
||||||
return this.request('delete', '/api/v1/wallet', wallet.adminkey).then(
|
return this.request('delete', '/api/v1/wallet', wallet.adminkey).then(
|
||||||
_ => {
|
_ => {
|
||||||
let url = new URL(window.location.href)
|
let url = new URL(window.location.href)
|
||||||
@@ -147,22 +152,22 @@ window.LNbits = {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
getPayments: function (wallet, params) {
|
getPayments(wallet, params) {
|
||||||
return this.request(
|
return this.request(
|
||||||
'get',
|
'get',
|
||||||
'/api/v1/payments/paginated?' + params,
|
'/api/v1/payments/paginated?' + params,
|
||||||
wallet.inkey
|
wallet.inkey
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
getPayment: function (wallet, paymentHash) {
|
getPayment(wallet, paymentHash) {
|
||||||
return this.request(
|
return this.request(
|
||||||
'get',
|
'get',
|
||||||
'/api/v1/payments/' + paymentHash,
|
'/api/v1/payments/' + paymentHash,
|
||||||
wallet.inkey
|
wallet.inkey
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
updateBalance: function (credit, wallet_id) {
|
updateBalance(credit, wallet_id) {
|
||||||
return this.request('PUT', '/users/api/v1/topup', null, {
|
return this.request('PUT', '/users/api/v1/balance', null, {
|
||||||
amount: credit,
|
amount: credit,
|
||||||
id: wallet_id
|
id: wallet_id
|
||||||
})
|
})
|
||||||
@@ -174,46 +179,24 @@ window.LNbits = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
events: {
|
events: {
|
||||||
onInvoicePaid: function (wallet, cb) {
|
onInvoicePaid(wallet, cb) {
|
||||||
let listener = ev => {
|
ws = new WebSocket(`${websocketUrl}/${wallet.inkey}`)
|
||||||
cb(JSON.parse(ev.data))
|
ws.onmessage = ev => {
|
||||||
}
|
const data = JSON.parse(ev.data)
|
||||||
this.listenersCount = this.listenersCount || {[wallet.inkey]: 0}
|
if (data.payment) {
|
||||||
this.listenersCount[wallet.inkey]++
|
cb(data)
|
||||||
|
|
||||||
this.listeners = this.listeners || {}
|
|
||||||
if (!(wallet.inkey in this.listeners)) {
|
|
||||||
this.listeners[wallet.inkey] = new EventSource(
|
|
||||||
'/api/v1/payments/sse?api-key=' + wallet.inkey
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.listeners[wallet.inkey].addEventListener(
|
|
||||||
'payment-received',
|
|
||||||
listener
|
|
||||||
)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
this.listeners[wallet.inkey].removeEventListener(
|
|
||||||
'payment-received',
|
|
||||||
listener
|
|
||||||
)
|
|
||||||
this.listenersCount[wallet.inkey]--
|
|
||||||
|
|
||||||
if (this.listenersCount[wallet.inkey] <= 0) {
|
|
||||||
this.listeners[wallet.inkey].close()
|
|
||||||
delete this.listeners[wallet.inkey]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return ws.onclose
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
map: {
|
map: {
|
||||||
extension: function (data) {
|
extension(data) {
|
||||||
const obj = {...data}
|
const obj = {...data}
|
||||||
obj.url = ['/', obj.code, '/'].join('')
|
obj.url = ['/', obj.code, '/'].join('')
|
||||||
return obj
|
return obj
|
||||||
},
|
},
|
||||||
user: function (data) {
|
user(data) {
|
||||||
const obj = {
|
const obj = {
|
||||||
id: data.id,
|
id: data.id,
|
||||||
admin: data.admin,
|
admin: data.admin,
|
||||||
@@ -224,13 +207,13 @@ window.LNbits = {
|
|||||||
}
|
}
|
||||||
const mapWallet = this.wallet
|
const mapWallet = this.wallet
|
||||||
obj.wallets = obj.wallets
|
obj.wallets = obj.wallets
|
||||||
.map(function (obj) {
|
.map(obj => {
|
||||||
return mapWallet(obj)
|
return mapWallet(obj)
|
||||||
})
|
})
|
||||||
.sort(function (a, b) {
|
.sort((a, b) => {
|
||||||
return a.name.localeCompare(b.name)
|
return a.name.localeCompare(b.name)
|
||||||
})
|
})
|
||||||
obj.walletOptions = obj.wallets.map(function (obj) {
|
obj.walletOptions = obj.wallets.map(obj => {
|
||||||
return {
|
return {
|
||||||
label: [obj.name, ' - ', obj.id].join(''),
|
label: [obj.name, ' - ', obj.id].join(''),
|
||||||
value: obj.id
|
value: obj.id
|
||||||
@@ -238,7 +221,7 @@ window.LNbits = {
|
|||||||
})
|
})
|
||||||
return obj
|
return obj
|
||||||
},
|
},
|
||||||
wallet: function (data) {
|
wallet(data) {
|
||||||
newWallet = {
|
newWallet = {
|
||||||
id: data.id,
|
id: data.id,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
@@ -254,7 +237,7 @@ window.LNbits = {
|
|||||||
newWallet.url = `/wallet?&wal=${data.id}`
|
newWallet.url = `/wallet?&wal=${data.id}`
|
||||||
return newWallet
|
return newWallet
|
||||||
},
|
},
|
||||||
payment: function (data) {
|
payment(data) {
|
||||||
obj = {
|
obj = {
|
||||||
checking_id: data.checking_id,
|
checking_id: data.checking_id,
|
||||||
status: data.status,
|
status: data.status,
|
||||||
@@ -274,11 +257,11 @@ window.LNbits = {
|
|||||||
fiat_currency: data.fiat_currency
|
fiat_currency: data.fiat_currency
|
||||||
}
|
}
|
||||||
|
|
||||||
obj.date = Quasar.date.formatDate(new Date(obj.time), 'YYYY-MM-DD HH:mm')
|
obj.date = Quasar.date.formatDate(new Date(obj.time), window.dateFormat)
|
||||||
obj.dateFrom = moment(obj.date).fromNow()
|
obj.dateFrom = moment(obj.date).fromNow()
|
||||||
obj.expirydate = Quasar.date.formatDate(
|
obj.expirydate = Quasar.date.formatDate(
|
||||||
new Date(obj.expiry),
|
new Date(obj.expiry),
|
||||||
'YYYY-MM-DD HH:mm'
|
window.dateFormat
|
||||||
)
|
)
|
||||||
obj.expirydateFrom = moment(obj.expirydate).fromNow()
|
obj.expirydateFrom = moment(obj.expirydate).fromNow()
|
||||||
obj.msat = obj.amount
|
obj.msat = obj.amount
|
||||||
@@ -300,7 +283,7 @@ window.LNbits = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
utils: {
|
utils: {
|
||||||
confirmDialog: function (msg) {
|
confirmDialog(msg) {
|
||||||
return Quasar.Dialog.create({
|
return Quasar.Dialog.create({
|
||||||
message: msg,
|
message: msg,
|
||||||
ok: {
|
ok: {
|
||||||
@@ -313,7 +296,7 @@ window.LNbits = {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
digestMessage: async function (message) {
|
async digestMessage(message) {
|
||||||
const msgUint8 = new TextEncoder().encode(message)
|
const msgUint8 = new TextEncoder().encode(message)
|
||||||
const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8)
|
const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8)
|
||||||
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
||||||
@@ -322,25 +305,28 @@ window.LNbits = {
|
|||||||
.join('')
|
.join('')
|
||||||
return hashHex
|
return hashHex
|
||||||
},
|
},
|
||||||
formatDate: function (timestamp) {
|
formatDate(timestamp) {
|
||||||
return Quasar.date.formatDate(
|
return Quasar.date.formatDate(
|
||||||
new Date(timestamp * 1000),
|
new Date(timestamp * 1000),
|
||||||
'YYYY-MM-DD HH:mm'
|
window.dateFormat
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
formatCurrency: function (value, currency) {
|
formatDateString(isoDateString) {
|
||||||
|
return Quasar.date.formatDate(new Date(isoDateString), window.dateFormat)
|
||||||
|
},
|
||||||
|
formatCurrency(value, currency) {
|
||||||
return new Intl.NumberFormat(window.LOCALE, {
|
return new Intl.NumberFormat(window.LOCALE, {
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
currency: currency
|
currency: currency
|
||||||
}).format(value)
|
}).format(value)
|
||||||
},
|
},
|
||||||
formatSat: function (value) {
|
formatSat(value) {
|
||||||
return new Intl.NumberFormat(window.LOCALE).format(value)
|
return new Intl.NumberFormat(window.LOCALE).format(value)
|
||||||
},
|
},
|
||||||
formatMsat: function (value) {
|
formatMsat(value) {
|
||||||
return this.formatSat(value / 1000)
|
return this.formatSat(value / 1000)
|
||||||
},
|
},
|
||||||
notifyApiError: function (error) {
|
notifyApiError(error) {
|
||||||
if (!error.response) {
|
if (!error.response) {
|
||||||
return console.error(error)
|
return console.error(error)
|
||||||
}
|
}
|
||||||
@@ -361,12 +347,12 @@ window.LNbits = {
|
|||||||
icon: null
|
icon: null
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
search: function (data, q, field, separator) {
|
search(data, q, field, separator) {
|
||||||
try {
|
try {
|
||||||
const queries = q.toLowerCase().split(separator || ' ')
|
const queries = q.toLowerCase().split(separator || ' ')
|
||||||
return data.filter(function (obj) {
|
return data.filter(obj => {
|
||||||
let matches = 0
|
let matches = 0
|
||||||
_.each(queries, function (q) {
|
_.each(queries, q => {
|
||||||
if (obj[field].indexOf(q) !== -1) matches++
|
if (obj[field].indexOf(q) !== -1) matches++
|
||||||
})
|
})
|
||||||
return matches === queries.length
|
return matches === queries.length
|
||||||
@@ -378,8 +364,9 @@ window.LNbits = {
|
|||||||
prepareFilterQuery(tableConfig, props) {
|
prepareFilterQuery(tableConfig, props) {
|
||||||
if (props) {
|
if (props) {
|
||||||
tableConfig.pagination = props.pagination
|
tableConfig.pagination = props.pagination
|
||||||
|
tableConfig.filter = props.filter
|
||||||
}
|
}
|
||||||
let pagination = tableConfig.pagination
|
const pagination = tableConfig.pagination
|
||||||
tableConfig.loading = true
|
tableConfig.loading = true
|
||||||
const query = {
|
const query = {
|
||||||
limit: pagination.rowsPerPage,
|
limit: pagination.rowsPerPage,
|
||||||
@@ -393,8 +380,8 @@ window.LNbits = {
|
|||||||
}
|
}
|
||||||
return new URLSearchParams(query)
|
return new URLSearchParams(query)
|
||||||
},
|
},
|
||||||
exportCSV: function (columns, data, fileName) {
|
exportCSV(columns, data, fileName) {
|
||||||
const wrapCsvValue = function (val, formatFn) {
|
const wrapCsvValue = (val, formatFn) => {
|
||||||
let formatted = formatFn !== void 0 ? formatFn(val) : val
|
let formatted = formatFn !== void 0 ? formatFn(val) : val
|
||||||
|
|
||||||
formatted =
|
formatted =
|
||||||
@@ -406,14 +393,14 @@ window.LNbits = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const content = [
|
const content = [
|
||||||
columns.map(function (col) {
|
columns.map(col => {
|
||||||
return wrapCsvValue(col.label)
|
return wrapCsvValue(col.label)
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
.concat(
|
.concat(
|
||||||
data.map(function (row) {
|
data.map(row => {
|
||||||
return columns
|
return columns
|
||||||
.map(function (col) {
|
.map(col => {
|
||||||
return wrapCsvValue(
|
return wrapCsvValue(
|
||||||
typeof col.field === 'function'
|
typeof col.field === 'function'
|
||||||
? col.field(row)
|
? col.field(row)
|
||||||
@@ -446,16 +433,16 @@ window.LNbits = {
|
|||||||
converter.setOption('simpleLineBreaks', true)
|
converter.setOption('simpleLineBreaks', true)
|
||||||
return converter.makeHtml(text)
|
return converter.makeHtml(text)
|
||||||
},
|
},
|
||||||
hexToRgb: function (hex) {
|
hexToRgb(hex) {
|
||||||
return Quasar.colors.hexToRgb(hex)
|
return Quasar.colors.hexToRgb(hex)
|
||||||
},
|
},
|
||||||
hexDarken: function (hex, percent) {
|
hexDarken(hex, percent) {
|
||||||
return Quasar.colors.lighten(hex, percent)
|
return Quasar.colors.lighten(hex, percent)
|
||||||
},
|
},
|
||||||
hexAlpha: function (hex, alpha) {
|
hexAlpha(hex, alpha) {
|
||||||
return Quasar.colors.changeAlpha(hex, alpha)
|
return Quasar.colors.changeAlpha(hex, alpha)
|
||||||
},
|
},
|
||||||
getPaletteColor: function (color) {
|
getPaletteColor(color) {
|
||||||
return Quasar.colors.getPaletteColor(color)
|
return Quasar.colors.getPaletteColor(color)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -463,11 +450,13 @@ window.LNbits = {
|
|||||||
|
|
||||||
window.windowMixin = {
|
window.windowMixin = {
|
||||||
i18n: window.i18n,
|
i18n: window.i18n,
|
||||||
data: function () {
|
data() {
|
||||||
return {
|
return {
|
||||||
toggleSubs: true,
|
toggleSubs: true,
|
||||||
|
walletFlip: true,
|
||||||
reactionChoice: 'confettiBothSides',
|
reactionChoice: 'confettiBothSides',
|
||||||
borderChoice: '',
|
borderChoice: '',
|
||||||
|
backgroundImage: '',
|
||||||
gradientChoice:
|
gradientChoice:
|
||||||
this.$q.localStorage.getItem('lnbits.gradientBg') || false,
|
this.$q.localStorage.getItem('lnbits.gradientBg') || false,
|
||||||
isUserAuthorized: false,
|
isUserAuthorized: false,
|
||||||
@@ -485,11 +474,22 @@ window.windowMixin = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
changeColor: function (newValue) {
|
flipWallets(smallScreen) {
|
||||||
|
this.walletFlip = !this.walletFlip
|
||||||
|
if (this.walletFlip && smallScreen) {
|
||||||
|
this.g.visibleDrawer = false
|
||||||
|
}
|
||||||
|
this.$q.localStorage.set('lnbits.walletFlip', this.walletFlip)
|
||||||
|
},
|
||||||
|
simpleMobile() {
|
||||||
|
this.mobileSimple = !this.mobileSimple
|
||||||
|
this.$q.localStorage.set('lnbits.mobileSimple', this.mobileSimple)
|
||||||
|
},
|
||||||
|
changeColor(newValue) {
|
||||||
document.body.setAttribute('data-theme', newValue)
|
document.body.setAttribute('data-theme', newValue)
|
||||||
this.$q.localStorage.set('lnbits.theme', newValue)
|
this.$q.localStorage.set('lnbits.theme', newValue)
|
||||||
},
|
},
|
||||||
applyGradient: function () {
|
applyGradient() {
|
||||||
if (this.$q.localStorage.getItem('lnbits.gradientBg')) {
|
if (this.$q.localStorage.getItem('lnbits.gradientBg')) {
|
||||||
this.setColors()
|
this.setColors()
|
||||||
darkBgColor = this.$q.localStorage.getItem('lnbits.darkBgColor')
|
darkBgColor = this.$q.localStorage.getItem('lnbits.darkBgColor')
|
||||||
@@ -500,16 +500,58 @@ window.windowMixin = {
|
|||||||
gradientStyle,
|
gradientStyle,
|
||||||
'important'
|
'important'
|
||||||
)
|
)
|
||||||
const gradientStyleCards = `background-color: ${LNbits.utils.hexAlpha(String(darkBgColor), 0.4)} !important`
|
const gradientStyleCards = `background-color: ${LNbits.utils.hexAlpha(String(darkBgColor), 0.55)} !important`
|
||||||
const style = document.createElement('style')
|
const style = document.createElement('style')
|
||||||
style.innerHTML =
|
style.innerHTML = `
|
||||||
`body[data-theme="${this.$q.localStorage.getItem('lnbits.theme')}"] .q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark), body.body${this.$q.dark.isActive ? '--dark' : ''} .q-header, body.body${this.$q.dark.isActive ? '--dark' : ''} .q-drawer { ${gradientStyleCards} }` +
|
body[data-theme="${this.$q.localStorage.getItem('lnbits.theme')}"] .q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark),
|
||||||
`body[data-theme="${this.$q.localStorage.getItem('lnbits.theme')}"].body--dark{background: ${LNbits.utils.hexDarken(String(primaryColor), -88)} !important; }` +
|
body.body${this.$q.dark.isActive ? '--dark' : ''} .q-header,
|
||||||
`[data-theme="${this.$q.localStorage.getItem('lnbits.theme')}"] .q-card--dark{background: ${String(darkBgColor)} !important;} }`
|
body.body${this.$q.dark.isActive ? '--dark' : ''} .q-drawer,
|
||||||
|
body.body${this.$q.dark.isActive ? '--dark' : ''} .q-tab-panels {
|
||||||
|
${gradientStyleCards}
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme="${this.$q.localStorage.getItem('lnbits.theme')}"].body--dark {
|
||||||
|
background: ${LNbits.utils.hexDarken(String(primaryColor), -88)} !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="${this.$q.localStorage.getItem('lnbits.theme')}"] .q-card--dark {
|
||||||
|
background: ${String(darkBgColor)} !important;
|
||||||
|
}
|
||||||
|
`
|
||||||
document.head.appendChild(style)
|
document.head.appendChild(style)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
applyBorder: function () {
|
applyBackgroundImage() {
|
||||||
|
if (this.backgroundImage) {
|
||||||
|
this.$q.localStorage.set('lnbits.backgroundImage', this.backgroundImage)
|
||||||
|
}
|
||||||
|
let bgImage = this.$q.localStorage.getItem('lnbits.backgroundImage')
|
||||||
|
if (bgImage) {
|
||||||
|
const style = document.createElement('style')
|
||||||
|
style.innerHTML = `
|
||||||
|
body[data-theme="${this.$q.localStorage.getItem('lnbits.theme')}"]::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: url(${bgImage});
|
||||||
|
background-size: cover;
|
||||||
|
filter: blur(8px);
|
||||||
|
z-index: -1;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme="${this.$q.localStorage.getItem('lnbits.theme')}"] .q-page-container {
|
||||||
|
backdrop-filter: none; /* Ensure the page content is not affected */
|
||||||
|
}`
|
||||||
|
document.head.appendChild(style)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
applyBorder() {
|
||||||
if (this.borderChoice) {
|
if (this.borderChoice) {
|
||||||
this.$q.localStorage.setItem('lnbits.border', this.borderChoice)
|
this.$q.localStorage.setItem('lnbits.border', this.borderChoice)
|
||||||
}
|
}
|
||||||
@@ -523,6 +565,9 @@ window.windowMixin = {
|
|||||||
if (borderStyle == 'hard-border') {
|
if (borderStyle == 'hard-border') {
|
||||||
borderStyleCSS = `box-shadow: 0 0 0 1px rgba(0,0,0,.12), 0 0 0 1px #ffffff47; border: none;`
|
borderStyleCSS = `box-shadow: 0 0 0 1px rgba(0,0,0,.12), 0 0 0 1px #ffffff47; border: none;`
|
||||||
}
|
}
|
||||||
|
if (borderStyle == 'neon-border') {
|
||||||
|
borderStyleCSS = `border: 2px solid ${this.$q.localStorage.getItem('lnbits.primaryColor')}; box-shadow: none;`
|
||||||
|
}
|
||||||
if (borderStyle == 'no-border') {
|
if (borderStyle == 'no-border') {
|
||||||
borderStyleCSS = `box-shadow: none; border: none;`
|
borderStyleCSS = `box-shadow: none; border: none;`
|
||||||
}
|
}
|
||||||
@@ -533,7 +578,7 @@ window.windowMixin = {
|
|||||||
style.innerHTML = `body[data-theme="${this.$q.localStorage.getItem('lnbits.theme')}"] .q-card.q-card--dark, .q-date--dark { ${borderStyleCSS} }`
|
style.innerHTML = `body[data-theme="${this.$q.localStorage.getItem('lnbits.theme')}"] .q-card.q-card--dark, .q-date--dark { ${borderStyleCSS} }`
|
||||||
document.head.appendChild(style)
|
document.head.appendChild(style)
|
||||||
},
|
},
|
||||||
setColors: function () {
|
setColors() {
|
||||||
this.$q.localStorage.set(
|
this.$q.localStorage.set(
|
||||||
'lnbits.primaryColor',
|
'lnbits.primaryColor',
|
||||||
LNbits.utils.getPaletteColor('primary')
|
LNbits.utils.getPaletteColor('primary')
|
||||||
@@ -547,15 +592,15 @@ window.windowMixin = {
|
|||||||
LNbits.utils.getPaletteColor('dark')
|
LNbits.utils.getPaletteColor('dark')
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
copyText: function (text, message, position) {
|
copyText(text, message, position) {
|
||||||
Quasar.copyToClipboard(text).then(function () {
|
Quasar.copyToClipboard(text).then(() => {
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
message: message || 'Copied to clipboard!',
|
message: message || 'Copied to clipboard!',
|
||||||
position: position || 'bottom'
|
position: position || 'bottom'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
checkUsrInUrl: async function () {
|
async checkUsrInUrl() {
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams(window.location.search)
|
const params = new URLSearchParams(window.location.search)
|
||||||
const usr = params.get('usr')
|
const usr = params.get('usr')
|
||||||
@@ -581,7 +626,7 @@ window.windowMixin = {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
logout: async function () {
|
async logout() {
|
||||||
LNbits.utils
|
LNbits.utils
|
||||||
.confirmDialog(
|
.confirmDialog(
|
||||||
'Do you really want to logout?' +
|
'Do you really want to logout?' +
|
||||||
@@ -647,7 +692,7 @@ window.windowMixin = {
|
|||||||
this.setColors()
|
this.setColors()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created: async function () {
|
async created() {
|
||||||
if (
|
if (
|
||||||
this.$q.localStorage.getItem('lnbits.darkMode') == true ||
|
this.$q.localStorage.getItem('lnbits.darkMode') == true ||
|
||||||
this.$q.localStorage.getItem('lnbits.darkMode') == false
|
this.$q.localStorage.getItem('lnbits.darkMode') == false
|
||||||
@@ -699,6 +744,7 @@ window.windowMixin = {
|
|||||||
|
|
||||||
this.applyGradient()
|
this.applyGradient()
|
||||||
this.applyBorder()
|
this.applyBorder()
|
||||||
|
this.applyBackgroundImage()
|
||||||
|
|
||||||
if (window.user) {
|
if (window.user) {
|
||||||
this.g.user = Object.freeze(window.LNbits.map.user(window.user))
|
this.g.user = Object.freeze(window.LNbits.map.user(window.user))
|
||||||
@@ -713,10 +759,12 @@ window.windowMixin = {
|
|||||||
}
|
}
|
||||||
await this.checkUsrInUrl()
|
await this.checkUsrInUrl()
|
||||||
this.themeParams()
|
this.themeParams()
|
||||||
|
this.walletFlip = this.$q.localStorage.getItem('lnbits.walletFlip')
|
||||||
|
this.mobileSimple = this.$q.localStorage.getItem('lnbits.mobileSimple')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.decryptLnurlPayAES = function (success_action, preimage) {
|
window.decryptLnurlPayAES = (success_action, preimage) => {
|
||||||
let keyb = new Uint8Array(
|
let keyb = new Uint8Array(
|
||||||
preimage.match(/[\da-f]{2}/gi).map(h => parseInt(h, 16))
|
preimage.match(/[\da-f]{2}/gi).map(h => parseInt(h, 16))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ window.app.component('lnbits-fsat', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
fsat: function () {
|
fsat() {
|
||||||
return LNbits.utils.formatSat(this.amount)
|
return LNbits.utils.formatSat(this.amount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ window.app.component('lnbits-fsat', {
|
|||||||
window.app.component('lnbits-wallet-list', {
|
window.app.component('lnbits-wallet-list', {
|
||||||
template: '#lnbits-wallet-list',
|
template: '#lnbits-wallet-list',
|
||||||
props: ['balance'],
|
props: ['balance'],
|
||||||
data: function () {
|
data() {
|
||||||
return {
|
return {
|
||||||
user: null,
|
user: null,
|
||||||
activeWallet: null,
|
activeWallet: null,
|
||||||
@@ -35,9 +35,9 @@ window.app.component('lnbits-wallet-list', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
wallets: function () {
|
wallets() {
|
||||||
var bal = this.balance
|
const bal = this.balance
|
||||||
return this.user.wallets.map(function (obj) {
|
return this.user.wallets.map(obj => {
|
||||||
obj.live_fsat =
|
obj.live_fsat =
|
||||||
bal.length && bal[0] === obj.id
|
bal.length && bal[0] === obj.id
|
||||||
? LNbits.utils.formatSat(bal[1])
|
? LNbits.utils.formatSat(bal[1])
|
||||||
@@ -47,11 +47,11 @@ window.app.component('lnbits-wallet-list', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
createWallet: function () {
|
createWallet() {
|
||||||
LNbits.api.createWallet(this.user.wallets[0], this.walletName)
|
LNbits.api.createWallet(this.user.wallets[0], this.walletName)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created: function () {
|
created() {
|
||||||
if (window.user) {
|
if (window.user) {
|
||||||
this.user = LNbits.map.user(window.user)
|
this.user = LNbits.map.user(window.user)
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,7 @@ window.app.component('lnbits-wallet-list', {
|
|||||||
|
|
||||||
window.app.component('lnbits-extension-list', {
|
window.app.component('lnbits-extension-list', {
|
||||||
template: '#lnbits-extension-list',
|
template: '#lnbits-extension-list',
|
||||||
data: function () {
|
data() {
|
||||||
return {
|
return {
|
||||||
extensions: [],
|
extensions: [],
|
||||||
user: null,
|
user: null,
|
||||||
@@ -78,17 +78,17 @@ window.app.component('lnbits-extension-list', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateUserExtensions: function (filterBy) {
|
updateUserExtensions(filterBy) {
|
||||||
if (!this.user) return []
|
if (!this.user) return []
|
||||||
|
|
||||||
const path = window.location.pathname
|
const path = window.location.pathname
|
||||||
const userExtensions = this.user.extensions
|
const userExtensions = this.user.extensions
|
||||||
|
|
||||||
return this.extensions
|
return this.extensions
|
||||||
.filter(function (o) {
|
.filter(o => {
|
||||||
return userExtensions.indexOf(o.code) !== -1
|
return userExtensions.indexOf(o.code) !== -1
|
||||||
})
|
})
|
||||||
.filter(function (o) {
|
.filter(o => {
|
||||||
if (!filterBy) return true
|
if (!filterBy) return true
|
||||||
return (
|
return (
|
||||||
`${o.code} ${o.name} ${o.short_description} ${o.url}`
|
`${o.code} ${o.name} ${o.short_description} ${o.url}`
|
||||||
@@ -96,13 +96,13 @@ window.app.component('lnbits-extension-list', {
|
|||||||
.indexOf(filterBy.toLocaleLowerCase()) !== -1
|
.indexOf(filterBy.toLocaleLowerCase()) !== -1
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.map(function (obj) {
|
.map(obj => {
|
||||||
obj.isActive = path.startsWith(obj.url)
|
obj.isActive = path.startsWith(obj.url)
|
||||||
return obj
|
return obj
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created: async function () {
|
async created() {
|
||||||
if (window.user) {
|
if (window.user) {
|
||||||
this.user = LNbits.map.user(window.user)
|
this.user = LNbits.map.user(window.user)
|
||||||
}
|
}
|
||||||
@@ -110,10 +110,10 @@ window.app.component('lnbits-extension-list', {
|
|||||||
try {
|
try {
|
||||||
const {data} = await LNbits.api.request('GET', '/api/v1/extension')
|
const {data} = await LNbits.api.request('GET', '/api/v1/extension')
|
||||||
this.extensions = data
|
this.extensions = data
|
||||||
.map(function (data) {
|
.map(data => {
|
||||||
return LNbits.map.extension(data)
|
return LNbits.map.extension(data)
|
||||||
})
|
})
|
||||||
.sort(function (a, b) {
|
.sort((a, b) => {
|
||||||
return a.name.localeCompare(b.name)
|
return a.name.localeCompare(b.name)
|
||||||
})
|
})
|
||||||
this.userExtensions = this.updateUserExtensions()
|
this.userExtensions = this.updateUserExtensions()
|
||||||
@@ -127,7 +127,7 @@ window.app.component('lnbits-manage', {
|
|||||||
template: '#lnbits-manage',
|
template: '#lnbits-manage',
|
||||||
props: ['showAdmin', 'showNode', 'showExtensions', 'showUsers', 'showAudit'],
|
props: ['showAdmin', 'showNode', 'showExtensions', 'showUsers', 'showAudit'],
|
||||||
methods: {
|
methods: {
|
||||||
isActive: function (path) {
|
isActive(path) {
|
||||||
return window.location.pathname === path
|
return window.location.pathname === path
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -148,7 +148,7 @@ window.app.component('lnbits-payment-details', {
|
|||||||
template: '#lnbits-payment-details',
|
template: '#lnbits-payment-details',
|
||||||
props: ['payment'],
|
props: ['payment'],
|
||||||
mixins: [window.windowMixin],
|
mixins: [window.windowMixin],
|
||||||
data: function () {
|
data() {
|
||||||
return {
|
return {
|
||||||
LNBITS_DENOMINATION: LNBITS_DENOMINATION
|
LNBITS_DENOMINATION: LNBITS_DENOMINATION
|
||||||
}
|
}
|
||||||
@@ -203,7 +203,7 @@ window.app.component('lnbits-lnurlpay-success-action', {
|
|||||||
decryptedValue: this.success_action.ciphertext
|
decryptedValue: this.success_action.ciphertext
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted: function () {
|
mounted() {
|
||||||
if (this.success_action.tag !== 'aes') return null
|
if (this.success_action.tag !== 'aes') return null
|
||||||
decryptLnurlPayAES(this.success_action, this.payment.preimage).then(
|
decryptLnurlPayAES(this.success_action, this.payment.preimage).then(
|
||||||
value => {
|
value => {
|
||||||
@@ -302,8 +302,6 @@ window.app.component('lnbits-notifications-btn', {
|
|||||||
return subscribedUsers.includes(user)
|
return subscribedUsers.includes(user)
|
||||||
},
|
},
|
||||||
subscribe() {
|
subscribe() {
|
||||||
var self = this
|
|
||||||
|
|
||||||
// catch clicks from disabled type='a' button (https://github.com/quasarframework/quasar/issues/9258)
|
// catch clicks from disabled type='a' button (https://github.com/quasarframework/quasar/issues/9258)
|
||||||
if (!this.isSupported || this.isPermissionDenied) {
|
if (!this.isSupported || this.isPermissionDenied) {
|
||||||
return
|
return
|
||||||
@@ -315,56 +313,48 @@ window.app.component('lnbits-notifications-btn', {
|
|||||||
this.isPermissionGranted = permission === 'granted'
|
this.isPermissionGranted = permission === 'granted'
|
||||||
this.isPermissionDenied = permission === 'denied'
|
this.isPermissionDenied = permission === 'denied'
|
||||||
})
|
})
|
||||||
.catch(function (e) {
|
.catch(console.log)
|
||||||
console.log(e)
|
|
||||||
})
|
|
||||||
|
|
||||||
// create push subscription
|
// create push subscription
|
||||||
navigator.serviceWorker.ready.then(registration => {
|
navigator.serviceWorker.ready.then(registration => {
|
||||||
navigator.serviceWorker.getRegistration().then(registration => {
|
navigator.serviceWorker.getRegistration().then(registration => {
|
||||||
registration.pushManager
|
registration.pushManager
|
||||||
.getSubscription()
|
.getSubscription()
|
||||||
.then(function (subscription) {
|
.then(subscription => {
|
||||||
if (
|
if (
|
||||||
subscription === null ||
|
subscription === null ||
|
||||||
!self.isUserSubscribed(self.g.user.id)
|
!this.isUserSubscribed(this.g.user.id)
|
||||||
) {
|
) {
|
||||||
const applicationServerKey = self.urlB64ToUint8Array(
|
const applicationServerKey = this.urlB64ToUint8Array(
|
||||||
self.pubkey
|
this.pubkey
|
||||||
)
|
)
|
||||||
const options = {applicationServerKey, userVisibleOnly: true}
|
const options = {applicationServerKey, userVisibleOnly: true}
|
||||||
|
|
||||||
registration.pushManager
|
registration.pushManager
|
||||||
.subscribe(options)
|
.subscribe(options)
|
||||||
.then(function (subscription) {
|
.then(subscription => {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request(
|
.request(
|
||||||
'POST',
|
'POST',
|
||||||
'/api/v1/webpush',
|
'/api/v1/webpush',
|
||||||
self.g.user.wallets[0].adminkey,
|
this.g.user.wallets[0].adminkey,
|
||||||
{
|
{
|
||||||
subscription: JSON.stringify(subscription)
|
subscription: JSON.stringify(subscription)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.then(function (response) {
|
.then(response => {
|
||||||
self.saveUserSubscribed(response.data.user)
|
this.saveUserSubscribed(response.data.user)
|
||||||
self.isSubscribed = true
|
this.isSubscribed = true
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
})
|
||||||
|
.catch(LNbits.utils.notifyApiError)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(function (e) {
|
.catch(console.log)
|
||||||
console.log(e)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
unsubscribe() {
|
unsubscribe() {
|
||||||
var self = this
|
|
||||||
|
|
||||||
navigator.serviceWorker.ready
|
navigator.serviceWorker.ready
|
||||||
.then(registration => {
|
.then(registration => {
|
||||||
registration.pushManager.getSubscription().then(subscription => {
|
registration.pushManager.getSubscription().then(subscription => {
|
||||||
@@ -373,23 +363,19 @@ window.app.component('lnbits-notifications-btn', {
|
|||||||
.request(
|
.request(
|
||||||
'DELETE',
|
'DELETE',
|
||||||
'/api/v1/webpush?endpoint=' + btoa(subscription.endpoint),
|
'/api/v1/webpush?endpoint=' + btoa(subscription.endpoint),
|
||||||
self.g.user.wallets[0].adminkey
|
this.g.user.wallets[0].adminkey
|
||||||
)
|
)
|
||||||
.then(function () {
|
.then(() => {
|
||||||
self.removeUserSubscribed(self.g.user.id)
|
this.removeUserSubscribed(this.g.user.id)
|
||||||
self.isSubscribed = false
|
this.isSubscribed = false
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
})
|
||||||
|
.catch(LNbits.utils.notifyApiError)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch(function (e) {
|
.catch(console.log)
|
||||||
console.log(e)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
checkSupported: function () {
|
checkSupported() {
|
||||||
let https = window.location.protocol === 'https:'
|
let https = window.location.protocol === 'https:'
|
||||||
let serviceWorkerApi = 'serviceWorker' in navigator
|
let serviceWorkerApi = 'serviceWorker' in navigator
|
||||||
let notificationApi = 'Notification' in window
|
let notificationApi = 'Notification' in window
|
||||||
@@ -411,22 +397,18 @@ window.app.component('lnbits-notifications-btn', {
|
|||||||
|
|
||||||
return this.isSupported
|
return this.isSupported
|
||||||
},
|
},
|
||||||
updateSubscriptionStatus: async function () {
|
async updateSubscriptionStatus() {
|
||||||
var self = this
|
|
||||||
|
|
||||||
await navigator.serviceWorker.ready
|
await navigator.serviceWorker.ready
|
||||||
.then(registration => {
|
.then(registration => {
|
||||||
registration.pushManager.getSubscription().then(subscription => {
|
registration.pushManager.getSubscription().then(subscription => {
|
||||||
self.isSubscribed =
|
this.isSubscribed =
|
||||||
!!subscription && self.isUserSubscribed(self.g.user.id)
|
!!subscription && this.isUserSubscribed(this.g.user.id)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch(function (e) {
|
.catch(console.log)
|
||||||
console.log(e)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created: function () {
|
created() {
|
||||||
this.isPermissionDenied = Notification.permission === 'denied'
|
this.isPermissionDenied = Notification.permission === 'denied'
|
||||||
|
|
||||||
if (this.checkSupported()) {
|
if (this.checkSupported()) {
|
||||||
@@ -502,7 +484,7 @@ window.app.component('lnbits-dynamic-chips', {
|
|||||||
window.app.component('lnbits-update-balance', {
|
window.app.component('lnbits-update-balance', {
|
||||||
template: '#lnbits-update-balance',
|
template: '#lnbits-update-balance',
|
||||||
mixins: [window.windowMixin],
|
mixins: [window.windowMixin],
|
||||||
props: ['wallet_id', 'callback'],
|
props: ['wallet_id', 'credit-value', 'small_btn'],
|
||||||
computed: {
|
computed: {
|
||||||
denomination() {
|
denomination() {
|
||||||
return LNBITS_DENOMINATION
|
return LNBITS_DENOMINATION
|
||||||
@@ -511,39 +493,36 @@ window.app.component('lnbits-update-balance', {
|
|||||||
return user.super_user
|
return user.super_user
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data: function () {
|
data() {
|
||||||
return {
|
return {
|
||||||
credit: 0
|
credit: 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
credit(val) {
|
||||||
|
this.updateBalance(val)
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateBalance: function (credit) {
|
updateBalance(credit) {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.updateBalance(credit, this.wallet_id)
|
.updateBalance(credit, this.wallet_id)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
if (res.data.status !== 'Success') {
|
if (res.data.success !== true) {
|
||||||
throw new Error(res.data)
|
throw new Error(res.data)
|
||||||
}
|
}
|
||||||
this.callback({
|
|
||||||
success: true,
|
|
||||||
credit: parseInt(credit),
|
|
||||||
wallet_id: this.wallet_id
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.then(_ => {
|
|
||||||
credit = parseInt(credit)
|
credit = parseInt(credit)
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: this.$t('wallet_topup_ok', {
|
message: this.$t('credit_ok', {
|
||||||
amount: credit
|
amount: credit
|
||||||
}),
|
}),
|
||||||
icon: null
|
icon: null
|
||||||
})
|
})
|
||||||
|
this.$emit('credit-value', credit)
|
||||||
return credit
|
return credit
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(LNbits.utils.notifyApiError)
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ window.app.component('lnbits-extension-settings-form', {
|
|||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getSettings: async function () {
|
async getSettings() {
|
||||||
try {
|
try {
|
||||||
const {data} = await LNbits.api.request(
|
const {data} = await LNbits.api.request(
|
||||||
'GET',
|
'GET',
|
||||||
@@ -34,7 +34,7 @@ window.app.component('lnbits-extension-settings-form', {
|
|||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
resetSettings: async function () {
|
async resetSettings() {
|
||||||
LNbits.utils
|
LNbits.utils
|
||||||
.confirmDialog('Are you sure you want to reset the settings?')
|
.confirmDialog('Are you sure you want to reset the settings?')
|
||||||
.onOk(async () => {
|
.onOk(async () => {
|
||||||
@@ -47,10 +47,10 @@ window.app.component('lnbits-extension-settings-form', {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created: async function () {
|
async created() {
|
||||||
await this.getSettings()
|
await this.getSettings()
|
||||||
},
|
},
|
||||||
data: function () {
|
data() {
|
||||||
return {
|
return {
|
||||||
settings: undefined
|
settings: undefined
|
||||||
}
|
}
|
||||||
@@ -61,7 +61,7 @@ window.app.component('lnbits-extension-settings-btn-dialog', {
|
|||||||
template: '#lnbits-extension-settings-btn-dialog',
|
template: '#lnbits-extension-settings-btn-dialog',
|
||||||
name: 'lnbits-extension-settings-btn-dialog',
|
name: 'lnbits-extension-settings-btn-dialog',
|
||||||
props: ['options', 'adminkey', 'endpoint'],
|
props: ['options', 'adminkey', 'endpoint'],
|
||||||
data: function () {
|
data() {
|
||||||
return {
|
return {
|
||||||
show: false
|
show: false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ window.app.component('payment-chart', {
|
|||||||
name: 'payment-chart',
|
name: 'payment-chart',
|
||||||
props: ['wallet'],
|
props: ['wallet'],
|
||||||
mixins: [window.windowMixin],
|
mixins: [window.windowMixin],
|
||||||
data: function () {
|
data() {
|
||||||
return {
|
return {
|
||||||
paymentsChart: {
|
paymentsChart: {
|
||||||
show: false,
|
show: false,
|
||||||
@@ -105,7 +105,7 @@ window.app.component('payment-chart', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
showChart: function () {
|
showChart() {
|
||||||
this.paymentsChart.show = true
|
this.paymentsChart.show = true
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request(
|
.request(
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ window.app.component('payment-list', {
|
|||||||
template: '#payment-list',
|
template: '#payment-list',
|
||||||
props: ['update', 'wallet', 'mobileSimple', 'lazy'],
|
props: ['update', 'wallet', 'mobileSimple', 'lazy'],
|
||||||
mixins: [window.windowMixin],
|
mixins: [window.windowMixin],
|
||||||
data: function () {
|
data() {
|
||||||
return {
|
return {
|
||||||
denomination: LNBITS_DENOMINATION,
|
denomination: LNBITS_DENOMINATION,
|
||||||
|
failedPaymentsToggle: false,
|
||||||
payments: [],
|
payments: [],
|
||||||
paymentsTable: {
|
paymentsTable: {
|
||||||
columns: [
|
columns: [
|
||||||
@@ -32,6 +33,9 @@ window.app.component('payment-list', {
|
|||||||
rowsNumber: 10
|
rowsNumber: 10
|
||||||
},
|
},
|
||||||
search: null,
|
search: null,
|
||||||
|
filter: {
|
||||||
|
'status[ne]': 'failed'
|
||||||
|
},
|
||||||
loading: false
|
loading: false
|
||||||
},
|
},
|
||||||
exportTagName: '',
|
exportTagName: '',
|
||||||
@@ -107,14 +111,13 @@ window.app.component('payment-list', {
|
|||||||
field: row => row.extra.wallet_fiat_amount
|
field: row => row.extra.wallet_fiat_amount
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
filter: null,
|
|
||||||
loading: false
|
loading: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
filteredPayments: function () {
|
filteredPayments() {
|
||||||
var q = this.paymentsTable.search
|
const q = this.paymentsTable.search
|
||||||
if (!q || q === '') return this.payments
|
if (!q || q === '') return this.payments
|
||||||
|
|
||||||
return LNbits.utils.search(this.payments, q)
|
return LNbits.utils.search(this.payments, q)
|
||||||
@@ -125,12 +128,12 @@ window.app.component('payment-list', {
|
|||||||
}
|
}
|
||||||
return this.payments
|
return this.payments
|
||||||
},
|
},
|
||||||
pendingPaymentsExist: function () {
|
pendingPaymentsExist() {
|
||||||
return this.payments.findIndex(payment => payment.pending) !== -1
|
return this.payments.findIndex(payment => payment.pending) !== -1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchPayments: function (props) {
|
fetchPayments(props) {
|
||||||
const params = LNbits.utils.prepareFilterQuery(this.paymentsTable, props)
|
const params = LNbits.utils.prepareFilterQuery(this.paymentsTable, props)
|
||||||
return LNbits.api
|
return LNbits.api
|
||||||
.getPayments(this.wallet, params)
|
.getPayments(this.wallet, params)
|
||||||
@@ -146,7 +149,7 @@ window.app.component('payment-list', {
|
|||||||
LNbits.utils.notifyApiError(err)
|
LNbits.utils.notifyApiError(err)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
paymentTableRowKey: function (row) {
|
paymentTableRowKey(row) {
|
||||||
return row.payment_hash + row.amount
|
return row.payment_hash + row.amount
|
||||||
},
|
},
|
||||||
exportCSV(detailed = false) {
|
exportCSV(detailed = false) {
|
||||||
@@ -191,7 +194,7 @@ window.app.component('payment-list', {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
addFilterTag: function () {
|
addFilterTag() {
|
||||||
if (!this.exportTagName) return
|
if (!this.exportTagName) return
|
||||||
const value = this.exportTagName.trim()
|
const value = this.exportTagName.trim()
|
||||||
this.exportPaymentTagList = this.exportPaymentTagList.filter(
|
this.exportPaymentTagList = this.exportPaymentTagList.filter(
|
||||||
@@ -200,12 +203,12 @@ window.app.component('payment-list', {
|
|||||||
this.exportPaymentTagList.push(value)
|
this.exportPaymentTagList.push(value)
|
||||||
this.exportTagName = ''
|
this.exportTagName = ''
|
||||||
},
|
},
|
||||||
removeExportTag: function (value) {
|
removeExportTag(value) {
|
||||||
this.exportPaymentTagList = this.exportPaymentTagList.filter(
|
this.exportPaymentTagList = this.exportPaymentTagList.filter(
|
||||||
v => v !== value
|
v => v !== value
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
formatCurrency: function (amount, currency) {
|
formatCurrency(amount, currency) {
|
||||||
try {
|
try {
|
||||||
return LNbits.utils.formatCurrency(amount, currency)
|
return LNbits.utils.formatCurrency(amount, currency)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -215,14 +218,24 @@ window.app.component('payment-list', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
lazy: function (newVal) {
|
failedPaymentsToggle(newVal) {
|
||||||
|
if (newVal === false) {
|
||||||
|
this.paymentsTable.filter = {
|
||||||
|
'status[ne]': 'failed'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.paymentsTable.filter = null
|
||||||
|
}
|
||||||
|
this.fetchPayments()
|
||||||
|
},
|
||||||
|
lazy(newVal) {
|
||||||
if (newVal === true) this.fetchPayments()
|
if (newVal === true) this.fetchPayments()
|
||||||
},
|
},
|
||||||
update: function () {
|
update() {
|
||||||
this.fetchPayments()
|
this.fetchPayments()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created: function () {
|
created() {
|
||||||
if (this.lazy === undefined) this.fetchPayments()
|
if (this.lazy === undefined) this.fetchPayments()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,28 +1,19 @@
|
|||||||
function eventReactionWebocket(event_id) {
|
function eventReaction(amount) {
|
||||||
localUrl = ''
|
localUrl = ''
|
||||||
reaction = localStorage.getItem('lnbits.reactions')
|
reaction = localStorage.getItem('lnbits.reactions')
|
||||||
if (!reaction || reaction === 'None') {
|
if (!reaction || reaction === 'None') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (location.protocol !== 'http:') {
|
try {
|
||||||
localUrl = 'wss://' + location.host + '/api/v1/ws/' + event_id
|
if (amount < 0) {
|
||||||
} else {
|
return
|
||||||
localUrl = 'ws://' + location.host + '/api/v1/ws/' + event_id
|
|
||||||
}
|
|
||||||
connection = new WebSocket(localUrl)
|
|
||||||
connection.onmessage = function (e) {
|
|
||||||
try {
|
|
||||||
const parsedData = JSON.parse(e.data)
|
|
||||||
if (parsedData.payment.amount < 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
reaction = localStorage.getItem('lnbits.reactions')
|
|
||||||
if (reaction) {
|
|
||||||
window[reaction.split('|')[1]]()
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
}
|
}
|
||||||
|
reaction = localStorage.getItem('lnbits.reactions')
|
||||||
|
if (reaction) {
|
||||||
|
window[reaction.split('|')[1]]()
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function confettiBothSides() {
|
function confettiBothSides() {
|
||||||
|
|||||||
+12
-12
@@ -1,7 +1,7 @@
|
|||||||
window.app = Vue.createApp({
|
window.app = Vue.createApp({
|
||||||
el: '#vue',
|
el: '#vue',
|
||||||
mixins: [window.windowMixin],
|
mixins: [window.windowMixin],
|
||||||
data: function () {
|
data() {
|
||||||
return {
|
return {
|
||||||
disclaimerDialog: {
|
disclaimerDialog: {
|
||||||
show: false,
|
show: false,
|
||||||
@@ -30,11 +30,11 @@ window.app = Vue.createApp({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
showLogin: function (authMethod) {
|
showLogin(authMethod) {
|
||||||
this.authAction = 'login'
|
this.authAction = 'login'
|
||||||
this.authMethod = authMethod
|
this.authMethod = authMethod
|
||||||
},
|
},
|
||||||
showRegister: function (authMethod) {
|
showRegister(authMethod) {
|
||||||
this.user = ''
|
this.user = ''
|
||||||
this.username = null
|
this.username = null
|
||||||
this.password = null
|
this.password = null
|
||||||
@@ -43,7 +43,7 @@ window.app = Vue.createApp({
|
|||||||
this.authAction = 'register'
|
this.authAction = 'register'
|
||||||
this.authMethod = authMethod
|
this.authMethod = authMethod
|
||||||
},
|
},
|
||||||
signInWithNostr: async function () {
|
async signInWithNostr() {
|
||||||
try {
|
try {
|
||||||
const nostrToken = await this.createNostrToken()
|
const nostrToken = await this.createNostrToken()
|
||||||
if (!nostrToken) {
|
if (!nostrToken) {
|
||||||
@@ -65,7 +65,7 @@ window.app = Vue.createApp({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
createNostrToken: async function () {
|
async createNostrToken() {
|
||||||
try {
|
try {
|
||||||
async function _signEvent(e) {
|
async function _signEvent(e) {
|
||||||
try {
|
try {
|
||||||
@@ -116,7 +116,7 @@ window.app = Vue.createApp({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
register: async function () {
|
async register() {
|
||||||
try {
|
try {
|
||||||
await LNbits.api.register(
|
await LNbits.api.register(
|
||||||
this.username,
|
this.username,
|
||||||
@@ -129,7 +129,7 @@ window.app = Vue.createApp({
|
|||||||
LNbits.utils.notifyApiError(e)
|
LNbits.utils.notifyApiError(e)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
reset: async function () {
|
async reset() {
|
||||||
try {
|
try {
|
||||||
await LNbits.api.reset(
|
await LNbits.api.reset(
|
||||||
this.reset_key,
|
this.reset_key,
|
||||||
@@ -141,7 +141,7 @@ window.app = Vue.createApp({
|
|||||||
LNbits.utils.notifyApiError(e)
|
LNbits.utils.notifyApiError(e)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
login: async function () {
|
async login() {
|
||||||
try {
|
try {
|
||||||
await LNbits.api.login(this.username, this.password)
|
await LNbits.api.login(this.username, this.password)
|
||||||
window.location.href = '/wallet'
|
window.location.href = '/wallet'
|
||||||
@@ -149,7 +149,7 @@ window.app = Vue.createApp({
|
|||||||
LNbits.utils.notifyApiError(e)
|
LNbits.utils.notifyApiError(e)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
loginUsr: async function () {
|
async loginUsr() {
|
||||||
try {
|
try {
|
||||||
await LNbits.api.loginUsr(this.usr)
|
await LNbits.api.loginUsr(this.usr)
|
||||||
this.usr = ''
|
this.usr = ''
|
||||||
@@ -159,19 +159,19 @@ window.app = Vue.createApp({
|
|||||||
LNbits.utils.notifyApiError(e)
|
LNbits.utils.notifyApiError(e)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
createWallet: function () {
|
createWallet() {
|
||||||
LNbits.api.createAccount(this.walletName).then(res => {
|
LNbits.api.createAccount(this.walletName).then(res => {
|
||||||
window.location = '/wallet?usr=' + res.data.user + '&wal=' + res.data.id
|
window.location = '/wallet?usr=' + res.data.user + '&wal=' + res.data.id
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
processing: function () {
|
processing() {
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
timeout: 0,
|
timeout: 0,
|
||||||
message: 'Processing...',
|
message: 'Processing...',
|
||||||
icon: null
|
icon: null
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
validateUsername: function (val) {
|
validateUsername(val) {
|
||||||
const usernameRegex = new RegExp(
|
const usernameRegex = new RegExp(
|
||||||
'^(?=[a-zA-Z0-9._]{2,20}$)(?!.*[_.]{2})[^_.].*[^_.]$'
|
'^(?=[a-zA-Z0-9._]{2,20}$)(?!.*[_.]{2})[^_.].*[^_.]$'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
window.app.component('lnbits-node-ranks', {
|
window.app.component('lnbits-node-ranks', {
|
||||||
props: ['ranks'],
|
props: ['ranks'],
|
||||||
data: function () {
|
data() {
|
||||||
return {
|
return {
|
||||||
user: {},
|
user: {},
|
||||||
stats: [
|
stats: [
|
||||||
@@ -31,7 +31,7 @@ window.app.component('lnbits-node-ranks', {
|
|||||||
|
|
||||||
window.app.component('lnbits-channel-stats', {
|
window.app.component('lnbits-channel-stats', {
|
||||||
props: ['stats'],
|
props: ['stats'],
|
||||||
data: function () {
|
data() {
|
||||||
return {
|
return {
|
||||||
states: [
|
states: [
|
||||||
{label: 'Active', value: 'active', color: 'green'},
|
{label: 'Active', value: 'active', color: 'green'},
|
||||||
@@ -58,7 +58,7 @@ window.app.component('lnbits-channel-stats', {
|
|||||||
</div>
|
</div>
|
||||||
</q-card>
|
</q-card>
|
||||||
`,
|
`,
|
||||||
created: function () {
|
created() {
|
||||||
if (window.user) {
|
if (window.user) {
|
||||||
this.user = LNbits.map.user(window.user)
|
this.user = LNbits.map.user(window.user)
|
||||||
}
|
}
|
||||||
@@ -68,7 +68,7 @@ window.app.component('lnbits-channel-stats', {
|
|||||||
window.app.component('lnbits-stat', {
|
window.app.component('lnbits-stat', {
|
||||||
props: ['title', 'amount', 'msat', 'btc'],
|
props: ['title', 'amount', 'msat', 'btc'],
|
||||||
computed: {
|
computed: {
|
||||||
value: function () {
|
value() {
|
||||||
return (
|
return (
|
||||||
this.amount ??
|
this.amount ??
|
||||||
(this.btc
|
(this.btc
|
||||||
@@ -178,7 +178,7 @@ window.app.component('lnbits-node-info', {
|
|||||||
window.app.component('lnbits-stat', {
|
window.app.component('lnbits-stat', {
|
||||||
props: ['title', 'amount', 'msat', 'btc'],
|
props: ['title', 'amount', 'msat', 'btc'],
|
||||||
computed: {
|
computed: {
|
||||||
value: function () {
|
value() {
|
||||||
return (
|
return (
|
||||||
this.amount ??
|
this.amount ??
|
||||||
(this.btc
|
(this.btc
|
||||||
@@ -206,7 +206,7 @@ window.app.component('lnbits-stat', {
|
|||||||
window.app.component('lnbits-channel-balance', {
|
window.app.component('lnbits-channel-balance', {
|
||||||
props: ['balance', 'color'],
|
props: ['balance', 'color'],
|
||||||
methods: {
|
methods: {
|
||||||
formatMsat: function (msat) {
|
formatMsat(msat) {
|
||||||
return LNbits.utils.formatMsat(msat)
|
return LNbits.utils.formatMsat(msat)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -247,13 +247,10 @@ window.app.component('lnbits-channel-balance', {
|
|||||||
window.app.component('lnbits-date', {
|
window.app.component('lnbits-date', {
|
||||||
props: ['ts'],
|
props: ['ts'],
|
||||||
computed: {
|
computed: {
|
||||||
date: function () {
|
date() {
|
||||||
return Quasar.date.formatDate(
|
return LNbits.utils.formatDate(this.ts)
|
||||||
new Date(this.ts * 1000),
|
|
||||||
'YYYY-MM-DD HH:mm'
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
dateFrom: function () {
|
dateFrom() {
|
||||||
return moment(this.date).fromNow()
|
return moment(this.date).fromNow()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+17
-73
@@ -1,10 +1,9 @@
|
|||||||
window.app = Vue.createApp({
|
window.app = Vue.createApp({
|
||||||
el: '#vue',
|
el: '#vue',
|
||||||
mixins: [window.windowMixin],
|
mixins: [window.windowMixin],
|
||||||
data: function () {
|
data() {
|
||||||
return {
|
return {
|
||||||
paymentsWallet: {},
|
paymentsWallet: {},
|
||||||
wallet: {},
|
|
||||||
cancel: {},
|
cancel: {},
|
||||||
users: [],
|
users: [],
|
||||||
wallets: [],
|
wallets: [],
|
||||||
@@ -21,9 +20,6 @@ window.app = Vue.createApp({
|
|||||||
userId: null,
|
userId: null,
|
||||||
show: false
|
show: false
|
||||||
},
|
},
|
||||||
topupDialog: {
|
|
||||||
show: false
|
|
||||||
},
|
|
||||||
activeUser: {
|
activeUser: {
|
||||||
data: null,
|
data: null,
|
||||||
showUserId: false,
|
showUserId: false,
|
||||||
@@ -172,10 +168,10 @@ window.app = Vue.createApp({
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
formatDate: function (value) {
|
formatDate(date) {
|
||||||
return LNbits.utils.formatDate(value)
|
return LNbits.utils.formatDateString(date)
|
||||||
},
|
},
|
||||||
formatSat: function (value) {
|
formatSat(value) {
|
||||||
return LNbits.utils.formatSat(Math.floor(value / 1000))
|
return LNbits.utils.formatSat(Math.floor(value / 1000))
|
||||||
},
|
},
|
||||||
backToUsersPage() {
|
backToUsersPage() {
|
||||||
@@ -184,6 +180,9 @@ window.app = Vue.createApp({
|
|||||||
this.activeWallet.show = false
|
this.activeWallet.show = false
|
||||||
this.fetchUsers()
|
this.fetchUsers()
|
||||||
},
|
},
|
||||||
|
handleBalanceUpdate() {
|
||||||
|
this.fetchWallets(this.activeWallet.userId)
|
||||||
|
},
|
||||||
resetPassword(user_id) {
|
resetPassword(user_id) {
|
||||||
return LNbits.api
|
return LNbits.api
|
||||||
.request('PUT', `/users/api/v1/user/${user_id}/reset_password`)
|
.request('PUT', `/users/api/v1/user/${user_id}/reset_password`)
|
||||||
@@ -197,9 +196,7 @@ window.app = Vue.createApp({
|
|||||||
this.copyText(url)
|
this.copyText(url)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(LNbits.utils.notifyApiError)
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
createUser() {
|
createUser() {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
@@ -215,9 +212,7 @@ window.app = Vue.createApp({
|
|||||||
this.activeUser.data = resp.data
|
this.activeUser.data = resp.data
|
||||||
this.fetchUsers()
|
this.fetchUsers()
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(LNbits.utils.notifyApiError)
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
updateUser() {
|
updateUser() {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
@@ -237,9 +232,7 @@ window.app = Vue.createApp({
|
|||||||
this.activeUser.show = false
|
this.activeUser.show = false
|
||||||
this.fetchUsers()
|
this.fetchUsers()
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(LNbits.utils.notifyApiError)
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
createWallet() {
|
createWallet() {
|
||||||
const userId = this.activeWallet.userId
|
const userId = this.activeWallet.userId
|
||||||
@@ -265,9 +258,7 @@ window.app = Vue.createApp({
|
|||||||
message: 'Wallet created!'
|
message: 'Wallet created!'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(LNbits.utils.notifyApiError)
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
deleteUser(user_id) {
|
deleteUser(user_id) {
|
||||||
LNbits.utils
|
LNbits.utils
|
||||||
@@ -285,9 +276,7 @@ window.app = Vue.createApp({
|
|||||||
this.activeUser.data = null
|
this.activeUser.data = null
|
||||||
this.activeUser.show = false
|
this.activeUser.show = false
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(LNbits.utils.notifyApiError)
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
undeleteUserWallet(user_id, wallet) {
|
undeleteUserWallet(user_id, wallet) {
|
||||||
@@ -304,9 +293,7 @@ window.app = Vue.createApp({
|
|||||||
icon: null
|
icon: null
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(LNbits.utils.notifyApiError)
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
deleteUserWallet(user_id, wallet, deleted) {
|
deleteUserWallet(user_id, wallet, deleted) {
|
||||||
const dialogText = deleted
|
const dialogText = deleted
|
||||||
@@ -323,9 +310,7 @@ window.app = Vue.createApp({
|
|||||||
icon: null
|
icon: null
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(LNbits.utils.notifyApiError)
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
copyWalletLink(walletId) {
|
copyWalletLink(walletId) {
|
||||||
@@ -342,9 +327,7 @@ window.app = Vue.createApp({
|
|||||||
this.usersTable.pagination.rowsNumber = res.data.total
|
this.usersTable.pagination.rowsNumber = res.data.total
|
||||||
this.users = res.data.data
|
this.users = res.data.data
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(LNbits.utils.notifyApiError)
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
fetchWallets(userId) {
|
fetchWallets(userId) {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
@@ -354,9 +337,7 @@ window.app = Vue.createApp({
|
|||||||
this.activeWallet.userId = userId
|
this.activeWallet.userId = userId
|
||||||
this.activeWallet.show = true
|
this.activeWallet.show = true
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(LNbits.utils.notifyApiError)
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleAdmin(userId) {
|
toggleAdmin(userId) {
|
||||||
@@ -370,9 +351,7 @@ window.app = Vue.createApp({
|
|||||||
icon: null
|
icon: null
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(LNbits.utils.notifyApiError)
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
exportUsers() {
|
exportUsers() {
|
||||||
console.log('export users')
|
console.log('export users')
|
||||||
@@ -403,45 +382,10 @@ window.app = Vue.createApp({
|
|||||||
this.activeUser.show = false
|
this.activeUser.show = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
showTopupDialog(walletId) {
|
|
||||||
this.wallet.id = walletId
|
|
||||||
this.topupDialog.show = true
|
|
||||||
},
|
|
||||||
showPayments(wallet_id) {
|
showPayments(wallet_id) {
|
||||||
this.paymentsWallet = this.wallets.find(wallet => wallet.id === wallet_id)
|
this.paymentsWallet = this.wallets.find(wallet => wallet.id === wallet_id)
|
||||||
this.paymentPage.show = true
|
this.paymentPage.show = true
|
||||||
},
|
},
|
||||||
topupCallback(res) {
|
|
||||||
if (res.success) {
|
|
||||||
this.wallets.forEach(wallet => {
|
|
||||||
if (res.wallet_id === wallet.id) {
|
|
||||||
wallet.balance_msat += res.credit * 1000
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.fetchUsers()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
topupWallet() {
|
|
||||||
LNbits.api
|
|
||||||
.request(
|
|
||||||
'PUT',
|
|
||||||
'/users/api/v1/topup',
|
|
||||||
this.g.user.wallets[0].adminkey,
|
|
||||||
this.wallet
|
|
||||||
)
|
|
||||||
.then(_ => {
|
|
||||||
Quasar.Notify.create({
|
|
||||||
type: 'positive',
|
|
||||||
message: `Added ${this.wallet.amount} to ${this.wallet.id}`,
|
|
||||||
icon: null
|
|
||||||
})
|
|
||||||
this.wallet = {}
|
|
||||||
this.fetchWallets(this.activeWallet.userId)
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
searchUserBy(fieldName) {
|
searchUserBy(fieldName) {
|
||||||
const fieldValue = this.searchData[fieldName]
|
const fieldValue = this.searchData[fieldName]
|
||||||
this.usersTable.filter = {}
|
this.usersTable.filter = {}
|
||||||
|
|||||||
+246
-87
@@ -1,12 +1,13 @@
|
|||||||
window.app = Vue.createApp({
|
window.app = Vue.createApp({
|
||||||
el: '#vue',
|
el: '#vue',
|
||||||
mixins: [window.windowMixin],
|
mixins: [window.windowMixin],
|
||||||
data: function () {
|
data() {
|
||||||
return {
|
return {
|
||||||
updatePayments: false,
|
updatePayments: false,
|
||||||
origin: window.location.origin,
|
origin: window.location.origin,
|
||||||
wallet: LNbits.map.wallet(window.wallet),
|
wallet: LNbits.map.wallet(window.wallet),
|
||||||
user: LNbits.map.user(window.user),
|
user: LNbits.map.user(window.user),
|
||||||
|
mobileSimple: false,
|
||||||
exportUrl: `${window.location.origin}/wallet?usr=${window.user.id}&wal=${window.wallet.id}`,
|
exportUrl: `${window.location.origin}/wallet?usr=${window.user.id}&wal=${window.wallet.id}`,
|
||||||
baseUrl: `${window.location.protocol}//${window.location.host}/`,
|
baseUrl: `${window.location.protocol}//${window.location.host}/`,
|
||||||
receive: {
|
receive: {
|
||||||
@@ -50,8 +51,6 @@ window.app = Vue.createApp({
|
|||||||
},
|
},
|
||||||
balance: parseInt(wallet.balance_msat / 1000),
|
balance: parseInt(wallet.balance_msat / 1000),
|
||||||
fiatBalance: 0,
|
fiatBalance: 0,
|
||||||
mobileSimple: false,
|
|
||||||
credit: 0,
|
|
||||||
update: {
|
update: {
|
||||||
name: null,
|
name: null,
|
||||||
currency: null
|
currency: null
|
||||||
@@ -59,30 +58,32 @@ window.app = Vue.createApp({
|
|||||||
inkeyHidden: true,
|
inkeyHidden: true,
|
||||||
adminkeyHidden: true,
|
adminkeyHidden: true,
|
||||||
hasNfc: false,
|
hasNfc: false,
|
||||||
nfcReaderAbortController: null
|
nfcReaderAbortController: null,
|
||||||
|
isPrioritySwapped: false,
|
||||||
|
fiatTracking: false,
|
||||||
|
formattedFiatAmount: 0,
|
||||||
|
exchangeRate: 0,
|
||||||
|
formattedExchange: null,
|
||||||
|
ignoreWatcher: true,
|
||||||
|
primaryColor: this.$q.localStorage.getItem('lnbits.primaryColor'),
|
||||||
|
transactions: [],
|
||||||
|
transactionChart: null,
|
||||||
|
paymentsRef: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
formattedBalance: function () {
|
formattedBalance() {
|
||||||
if (LNBITS_DENOMINATION != 'sats') {
|
if (LNBITS_DENOMINATION != 'sats') {
|
||||||
return this.balance / 100
|
return this.balance / 100
|
||||||
} else {
|
} else {
|
||||||
return LNbits.utils.formatSat(this.balance || this.g.wallet.sat)
|
return LNbits.utils.formatSat(this.balance || this.g.wallet.sat)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
formattedFiatBalance() {
|
canPay() {
|
||||||
if (this.fiatBalance) {
|
|
||||||
return LNbits.utils.formatCurrency(
|
|
||||||
this.fiatBalance.toFixed(2),
|
|
||||||
this.g.wallet.currency
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
canPay: function () {
|
|
||||||
if (!this.parse.invoice) return false
|
if (!this.parse.invoice) return false
|
||||||
return this.parse.invoice.sat <= this.balance
|
return this.parse.invoice.sat <= this.balance
|
||||||
},
|
},
|
||||||
formattedAmount: function () {
|
formattedAmount() {
|
||||||
if (this.receive.unit != 'sat') {
|
if (this.receive.unit != 'sat') {
|
||||||
return LNbits.utils.formatCurrency(
|
return LNbits.utils.formatCurrency(
|
||||||
Number(this.receive.data.amount).toFixed(2),
|
Number(this.receive.data.amount).toFixed(2),
|
||||||
@@ -92,24 +93,113 @@ window.app = Vue.createApp({
|
|||||||
return LNbits.utils.formatMsat(this.receive.amountMsat) + ' sat'
|
return LNbits.utils.formatMsat(this.receive.amountMsat) + ' sat'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
formattedSatAmount: function () {
|
formattedSatAmount() {
|
||||||
return LNbits.utils.formatMsat(this.receive.amountMsat) + ' sat'
|
return LNbits.utils.formatMsat(this.receive.amountMsat) + ' sat'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
msatoshiFormat: function (value) {
|
formatFiatAmount(amount, currency) {
|
||||||
|
this.update.currency = currency
|
||||||
|
this.formattedFiatAmount = LNbits.utils.formatCurrency(
|
||||||
|
amount.toFixed(2),
|
||||||
|
currency
|
||||||
|
)
|
||||||
|
this.formattedExchange = LNbits.utils.formatCurrency(
|
||||||
|
this.exchangeRate,
|
||||||
|
currency
|
||||||
|
)
|
||||||
|
},
|
||||||
|
computeCumulativeBalance(transactions) {
|
||||||
|
let balance = 0
|
||||||
|
return transactions.map(transaction => {
|
||||||
|
balance += transaction.amount // Use the amount field for the balance
|
||||||
|
return balance
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
initCharts() {
|
||||||
|
this.transactionChart = new Chart(
|
||||||
|
this.$refs.transactionChart.getContext('2d'),
|
||||||
|
{
|
||||||
|
type: 'line',
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
layout: {
|
||||||
|
padding: 0,
|
||||||
|
margin: 0
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
enabled: true, // Enable tooltips to show data on hover
|
||||||
|
callbacks: {
|
||||||
|
// Custom tooltip callback to show amount and time
|
||||||
|
label: tooltipItem => {
|
||||||
|
const index = tooltipItem.dataIndex
|
||||||
|
const transaction = this.transactions[index] // Match tooltip point with transaction
|
||||||
|
const amount = transaction.amount
|
||||||
|
return [
|
||||||
|
`Balance: ${tooltipItem.raw / 1000}sats`, // Display cumulative balance
|
||||||
|
`Amount: ${amount / 1000}sats`
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
elements: {
|
||||||
|
point: {
|
||||||
|
radius: 4, // Points will now be visible
|
||||||
|
hoverRadius: 6 // Increase point size on hover
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
display: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
labels: this.transactions.map(
|
||||||
|
tx => new Date(tx.time).toLocaleString() // Use time for labels
|
||||||
|
),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Cumulative Balance',
|
||||||
|
data: this.computeCumulativeBalance(this.transactions),
|
||||||
|
backgroundColor: LNbits.utils.hexAlpha(this.primaryColor, 0.3),
|
||||||
|
borderColor: this.primaryColor,
|
||||||
|
borderWidth: 2,
|
||||||
|
fill: true,
|
||||||
|
tension: 0.2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
msatoshiFormat(value) {
|
||||||
return LNbits.utils.formatSat(value / 1000)
|
return LNbits.utils.formatSat(value / 1000)
|
||||||
},
|
},
|
||||||
closeCamera: function () {
|
closeCamera() {
|
||||||
this.parse.camera.show = false
|
this.parse.camera.show = false
|
||||||
},
|
},
|
||||||
showCamera: function () {
|
showCamera() {
|
||||||
this.parse.camera.show = true
|
this.parse.camera.show = true
|
||||||
},
|
},
|
||||||
focusInput(el) {
|
focusInput(el) {
|
||||||
this.$nextTick(() => this.$refs[el].focus())
|
this.$nextTick(() => this.$refs[el].focus())
|
||||||
},
|
},
|
||||||
showReceiveDialog: function () {
|
showReceiveDialog() {
|
||||||
this.receive.show = true
|
this.receive.show = true
|
||||||
this.receive.status = 'pending'
|
this.receive.status = 'pending'
|
||||||
this.receive.paymentReq = null
|
this.receive.paymentReq = null
|
||||||
@@ -121,12 +211,12 @@ window.app = Vue.createApp({
|
|||||||
this.receive.lnurl = null
|
this.receive.lnurl = null
|
||||||
this.focusInput('setAmount')
|
this.focusInput('setAmount')
|
||||||
},
|
},
|
||||||
onReceiveDialogHide: function () {
|
onReceiveDialogHide() {
|
||||||
if (this.hasNfc) {
|
if (this.hasNfc) {
|
||||||
this.nfcReaderAbortController.abort()
|
this.nfcReaderAbortController.abort()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
showParseDialog: function () {
|
showParseDialog() {
|
||||||
this.parse.show = true
|
this.parse.show = true
|
||||||
this.parse.invoice = null
|
this.parse.invoice = null
|
||||||
this.parse.lnurlpay = null
|
this.parse.lnurlpay = null
|
||||||
@@ -139,19 +229,22 @@ window.app = Vue.createApp({
|
|||||||
this.parse.camera.show = false
|
this.parse.camera.show = false
|
||||||
this.focusInput('textArea')
|
this.focusInput('textArea')
|
||||||
},
|
},
|
||||||
closeParseDialog: function () {
|
closeParseDialog() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
clearInterval(this.parse.paymentChecker)
|
clearInterval(this.parse.paymentChecker)
|
||||||
}, 10000)
|
}, 10000)
|
||||||
},
|
},
|
||||||
onPaymentReceived: function (paymentHash) {
|
onPaymentReceived(paymentHash) {
|
||||||
this.updatePayments = !this.updatePayments
|
this.updatePayments = !this.updatePayments
|
||||||
if (this.receive.paymentHash === paymentHash) {
|
if (this.receive.paymentHash === paymentHash) {
|
||||||
this.receive.show = false
|
this.receive.show = false
|
||||||
this.receive.paymentHash = null
|
this.receive.paymentHash = null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
createInvoice: function () {
|
handleBalanceUpdate(value) {
|
||||||
|
this.balance = this.balance + value
|
||||||
|
},
|
||||||
|
createInvoice() {
|
||||||
this.receive.status = 'loading'
|
this.receive.status = 'loading'
|
||||||
if (LNBITS_DENOMINATION != 'sats') {
|
if (LNBITS_DENOMINATION != 'sats') {
|
||||||
this.receive.data.amount = this.receive.data.amount * 100
|
this.receive.data.amount = this.receive.data.amount * 100
|
||||||
@@ -206,11 +299,11 @@ window.app = Vue.createApp({
|
|||||||
this.receive.status = 'pending'
|
this.receive.status = 'pending'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onInitQR: async function (promise) {
|
async onInitQR(promise) {
|
||||||
try {
|
try {
|
||||||
await promise
|
await promise
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
let mapping = {
|
const mapping = {
|
||||||
NotAllowedError: 'ERROR: you need to grant camera access permission',
|
NotAllowedError: 'ERROR: you need to grant camera access permission',
|
||||||
NotFoundError: 'ERROR: no camera on this device',
|
NotFoundError: 'ERROR: no camera on this device',
|
||||||
NotSupportedError:
|
NotSupportedError:
|
||||||
@@ -222,10 +315,10 @@ window.app = Vue.createApp({
|
|||||||
InsecureContextError:
|
InsecureContextError:
|
||||||
'ERROR: Camera access is only permitted in secure context. Use HTTPS or localhost rather than HTTP.'
|
'ERROR: Camera access is only permitted in secure context. Use HTTPS or localhost rather than HTTP.'
|
||||||
}
|
}
|
||||||
let valid_error = Object.keys(mapping).filter(key => {
|
const valid_error = Object.keys(mapping).filter(key => {
|
||||||
return error.name === key
|
return error.name === key
|
||||||
})
|
})
|
||||||
let camera_error = valid_error
|
const camera_error = valid_error
|
||||||
? mapping[valid_error]
|
? mapping[valid_error]
|
||||||
: `ERROR: Camera error (${error.name})`
|
: `ERROR: Camera error (${error.name})`
|
||||||
this.parse.camera.show = false
|
this.parse.camera.show = false
|
||||||
@@ -246,7 +339,7 @@ window.app = Vue.createApp({
|
|||||||
LNbits.utils.notifyApiError(err)
|
LNbits.utils.notifyApiError(err)
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
let data = response.data
|
const data = response.data
|
||||||
|
|
||||||
if (data.status === 'ERROR') {
|
if (data.status === 'ERROR') {
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
@@ -283,12 +376,12 @@ window.app = Vue.createApp({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
decodeQR: function (res) {
|
decodeQR(res) {
|
||||||
this.parse.data.request = res[0].rawValue
|
this.parse.data.request = res[0].rawValue
|
||||||
this.decodeRequest()
|
this.decodeRequest()
|
||||||
this.parse.camera.show = false
|
this.parse.camera.show = false
|
||||||
},
|
},
|
||||||
decodeRequest: function () {
|
decodeRequest() {
|
||||||
this.parse.show = true
|
this.parse.show = true
|
||||||
this.parse.data.request = this.parse.data.request.trim().toLowerCase()
|
this.parse.data.request = this.parse.data.request.trim().toLowerCase()
|
||||||
let req = this.parse.data.request
|
let req = this.parse.data.request
|
||||||
@@ -342,7 +435,7 @@ window.app = Vue.createApp({
|
|||||||
} else if (tag.description === 'description') {
|
} else if (tag.description === 'description') {
|
||||||
cleanInvoice.description = tag.value
|
cleanInvoice.description = tag.value
|
||||||
} else if (tag.description === 'expiry') {
|
} else if (tag.description === 'expiry') {
|
||||||
var expireDate = new Date(
|
const expireDate = new Date(
|
||||||
(invoice.data.time_stamp + tag.value) * 1000
|
(invoice.data.time_stamp + tag.value) * 1000
|
||||||
)
|
)
|
||||||
cleanInvoice.expireDate = Quasar.date.formatDate(
|
cleanInvoice.expireDate = Quasar.date.formatDate(
|
||||||
@@ -356,8 +449,8 @@ window.app = Vue.createApp({
|
|||||||
|
|
||||||
this.parse.invoice = Object.freeze(cleanInvoice)
|
this.parse.invoice = Object.freeze(cleanInvoice)
|
||||||
},
|
},
|
||||||
payInvoice: function () {
|
payInvoice() {
|
||||||
let dismissPaymentMsg = Quasar.Notify.create({
|
const dismissPaymentMsg = Quasar.Notify.create({
|
||||||
timeout: 0,
|
timeout: 0,
|
||||||
message: this.$t('processing_payment')
|
message: this.$t('processing_payment')
|
||||||
})
|
})
|
||||||
@@ -389,8 +482,8 @@ window.app = Vue.createApp({
|
|||||||
this.parse.show = false
|
this.parse.show = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
payLnurl: function () {
|
payLnurl() {
|
||||||
let dismissPaymentMsg = Quasar.Notify.create({
|
const dismissPaymentMsg = Quasar.Notify.create({
|
||||||
timeout: 0,
|
timeout: 0,
|
||||||
message: 'Processing payment...'
|
message: 'Processing payment...'
|
||||||
})
|
})
|
||||||
@@ -472,8 +565,8 @@ window.app = Vue.createApp({
|
|||||||
LNbits.utils.notifyApiError(err)
|
LNbits.utils.notifyApiError(err)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
authLnurl: function () {
|
authLnurl() {
|
||||||
let dismissAuthMsg = Quasar.Notify.create({
|
const dismissAuthMsg = Quasar.Notify.create({
|
||||||
timeout: 10,
|
timeout: 10,
|
||||||
message: 'Performing authentication...'
|
message: 'Performing authentication...'
|
||||||
})
|
})
|
||||||
@@ -503,7 +596,7 @@ window.app = Vue.createApp({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
updateWallet: function (data) {
|
updateWallet(data) {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request('PATCH', '/api/v1/wallet', this.g.wallet.adminkey, data)
|
.request('PATCH', '/api/v1/wallet', this.g.wallet.adminkey, data)
|
||||||
.then(_ => {
|
.then(_ => {
|
||||||
@@ -512,13 +605,12 @@ window.app = Vue.createApp({
|
|||||||
type: 'positive',
|
type: 'positive',
|
||||||
timeout: 3500
|
timeout: 3500
|
||||||
})
|
})
|
||||||
window.location.reload()
|
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
LNbits.utils.notifyApiError(err)
|
LNbits.utils.notifyApiError(err)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
deleteWallet: function () {
|
deleteWallet() {
|
||||||
LNbits.utils
|
LNbits.utils
|
||||||
.confirmDialog('Are you sure you want to delete this wallet?')
|
.confirmDialog('Are you sure you want to delete this wallet?')
|
||||||
.onOk(() => {
|
.onOk(() => {
|
||||||
@@ -536,43 +628,33 @@ window.app = Vue.createApp({
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
fetchBalance: function () {
|
updateFiatBalance(currency) {
|
||||||
LNbits.api.getWallet(this.g.wallet).then(response => {
|
// set rate from local storage to avoid clunky api calls
|
||||||
this.balance = Math.floor(response.data.balance / 1000)
|
this.exchangeRate = this.$q.localStorage.getItem('lnbits.exchangeRate')
|
||||||
document.dispatchEvent(
|
this.fiatBalance =
|
||||||
new CustomEvent('updateWalletBalance', {
|
(this.exchangeRate / 100000000) * (this.balance || this.g.wallet.sat)
|
||||||
detail: [this.g.wallet.id, this.balance]
|
this.formatFiatAmount(this.fiatBalance, currency)
|
||||||
})
|
// make api call
|
||||||
)
|
|
||||||
})
|
|
||||||
if (this.g.wallet.currency) {
|
|
||||||
this.updateFiatBalance()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateFiatBalance() {
|
|
||||||
if (!this.g.wallet.currency) return 0
|
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request('POST', `/api/v1/conversion`, null, {
|
.request('GET', `/api/v1/rate/` + currency, null)
|
||||||
amount: this.balance || this.g.wallet.sat,
|
|
||||||
to: this.g.wallet.currency
|
|
||||||
})
|
|
||||||
.then(response => {
|
.then(response => {
|
||||||
this.fiatBalance = response.data[this.g.wallet.currency]
|
this.fiatBalance =
|
||||||
|
(response.data.price / 100000000) *
|
||||||
|
(this.balance || this.g.wallet.sat)
|
||||||
|
this.exchangeRate = response.data.price.toFixed(2)
|
||||||
|
this.fiatTracking = true
|
||||||
|
this.formatFiatAmount(this.fiatBalance, currency)
|
||||||
|
this.$q.localStorage.set('lnbits.exchangeRate', this.exchangeRate)
|
||||||
})
|
})
|
||||||
.catch(e => console.error(e))
|
.catch(e => console.error(e))
|
||||||
},
|
},
|
||||||
updateBalanceCallback: function (res) {
|
pasteToTextArea() {
|
||||||
if (res.success && wallet.id === res.wallet_id) {
|
|
||||||
this.balance += res.credit
|
|
||||||
}
|
|
||||||
},
|
|
||||||
pasteToTextArea: function () {
|
|
||||||
this.$refs.textArea.focus() // Set cursor to textarea
|
this.$refs.textArea.focus() // Set cursor to textarea
|
||||||
navigator.clipboard.readText().then(text => {
|
navigator.clipboard.readText().then(text => {
|
||||||
this.parse.data.request = text.trim()
|
this.parse.data.request = text.trim()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
readNfcTag: function () {
|
readNfcTag() {
|
||||||
try {
|
try {
|
||||||
if (typeof NDEFReader == 'undefined') {
|
if (typeof NDEFReader == 'undefined') {
|
||||||
console.debug('NFC not supported on this device or browser.')
|
console.debug('NFC not supported on this device or browser.')
|
||||||
@@ -587,7 +669,7 @@ window.app = Vue.createApp({
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.hasNfc = true
|
this.hasNfc = true
|
||||||
let dismissNfcTapMsg = Quasar.Notify.create({
|
const dismissNfcTapMsg = Quasar.Notify.create({
|
||||||
message: 'Tap your NFC tag to pay this invoice with LNURLw.'
|
message: 'Tap your NFC tag to pay this invoice with LNURLw.'
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -635,8 +717,8 @@ window.app = Vue.createApp({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
payInvoiceWithNfc: function (lnurl) {
|
payInvoiceWithNfc(lnurl) {
|
||||||
let dismissPaymentMsg = Quasar.Notify.create({
|
const dismissPaymentMsg = Quasar.Notify.create({
|
||||||
timeout: 0,
|
timeout: 0,
|
||||||
spinner: true,
|
spinner: true,
|
||||||
message: this.$t('processing_payment')
|
message: this.$t('processing_payment')
|
||||||
@@ -667,10 +749,52 @@ window.app = Vue.createApp({
|
|||||||
dismissPaymentMsg()
|
dismissPaymentMsg()
|
||||||
LNbits.utils.notifyApiError(err)
|
LNbits.utils.notifyApiError(err)
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
handleWallet(wallet) {
|
||||||
|
console.log('Wallet from child:', wallet)
|
||||||
|
},
|
||||||
|
fetchPayments() {
|
||||||
|
return LNbits.api
|
||||||
|
.getPayments(
|
||||||
|
this.g.wallet,
|
||||||
|
'sortby=time&direction=asc&status[ne]=failed'
|
||||||
|
)
|
||||||
|
.then(response => {
|
||||||
|
console.log(response.data.data)
|
||||||
|
for (let payment of response.data.data) {
|
||||||
|
record = {
|
||||||
|
payment_hash: payment.payment_hash,
|
||||||
|
amount: payment.amount,
|
||||||
|
description: payment.description,
|
||||||
|
time: payment.time
|
||||||
|
}
|
||||||
|
this.transactions.push(record)
|
||||||
|
}
|
||||||
|
console.log(this.transactions)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
this.paymentsTable.loading = false
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
swapBalancePriority() {
|
||||||
|
this.isPrioritySwapped = !this.isPrioritySwapped
|
||||||
|
this.$q.localStorage.setItem(
|
||||||
|
'lnbits.isPrioritySwapped',
|
||||||
|
this.isPrioritySwapped
|
||||||
|
)
|
||||||
|
},
|
||||||
|
handleFiatTracking() {
|
||||||
|
if (this.fiatTracking === false) {
|
||||||
|
this.update.currency = ''
|
||||||
|
this.$q.localStorage.setItem('lnbits.isPrioritySwapped', false)
|
||||||
|
this.isPrioritySwapped = false
|
||||||
|
this.$q.localStorage.remove(`lnbits.exchangeRate`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created: function () {
|
created() {
|
||||||
let 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 =
|
||||||
urlParams.get('lightning') || urlParams.get('lnurl')
|
urlParams.get('lightning') || urlParams.get('lnurl')
|
||||||
@@ -680,34 +804,69 @@ window.app = Vue.createApp({
|
|||||||
if (this.$q.screen.lt.md) {
|
if (this.$q.screen.lt.md) {
|
||||||
this.mobileSimple = true
|
this.mobileSimple = true
|
||||||
}
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
this.ignoreWatcher = false
|
||||||
|
}, 3000)
|
||||||
|
if (this.g.wallet.currency) {
|
||||||
|
this.fiatTracking = true
|
||||||
|
this.updateFiatBalance(this.g.wallet.currency)
|
||||||
|
}
|
||||||
this.update.name = this.g.wallet.name
|
this.update.name = this.g.wallet.name
|
||||||
this.update.currency = this.g.wallet.currency
|
|
||||||
this.receive.units = ['sat', ...window.currencies]
|
this.receive.units = ['sat', ...window.currencies]
|
||||||
this.updateFiatBalance()
|
this.fetchPayments().then(() => {
|
||||||
|
this.initCharts()
|
||||||
|
})
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
updatePayments: function () {
|
'$q.screen.gt.sm'(value) {
|
||||||
|
if (value == true) {
|
||||||
|
this.mobileSimple = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updatePayments() {
|
||||||
this.fetchBalance()
|
this.fetchBalance()
|
||||||
}
|
},
|
||||||
|
'update.currency'(newValue) {
|
||||||
|
if (this.ignoreWatcher || this.update.currency == '') return
|
||||||
|
this.updateWallet({currency: newValue})
|
||||||
|
this.updateFiatBalance(newValue)
|
||||||
|
},
|
||||||
|
'$q.screen.gt.sm'(value) {
|
||||||
|
if (value == true) {
|
||||||
|
this.mobileSimple = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
mounted: function () {
|
mounted() {
|
||||||
// show disclaimer
|
// show disclaimer
|
||||||
if (!this.$q.localStorage.getItem('lnbits.disclaimerShown')) {
|
if (!this.$q.localStorage.getItem('lnbits.disclaimerShown')) {
|
||||||
this.disclaimerDialog.show = true
|
this.disclaimerDialog.show = true
|
||||||
this.$q.localStorage.set('lnbits.disclaimerShown', true)
|
this.$q.localStorage.set('lnbits.disclaimerShown', true)
|
||||||
}
|
}
|
||||||
|
// check blanace priority
|
||||||
|
if (this.$q.localStorage.getItem('lnbits.isPrioritySwapped')) {
|
||||||
|
this.isPrioritySwapped = this.$q.localStorage.getItem(
|
||||||
|
'lnbits.isPrioritySwapped'
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.isPrioritySwapped = false
|
||||||
|
this.$q.localStorage.setItem('lnbits.isPrioritySwapped', false)
|
||||||
|
}
|
||||||
// listen to incoming payments
|
// listen to incoming payments
|
||||||
LNbits.events.onInvoicePaid(this.g.wallet, payment => {
|
LNbits.events.onInvoicePaid(this.g.wallet, data => {
|
||||||
this.onPaymentReceived(payment.payment_hash)
|
console.log('Payment received:', data.payment.payment_hash)
|
||||||
|
console.log('Wallet balance:', data.wallet_balance)
|
||||||
|
console.log('Wallet ID:', this.g.wallet)
|
||||||
|
this.onPaymentReceived(data.payment.payment_hash)
|
||||||
|
this.balance = data.wallet_balance
|
||||||
|
eventReaction(data.payment.amount)
|
||||||
})
|
})
|
||||||
eventReactionWebocket(wallet.inkey)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (navigator.serviceWorker != null) {
|
if (navigator.serviceWorker != null) {
|
||||||
navigator.serviceWorker
|
navigator.serviceWorker.register('/service-worker.js').then(registration => {
|
||||||
.register('/service-worker.js')
|
console.log('Registered events at scope: ', registration.scope)
|
||||||
.then(function (registration) {
|
})
|
||||||
console.log('Registered events at scope: ', registration.scope)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
File diff suppressed because one or more lines are too long
@@ -65,16 +65,16 @@
|
|||||||
class="q-mr-md gt-md"
|
class="q-mr-md gt-md"
|
||||||
>
|
>
|
||||||
</q-badge>
|
</q-badge>
|
||||||
{%endif%}
|
{%endif%} {% if LNBITS_CUSTOM_BADGE is not none and
|
||||||
|
LNBITS_CUSTOM_BADGE != "" %}
|
||||||
<q-badge
|
<q-badge
|
||||||
v-if="'{{LNBITS_CUSTOM_BADGE}}' && '{{LNBITS_CUSTOM_BADGE}}' != 'None'"
|
|
||||||
v-show="$q.screen.gt.sm"
|
v-show="$q.screen.gt.sm"
|
||||||
color="{{ LNBITS_CUSTOM_BADGE_COLOR }}"
|
color="{{ LNBITS_CUSTOM_BADGE_COLOR }}"
|
||||||
class="q-mr-md"
|
class="q-mr-md"
|
||||||
label="{{LNBITS_CUSTOM_BADGE}}"
|
label="{{LNBITS_CUSTOM_BADGE|e}}"
|
||||||
>
|
>
|
||||||
</q-badge>
|
</q-badge>
|
||||||
{% if LNBITS_SERVICE_FEE > 0 %}
|
{% endif %} {% if LNBITS_SERVICE_FEE > 0 %}
|
||||||
<q-badge
|
<q-badge
|
||||||
v-show="$q.screen.gt.sm"
|
v-show="$q.screen.gt.sm"
|
||||||
v-if="g.user"
|
v-if="g.user"
|
||||||
@@ -131,6 +131,21 @@
|
|||||||
<q-item-label> </q-item-label>
|
<q-item-label> </q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
<q-item tag="a" href="/account#theme" clickable v-close-popup
|
||||||
|
><q-item-section>
|
||||||
|
<q-icon
|
||||||
|
:name="$q.dark.isActive ? 'dark_mode' : 'light_mode'"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>
|
||||||
|
<span v-text="$t('theme')"></span>
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label> </q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
<q-separator></q-separator>
|
<q-separator></q-separator>
|
||||||
<q-item clickable v-close-popup @click="logout"
|
<q-item clickable v-close-popup @click="logout"
|
||||||
><q-item-section>
|
><q-item-section>
|
||||||
@@ -158,7 +173,22 @@
|
|||||||
show-if-above
|
show-if-above
|
||||||
:elevated="$q.screen.lt.md"
|
:elevated="$q.screen.lt.md"
|
||||||
>
|
>
|
||||||
<lnbits-wallet-list :balance="balance"></lnbits-wallet-list>
|
<q-item-label
|
||||||
|
:style="$q.dark.isActive ? 'color:rgba(255, 255, 255, 0.64)' : ''"
|
||||||
|
class="q-item__label q-item__label--header"
|
||||||
|
header
|
||||||
|
v-text="$t('wallets')"
|
||||||
|
></q-item-label>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
:icon=" walletFlip ? 'keyboard_arrow_right' : 'keyboard_arrow_down'"
|
||||||
|
class="absolute-top-right"
|
||||||
|
@click="flipWallets($q.screen.lt.md)"
|
||||||
|
></q-btn>
|
||||||
|
<lnbits-wallet-list
|
||||||
|
v-if="!walletFlip"
|
||||||
|
:balance="balance"
|
||||||
|
></lnbits-wallet-list>
|
||||||
|
|
||||||
<lnbits-manage
|
<lnbits-manage
|
||||||
:show-admin="'{{LNBITS_ADMIN_UI}}' == 'True'"
|
:show-admin="'{{LNBITS_ADMIN_UI}}' == 'True'"
|
||||||
@@ -172,6 +202,68 @@
|
|||||||
{% endblock %} {% block page_container %}
|
{% endblock %} {% block page_container %}
|
||||||
<q-page-container>
|
<q-page-container>
|
||||||
<q-page class="q-px-md q-py-lg" :class="{'q-px-lg': $q.screen.gt.xs}">
|
<q-page class="q-px-md q-py-lg" :class="{'q-px-lg': $q.screen.gt.xs}">
|
||||||
|
<q-scroll-area
|
||||||
|
v-if="walletFlip"
|
||||||
|
style="
|
||||||
|
height: 130px;
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="row no-wrap q-gutter-md q-pr-md">
|
||||||
|
<q-card
|
||||||
|
v-for="wallet in g.user.wallets"
|
||||||
|
:key="wallet.id"
|
||||||
|
class="wallet-list-card"
|
||||||
|
tag="a"
|
||||||
|
:href="wallet.url"
|
||||||
|
:style="
|
||||||
|
g.wallet && g.wallet.id === wallet.id
|
||||||
|
? `border: 1px solid ${primaryColor}; width: 250px; text-decoration: none;`
|
||||||
|
: 'width: 250px; text-decoration: none; border: 0px'
|
||||||
|
"
|
||||||
|
:class="{
|
||||||
|
'active-wallet-card': g.wallet && g.wallet.id === wallet.id
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row items-center">
|
||||||
|
<q-avatar
|
||||||
|
size="lg"
|
||||||
|
:color="
|
||||||
|
g.wallet && g.wallet.id === wallet.id
|
||||||
|
? $q.dark.isActive
|
||||||
|
? 'primary'
|
||||||
|
: 'primary'
|
||||||
|
: 'grey-5'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
name="flash_on"
|
||||||
|
:size="$q.dark.isActive ? '21px' : '20px'"
|
||||||
|
:color="$q.dark.isActive ? 'black' : 'grey-3'"
|
||||||
|
></q-icon>
|
||||||
|
</q-avatar>
|
||||||
|
<div
|
||||||
|
class="text-h6 q-pl-md"
|
||||||
|
:class="{
|
||||||
|
'text-bold': g.wallet && g.wallet.id === wallet.id
|
||||||
|
}"
|
||||||
|
v-text="wallet.name"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div class="row items-center q-pt-sm">
|
||||||
|
<h6 class="q-my-none text-no-wrap">
|
||||||
|
<strong v-text="wallet.fsat"></strong>
|
||||||
|
<small> {{LNBITS_DENOMINATION}}</small>
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</q-scroll-area>
|
||||||
|
|
||||||
{% block page %}{% endblock %}
|
{% block page %}{% endblock %}
|
||||||
</q-page>
|
</q-page>
|
||||||
</q-page-container>
|
</q-page-container>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
dense
|
dense
|
||||||
class="lnbits-drawer__q-list"
|
class="lnbits-drawer__q-list"
|
||||||
>
|
>
|
||||||
<q-item-label header v-text="$t('wallets')"></q-item-label>
|
|
||||||
<q-item
|
<q-item
|
||||||
v-for="wallet in wallets"
|
v-for="wallet in wallets"
|
||||||
:key="wallet.id"
|
:key="wallet.id"
|
||||||
@@ -53,7 +52,6 @@
|
|||||||
side
|
side
|
||||||
v-show="activeWallet && activeWallet.id === wallet.id"
|
v-show="activeWallet && activeWallet.id === wallet.id"
|
||||||
>
|
>
|
||||||
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
|
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-item clickable @click="showForm = !showForm">
|
<q-item clickable @click="showForm = !showForm">
|
||||||
@@ -506,23 +504,53 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template id="lnbits-update-balance">
|
<template id="lnbits-update-balance">
|
||||||
<q-btn v-if="admin" round color="primary" icon="add" size="sm">
|
<q-btn
|
||||||
|
v-if="admin && small_btn"
|
||||||
|
flat
|
||||||
|
round
|
||||||
|
color="primary"
|
||||||
|
size="sm"
|
||||||
|
icon="add"
|
||||||
|
>
|
||||||
<q-popup-edit class="bg-accent text-white" v-slot="scope" v-model="credit">
|
<q-popup-edit class="bg-accent text-white" v-slot="scope" v-model="credit">
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
:label="$t('credit_label', {denomination: denomination})"
|
:label="$t('credit_label', {denomination: denomination})"
|
||||||
:hint="$t('credit_hint')"
|
|
||||||
v-model="scope.value"
|
v-model="scope.value"
|
||||||
dense
|
dense
|
||||||
autofocus
|
autofocus
|
||||||
@keyup.enter="updateBalance(scope.value)"
|
@keyup.enter="scope.set"
|
||||||
>
|
>
|
||||||
<template v-slot:append>
|
<template v-slot:append>
|
||||||
<q-icon name="edit" />
|
<q-icon name="edit" />
|
||||||
</template>
|
</template>
|
||||||
</q-input>
|
</q-input>
|
||||||
</q-popup-edit>
|
</q-popup-edit>
|
||||||
<q-tooltip>Topup Wallet</q-tooltip>
|
<q-tooltip v-text="$t('credit_hint')"></q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
v-if="admin && !small_btn"
|
||||||
|
color="primary"
|
||||||
|
:label="$t('credit_debit')"
|
||||||
|
class="float-right q-mt-sm"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<q-popup-edit class="bg-accent text-white" v-slot="scope" v-model="credit">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
:label="$t('credit_label', {denomination: denomination})"
|
||||||
|
v-model="scope.value"
|
||||||
|
dense
|
||||||
|
autofocus
|
||||||
|
@keyup.enter="scope.set"
|
||||||
|
>
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-icon name="edit" />
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</q-popup-edit>
|
||||||
|
<q-tooltip v-text="$t('credit_hint')"></q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -570,7 +598,10 @@
|
|||||||
<template id="payment-list">
|
<template id="payment-list">
|
||||||
<div class="row items-center no-wrap q-mb-sm">
|
<div class="row items-center no-wrap q-mb-sm">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h5 class="text-subtitle1 q-my-none" :v-text="$t('transactions')"></h5>
|
<span
|
||||||
|
class="text-subtitle1 q-my-none col q-mr-sm"
|
||||||
|
v-text="$t('transactions')"
|
||||||
|
></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="gt-sm col-auto">
|
<div class="gt-sm col-auto">
|
||||||
<q-btn-dropdown
|
<q-btn-dropdown
|
||||||
@@ -578,7 +609,7 @@
|
|||||||
persistent
|
persistent
|
||||||
class="q-mr-sm"
|
class="q-mr-sm"
|
||||||
color="grey"
|
color="grey"
|
||||||
:label="$t('export_csv')"
|
label="Export"
|
||||||
split
|
split
|
||||||
@click="exportCSV(false)"
|
@click="exportCSV(false)"
|
||||||
>
|
>
|
||||||
@@ -627,26 +658,38 @@
|
|||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
</q-btn-dropdown>
|
</q-btn-dropdown>
|
||||||
<payment-chart :wallet="wallet" />
|
<payment-chart :wallet="wallet"></payment-chart>
|
||||||
|
<q-checkbox
|
||||||
|
v-model="failedPaymentsToggle"
|
||||||
|
checked-icon="warning"
|
||||||
|
unchecked-icon="warning_off"
|
||||||
|
:color="failedPaymentsToggle ? 'yellow' : 'grey'"
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
|
<q-tooltip>
|
||||||
|
<span v-text="`View failed payments`"></span>
|
||||||
|
</q-tooltip>
|
||||||
|
</q-checkbox>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<q-input
|
<div class="row q-my-md">
|
||||||
:style="
|
<q-input
|
||||||
$q.screen.lt.md
|
:style="
|
||||||
? {
|
$q.screen.lt.md
|
||||||
display: mobileSimple ? 'none !important' : ''
|
? {
|
||||||
}
|
display: mobileSimple ? 'none !important' : ''
|
||||||
: ''
|
}
|
||||||
"
|
: ''
|
||||||
filled
|
"
|
||||||
dense
|
filled
|
||||||
clearable
|
dense
|
||||||
v-model="paymentsTable.search"
|
clearable
|
||||||
debounce="300"
|
v-model="paymentsTable.search"
|
||||||
:placeholder="$t('search_by_tag_memo_amount')"
|
debounce="300"
|
||||||
class="q-mb-md"
|
:placeholder="$t('search_by_tag_memo_amount')"
|
||||||
>
|
class="col"
|
||||||
</q-input>
|
/>
|
||||||
|
</div>
|
||||||
<q-table
|
<q-table
|
||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
|
|||||||
+21
-6
@@ -1,6 +1,6 @@
|
|||||||
import base64
|
import base64
|
||||||
import getpass
|
import getpass
|
||||||
from hashlib import md5
|
from hashlib import md5, pbkdf2_hmac, sha256
|
||||||
|
|
||||||
from Cryptodome import Random
|
from Cryptodome import Random
|
||||||
from Cryptodome.Cipher import AES
|
from Cryptodome.Cipher import AES
|
||||||
@@ -8,6 +8,21 @@ from Cryptodome.Cipher import AES
|
|||||||
BLOCK_SIZE = 16
|
BLOCK_SIZE = 16
|
||||||
|
|
||||||
|
|
||||||
|
def random_secret_and_hash() -> tuple[str, str]:
|
||||||
|
secret = Random.new().read(32)
|
||||||
|
return secret.hex(), sha256(secret).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def fake_privkey(secret: str) -> str:
|
||||||
|
return pbkdf2_hmac(
|
||||||
|
"sha256",
|
||||||
|
secret.encode(),
|
||||||
|
b"FakeWallet",
|
||||||
|
2048,
|
||||||
|
32,
|
||||||
|
).hex()
|
||||||
|
|
||||||
|
|
||||||
class AESCipher:
|
class AESCipher:
|
||||||
"""This class is compatible with crypto-js/aes.js
|
"""This class is compatible with crypto-js/aes.js
|
||||||
|
|
||||||
@@ -48,18 +63,18 @@ class AESCipher:
|
|||||||
final_key += key
|
final_key += key
|
||||||
return final_key[:output]
|
return final_key[:output]
|
||||||
|
|
||||||
def decrypt(self, encrypted: str) -> str: # type: ignore
|
def decrypt(self, encrypted: str) -> str:
|
||||||
"""Decrypts a string using AES-256-CBC."""
|
"""Decrypts a string using AES-256-CBC."""
|
||||||
passphrase = self.passphrase
|
passphrase = self.passphrase
|
||||||
encrypted = base64.b64decode(encrypted) # type: ignore
|
encrypted_bytes = base64.b64decode(encrypted)
|
||||||
assert encrypted[0:8] == b"Salted__"
|
assert encrypted_bytes[0:8] == b"Salted__"
|
||||||
salt = encrypted[8:16]
|
salt = encrypted_bytes[8:16]
|
||||||
key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16)
|
key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16)
|
||||||
key = key_iv[:32]
|
key = key_iv[:32]
|
||||||
iv = key_iv[32:]
|
iv = key_iv[32:]
|
||||||
aes = AES.new(key, AES.MODE_CBC, iv)
|
aes = AES.new(key, AES.MODE_CBC, iv)
|
||||||
try:
|
try:
|
||||||
return self.unpad(aes.decrypt(encrypted[16:])).decode() # type: ignore
|
return self.unpad(aes.decrypt(encrypted_bytes[16:])).decode()
|
||||||
except UnicodeDecodeError as exc:
|
except UnicodeDecodeError as exc:
|
||||||
raise ValueError("Wrong passphrase") from exc
|
raise ValueError("Wrong passphrase") from exc
|
||||||
|
|
||||||
|
|||||||
+56
-107
@@ -1,10 +1,10 @@
|
|||||||
import asyncio
|
from typing import Optional
|
||||||
from typing import Callable, NamedTuple
|
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
import jsonpath_ng.ext as jpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import ExchangeRateProvider, settings
|
||||||
from lnbits.utils.cache import cache
|
from lnbits.utils.cache import cache
|
||||||
|
|
||||||
currencies = {
|
currencies = {
|
||||||
@@ -186,137 +186,86 @@ def allowed_currencies():
|
|||||||
return list(currencies.keys())
|
return list(currencies.keys())
|
||||||
|
|
||||||
|
|
||||||
class Provider(NamedTuple):
|
async def btc_rates(currency: str) -> list[tuple[str, float]]:
|
||||||
name: str
|
def replacements(ticker: str):
|
||||||
domain: str
|
return {
|
||||||
api_url: str
|
"FROM": "BTC",
|
||||||
getter: Callable
|
"from": "btc",
|
||||||
exclude_to: list = []
|
"TO": ticker.upper(),
|
||||||
|
"to": ticker.lower(),
|
||||||
|
}
|
||||||
|
|
||||||
|
async def fetch_price(
|
||||||
exchange_rate_providers = {
|
provider: ExchangeRateProvider,
|
||||||
# https://binance-docs.github.io/apidocs/spot/en/#symbol-price-ticker
|
) -> Optional[tuple[str, float]]:
|
||||||
"binance": Provider(
|
|
||||||
"Binance",
|
|
||||||
"binance.com",
|
|
||||||
"https://api.binance.com/api/v3/ticker/price?symbol={FROM}{TO}",
|
|
||||||
lambda data, replacements: data["price"],
|
|
||||||
["czk"],
|
|
||||||
),
|
|
||||||
"blockchain": Provider(
|
|
||||||
"Blockchain",
|
|
||||||
"blockchain.com",
|
|
||||||
"https://blockchain.info/tobtc?currency={TO}&value=1000000",
|
|
||||||
lambda data, replacements: 1000000 / data,
|
|
||||||
),
|
|
||||||
"exir": Provider(
|
|
||||||
"Exir",
|
|
||||||
"exir.io",
|
|
||||||
"https://api.exir.io/v1/ticker?symbol={from}-{to}",
|
|
||||||
lambda data, replacements: data["last"],
|
|
||||||
["czk", "eur"],
|
|
||||||
),
|
|
||||||
"bitfinex": Provider(
|
|
||||||
"Bitfinex",
|
|
||||||
"bitfinex.com",
|
|
||||||
"https://api.bitfinex.com/v1/pubticker/{from}{to}",
|
|
||||||
lambda data, replacements: data["last_price"],
|
|
||||||
["czk"],
|
|
||||||
),
|
|
||||||
"bitstamp": Provider(
|
|
||||||
"Bitstamp",
|
|
||||||
"bitstamp.net",
|
|
||||||
"https://www.bitstamp.net/api/v2/ticker/{from}{to}/",
|
|
||||||
lambda data, replacements: data["last"],
|
|
||||||
["czk"],
|
|
||||||
),
|
|
||||||
"coinbase": Provider(
|
|
||||||
"Coinbase",
|
|
||||||
"coinbase.com",
|
|
||||||
"https://api.coinbase.com/v2/exchange-rates?currency={FROM}",
|
|
||||||
lambda data, replacements: data["data"]["rates"][replacements["TO"]],
|
|
||||||
),
|
|
||||||
"coinmate": Provider(
|
|
||||||
"CoinMate",
|
|
||||||
"coinmate.io",
|
|
||||||
"https://coinmate.io/api/ticker?currencyPair={FROM}_{TO}",
|
|
||||||
lambda data, replacements: data["data"]["last"],
|
|
||||||
),
|
|
||||||
"kraken": Provider(
|
|
||||||
"Kraken",
|
|
||||||
"kraken.com",
|
|
||||||
"https://api.kraken.com/0/public/Ticker?pair=XBT{TO}",
|
|
||||||
lambda data, replacements: data["result"]["XXBTZ" + replacements["TO"]]["c"][0],
|
|
||||||
["czk"],
|
|
||||||
),
|
|
||||||
"bitpay": Provider(
|
|
||||||
"BitPay",
|
|
||||||
"bitpay.com",
|
|
||||||
"https://bitpay.com/rates",
|
|
||||||
lambda data, replacements: next(
|
|
||||||
i["rate"] for i in data["data"] if i["code"] == replacements["TO"]
|
|
||||||
),
|
|
||||||
),
|
|
||||||
"yadio": Provider(
|
|
||||||
"yadio",
|
|
||||||
"yadio.io",
|
|
||||||
"https://api.yadio.io/exrates/{FROM}",
|
|
||||||
lambda data, replacements: data[replacements["FROM"]][replacements["TO"]],
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def btc_price(currency: str) -> float:
|
|
||||||
replacements = {
|
|
||||||
"FROM": "BTC",
|
|
||||||
"from": "btc",
|
|
||||||
"TO": currency.upper(),
|
|
||||||
"to": currency.lower(),
|
|
||||||
}
|
|
||||||
|
|
||||||
async def fetch_price(provider: Provider):
|
|
||||||
if currency.lower() in provider.exclude_to:
|
if currency.lower() in provider.exclude_to:
|
||||||
raise Exception(f"Provider {provider.name} does not support {currency}.")
|
logger.warning(f"Provider {provider.name} does not support {currency}.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
ticker = provider.convert_ticker(currency)
|
||||||
|
url = provider.api_url.format(**replacements(ticker))
|
||||||
|
json_path = provider.path.format(**replacements(ticker))
|
||||||
|
|
||||||
url = provider.api_url.format(**replacements)
|
|
||||||
try:
|
try:
|
||||||
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:
|
||||||
r = await client.get(url, timeout=0.5)
|
r = await client.get(url, timeout=0.5)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
|
if not provider.path:
|
||||||
|
return provider.name, float(r.text.replace(",", ""))
|
||||||
data = r.json()
|
data = r.json()
|
||||||
return float(provider.getter(data, replacements))
|
price_query = jpx.parse(json_path)
|
||||||
|
result = price_query.find(data)
|
||||||
|
return provider.name, float(result[0].value)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Failed to fetch Bitcoin price "
|
f"Failed to fetch Bitcoin price "
|
||||||
f"for {currency} from {provider.name}: {e}"
|
f"for {currency} from {provider.name}: {e}"
|
||||||
)
|
)
|
||||||
raise
|
|
||||||
|
|
||||||
results = await asyncio.gather(
|
return None
|
||||||
*[fetch_price(provider) for provider in exchange_rate_providers.values()],
|
|
||||||
return_exceptions=True,
|
|
||||||
)
|
|
||||||
rates = [r for r in results if not isinstance(r, BaseException)]
|
|
||||||
|
|
||||||
|
# OK to be in squence: fetch_price times out after 0.5 seconds
|
||||||
|
results = [
|
||||||
|
await fetch_price(provider)
|
||||||
|
for provider in settings.lnbits_exchange_rate_providers
|
||||||
|
]
|
||||||
|
|
||||||
|
return [r for r in results if r is not None]
|
||||||
|
|
||||||
|
|
||||||
|
async def btc_price(currency: str) -> float:
|
||||||
|
rates = await btc_rates(currency)
|
||||||
if not rates:
|
if not rates:
|
||||||
return 9999999999
|
return 9999999999
|
||||||
elif len(rates) == 1:
|
elif len(rates) == 1:
|
||||||
logger.warning("Could only fetch one Bitcoin price.")
|
logger.warning("Could only fetch one Bitcoin price.")
|
||||||
|
|
||||||
return sum(rates) / len(rates)
|
rates_values = [r[1] for r in rates]
|
||||||
|
return sum(rates_values) / len(rates_values)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_fiat_rate_and_price_satoshis(currency: str) -> tuple[float, float]:
|
||||||
|
price = await cache.save_result(
|
||||||
|
lambda: btc_price(currency),
|
||||||
|
f"btc-price-{currency}",
|
||||||
|
settings.lnbits_exchange_rate_cache_seconds,
|
||||||
|
)
|
||||||
|
return float(100_000_000 / price), price
|
||||||
|
|
||||||
|
|
||||||
async def get_fiat_rate_satoshis(currency: str) -> float:
|
async def get_fiat_rate_satoshis(currency: str) -> float:
|
||||||
price = await cache.save_result(
|
rate, _ = await get_fiat_rate_and_price_satoshis(currency)
|
||||||
lambda: btc_price(currency), f"btc-price-{currency}"
|
return rate
|
||||||
)
|
|
||||||
return float(100_000_000 / price)
|
|
||||||
|
|
||||||
|
|
||||||
async def fiat_amount_as_satoshis(amount: float, currency: str) -> int:
|
async def fiat_amount_as_satoshis(amount: float, currency: str) -> int:
|
||||||
return int(amount * (await get_fiat_rate_satoshis(currency)))
|
rate = await get_fiat_rate_satoshis(currency)
|
||||||
|
return int(amount * (rate))
|
||||||
|
|
||||||
|
|
||||||
async def satoshis_amount_as_fiat(amount: float, currency: str) -> float:
|
async def satoshis_amount_as_fiat(amount: float, currency: str) -> float:
|
||||||
return float(amount / (await get_fiat_rate_satoshis(currency)))
|
rate = await get_fiat_rate_satoshis(currency)
|
||||||
|
return float(amount / rate)
|
||||||
|
|||||||
+19
-3
@@ -1,8 +1,11 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import TYPE_CHECKING, AsyncGenerator, Coroutine, NamedTuple, Optional
|
from typing import TYPE_CHECKING, AsyncGenerator, Coroutine, NamedTuple, Optional
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from lnbits.nodes.base import Node
|
from lnbits.nodes.base import Node
|
||||||
|
|
||||||
@@ -93,6 +96,9 @@ class Wallet(ABC):
|
|||||||
|
|
||||||
__node_cls__: Optional[type[Node]] = None
|
__node_cls__: Optional[type[Node]] = None
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.pending_invoices: list[str] = []
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def cleanup(self):
|
async def cleanup(self):
|
||||||
pass
|
pass
|
||||||
@@ -130,9 +136,19 @@ class Wallet(ABC):
|
|||||||
) -> Coroutine[None, None, PaymentStatus]:
|
) -> Coroutine[None, None, PaymentStatus]:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
while True:
|
||||||
pass
|
for invoice in self.pending_invoices:
|
||||||
|
try:
|
||||||
|
status = await self.get_invoice_status(invoice)
|
||||||
|
if status.paid:
|
||||||
|
yield invoice
|
||||||
|
self.pending_invoices.remove(invoice)
|
||||||
|
elif status.failed:
|
||||||
|
self.pending_invoices.remove(invoice)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error(f"could not get status of invoice {invoice}: '{exc}' ")
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
def normalize_endpoint(self, endpoint: str, add_proto=True) -> str:
|
def normalize_endpoint(self, endpoint: str, add_proto=True) -> str:
|
||||||
endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
|
endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
|
||||||
|
|||||||
+6
-11
@@ -1,6 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import hashlib
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from hashlib import sha256
|
||||||
from os import urandom
|
from os import urandom
|
||||||
from typing import AsyncGenerator, Dict, Optional, Set
|
from typing import AsyncGenerator, Dict, Optional, Set
|
||||||
|
|
||||||
@@ -16,6 +16,7 @@ from bolt11 import (
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
from lnbits.utils.crypto import fake_privkey
|
||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
InvoiceResponse,
|
InvoiceResponse,
|
||||||
@@ -35,14 +36,8 @@ class FakeWallet(Wallet):
|
|||||||
self.queue: asyncio.Queue = asyncio.Queue(0)
|
self.queue: asyncio.Queue = asyncio.Queue(0)
|
||||||
self.payment_secrets: Dict[str, str] = {}
|
self.payment_secrets: Dict[str, str] = {}
|
||||||
self.paid_invoices: Set[str] = set()
|
self.paid_invoices: Set[str] = set()
|
||||||
self.secret: str = settings.fake_wallet_secret
|
self.secret = settings.fake_wallet_secret
|
||||||
self.privkey: str = hashlib.pbkdf2_hmac(
|
self.privkey = fake_privkey(self.secret)
|
||||||
"sha256",
|
|
||||||
self.secret.encode(),
|
|
||||||
b"FakeWallet",
|
|
||||||
2048,
|
|
||||||
32,
|
|
||||||
).hex()
|
|
||||||
|
|
||||||
async def cleanup(self):
|
async def cleanup(self):
|
||||||
pass
|
pass
|
||||||
@@ -71,7 +66,7 @@ class FakeWallet(Wallet):
|
|||||||
elif unhashed_description:
|
elif unhashed_description:
|
||||||
tags.add(
|
tags.add(
|
||||||
TagChar.description_hash,
|
TagChar.description_hash,
|
||||||
hashlib.sha256(unhashed_description).hexdigest(),
|
sha256(unhashed_description).hexdigest(),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
tags.add(TagChar.description, memo or "")
|
tags.add(TagChar.description, memo or "")
|
||||||
@@ -85,7 +80,7 @@ class FakeWallet(Wallet):
|
|||||||
secret = urandom(32).hex()
|
secret = urandom(32).hex()
|
||||||
tags.add(TagChar.payment_secret, secret)
|
tags.add(TagChar.payment_secret, secret)
|
||||||
|
|
||||||
payment_hash = hashlib.sha256(secret.encode()).hexdigest()
|
payment_hash = sha256(secret.encode()).hexdigest()
|
||||||
|
|
||||||
tags.add(TagChar.payment_hash, payment_hash)
|
tags.add(TagChar.payment_hash, payment_hash)
|
||||||
|
|
||||||
|
|||||||
+21
-33
@@ -4,6 +4,7 @@ from typing import AsyncGenerator, Dict, Optional
|
|||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
from websockets.client import connect
|
||||||
|
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
@@ -36,6 +37,7 @@ class LNbitsWallet(Wallet):
|
|||||||
"missing lnbits_key or lnbits_admin_key or lnbits_invoice_key"
|
"missing lnbits_key or lnbits_admin_key or lnbits_invoice_key"
|
||||||
)
|
)
|
||||||
self.endpoint = self.normalize_endpoint(settings.lnbits_endpoint)
|
self.endpoint = self.normalize_endpoint(settings.lnbits_endpoint)
|
||||||
|
self.ws_url = f"{self.endpoint.replace('http', 'ws', 1)}/api/v1/ws/{key}"
|
||||||
self.headers = {"X-Api-Key": key, "User-Agent": settings.user_agent}
|
self.headers = {"X-Api-Key": key, "User-Agent": settings.user_agent}
|
||||||
self.client = httpx.AsyncClient(base_url=self.endpoint, headers=self.headers)
|
self.client = httpx.AsyncClient(base_url=self.endpoint, headers=self.headers)
|
||||||
|
|
||||||
@@ -194,38 +196,24 @@ class LNbitsWallet(Wallet):
|
|||||||
return PaymentPendingStatus()
|
return PaymentPendingStatus()
|
||||||
|
|
||||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
url = f"{self.endpoint}/api/v1/payments/sse"
|
|
||||||
|
|
||||||
while settings.lnbits_running:
|
while settings.lnbits_running:
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient(
|
async with connect(self.ws_url) as ws:
|
||||||
timeout=None, headers=self.headers
|
logger.info("connected to LNbits fundingsource websocket.")
|
||||||
) as client:
|
while settings.lnbits_running:
|
||||||
del client.headers[
|
message = await ws.recv()
|
||||||
"accept-encoding"
|
message_dict = json.loads(message)
|
||||||
] # we have to disable compression for SSEs
|
if (
|
||||||
async with client.stream(
|
message_dict
|
||||||
"GET", url, content="text/event-stream"
|
and message_dict.get("payment")
|
||||||
) as r:
|
and message_dict["payment"].get("payment_hash")
|
||||||
sse_trigger = False
|
):
|
||||||
async for line in r.aiter_lines():
|
payment_hash = message_dict["payment"]["payment_hash"]
|
||||||
# The data we want to listen to is of this shape:
|
logger.info(f"payment-received: {payment_hash}")
|
||||||
# event: payment-received
|
yield payment_hash
|
||||||
# data: {.., "payment_hash" : "asd"}
|
except Exception as exc:
|
||||||
if line.startswith("event: payment-received"):
|
logger.error(
|
||||||
sse_trigger = True
|
f"lost connection to LNbits fundingsource websocket: '{exc}'"
|
||||||
continue
|
"retrying in 5 seconds"
|
||||||
elif sse_trigger and line.startswith("data:"):
|
)
|
||||||
data = json.loads(line[len("data:") :])
|
await asyncio.sleep(5)
|
||||||
sse_trigger = False
|
|
||||||
yield data["payment_hash"]
|
|
||||||
else:
|
|
||||||
sse_trigger = False
|
|
||||||
|
|
||||||
except (OSError, httpx.ReadError, httpx.ConnectError, httpx.ReadTimeout):
|
|
||||||
pass
|
|
||||||
|
|
||||||
logger.error(
|
|
||||||
"lost connection to lnbits /payments/sse, retrying in 5 seconds"
|
|
||||||
)
|
|
||||||
await asyncio.sleep(5)
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user