Compare commits

...
84 Commits
Author SHA1 Message Date
ArcandGitHub efafd00b43 Merge pull request #1621 from lnbits/fix_adminui_lnd
fix inputs on Lnd funding sources
2023-04-20 09:51:19 +01:00
ArcandGitHub 07946d873f Merge pull request #1630 from lnbits/update/flake8
FEAT: improve on flake8 linting
2023-04-20 09:49:11 +01:00
ArcandGitHub 91b28ebf83 Merge pull request #1612 from lnbits/fix_admin_ext_access
Fix admin ext access
2023-04-20 09:46:18 +01:00
ArcandGitHub dfb6378976 Merge pull request #1627 from lnbits/fix/formatting
formatting did only run on branch main?
2023-04-20 09:41:17 +01:00
ArcandGitHub 64fde0093c Merge pull request #1636 from lnbits/improve-errors-lndrest
BUG: SSL certificate for lndrest is required even though it could be a publicly issued one
2023-04-20 09:39:52 +01:00
ArcandGitHub c8f9473e01 Merge pull request #1634 from lnbits/allow_bip21_scan
Allow for scanning BIP21 QR codes
2023-04-20 09:36:52 +01:00
ArcandGitHub fea6183aef Merge pull request #1608 from lnbits/feat/i18n
FEAT: add localisation to lnbits frontend
2023-04-20 09:25:35 +01:00
dni ⚡ adae3b4302 optional ssl certifiace for lndrest 2023-04-19 10:51:00 +02:00
Tiago Vasconcelos ce82439fd8 format 2023-04-18 17:02:49 +01:00
Tiago Vasconcelos 0a210f766d allow to scan BIP21 QR codes 2023-04-18 15:39:53 +01:00
dni ⚡andGitHub fe315d0a9b FEAT: remove python 3.7 and update dependencies (#1629)
* FEAT: remove python 3.7 and update dependencies

* readd pylint
2023-04-17 09:44:08 +02:00
dni ⚡andGitHub 156a19f05c improve errorhandling for lndrest #252 (#1631) 2023-04-17 09:26:37 +02:00
dni ⚡ 06fa6c10c1 reintroduce E203, black does not like that one 2023-04-17 08:38:12 +02:00
dni ⚡ 0b596c00ca reduce max string length 2023-04-17 08:29:01 +02:00
dni ⚡ 0af22a6256 E471 2023-04-17 08:21:45 +02:00
dni ⚡ 1ac9d2572d E203, E221 2023-04-17 08:16:59 +02:00
dni ⚡ 4a72a5b6b3 E241, E501 2023-04-17 08:14:04 +02:00
dni ⚡ b465ecf22a E265 2023-04-17 08:11:07 +02:00
dni ⚡ 42b7cd3046 fix E266 2023-04-17 08:09:34 +02:00
dni ⚡ 45b36b896b move flake config from setup.cfg to .flake8, to not confuse it with setup.cfg python package settings 2023-04-17 08:08:16 +02:00
dni ⚡ 99964ee6ab formatting did only run on branch main? 2023-04-16 09:16:24 +02:00
ArcandGitHub d82118db20 Merge pull request #1623 from lnbits/bug/fake_wallet_invoice
hotfix fakewallet
2023-04-14 11:13:25 +01:00
dni ⚡ ffb0f4ccb0 hotfix fakewallet 2023-04-14 12:03:06 +02:00
Tiago Vasconcelos de38f38723 fix inputs on Lnd funding sources 2023-04-13 11:43:19 +01:00
dni ⚡ 8c3b4a87b1 add i18n to frontend, still incomplete, but a start 2023-04-13 09:32:12 +02:00
dni ⚡ 8ee6be30eb add vue-i18n dependecy 2023-04-13 07:59:12 +02:00
dni ⚡andGitHub cf3b169480 FEAT: ignore extensions for checks, linting, prettier (#1617) 2023-04-13 07:57:14 +02:00
ArcandGitHub 95a718c6c0 Merge pull request #1614 from lnbits/arcbtc-patch-1
Link install guide
2023-04-11 17:29:18 +01:00
dni ⚡andGitHub 2473e0e962 FEAT: lock name inside package-lock, by adding name to package.json (#1616) 2023-04-11 10:31:44 +02:00
ArcandGitHub 90d1e1ebb6 Update README.md 2023-04-08 14:53:49 +01:00
Vlad Stan 554ad88cbd doc: add comments 2023-04-06 12:32:23 +03:00
Vlad Stan 0d14d2b56e fix: remove admin extensions for non admin users 2023-04-05 19:40:16 +03:00
Vlad Stan d7e7d89e9a fix: mypy 2023-04-05 19:03:14 +03:00
Vlad Stan 1d0fcaa579 feat: show content by accepted type 2023-04-05 18:54:54 +03:00
Vlad Stan af212c820c fix: block access admin extensions to normal users 2023-04-05 17:40:08 +03:00
Vlad Stan 0a74ca3972 fix: use extension code instead on name 2023-04-05 17:39:22 +03:00
ArcandGitHub 47df94178e Merge pull request #1468 from lnbits/pyright3
introduce pyright + fix issues (supersedes #1444)
2023-04-05 12:42:23 +01:00
dni ⚡ 255638a9ce remove elif for vlad 2023-04-05 11:39:01 +02:00
dni ⚡ 50149c360c add assert 2023-04-05 11:38:07 +02:00
dni ⚡ 6fae6dab92 remove *1000 for fake 2023-04-05 11:35:09 +02:00
dni ⚡ be5f909d1c merge issue in lnd_rest 2023-04-04 08:00:43 +02:00
dni ⚡ 2a1a0f3a2f fix pyright issues 2023-04-04 07:49:13 +02:00
dni ⚡ 5dbd9a9192 add some assert description 2023-04-04 07:35:20 +02:00
dni ⚡ 1f6a961840 fix cln mypy 2023-04-04 07:35:20 +02:00
callebtcanddni ⚡ 9b7bda4e4e finish services.py 2023-04-04 07:35:20 +02:00
callebtcanddni ⚡ a8103ba091 remove unnecessary checking_id from fakewallet 2023-04-04 07:35:20 +02:00
callebtcanddni ⚡ 234b508368 shorten and ignore cln.py 2023-04-04 07:35:19 +02:00
callebtcanddni ⚡ f766f9b2c3 comment 2023-04-04 07:35:05 +02:00
callebtcanddni ⚡ 370ec7e366 remove error fallback 2023-04-04 07:35:04 +02:00
dni ⚡ 0822058661 fix newly introduced pyright issue from rebase 2023-04-04 07:35:04 +02:00
dni ⚡ 7be46587db do not ignore core and wallets for pyright, bug probably got introduced while rebasing... 2023-04-04 07:35:04 +02:00
dni ⚡ fa58a5fa4c flake it 2023-04-04 07:35:04 +02:00
dni ⚡ 486631b455 move middleware to middleware.py 2023-04-04 07:35:04 +02:00
dni ⚡ 435743a265 add **kwargs to create_invoice functions 2023-04-04 07:34:51 +02:00
dni ⚡ 72386033a9 formatting 2023-04-04 07:34:51 +02:00
dni ⚡ 69c0cd0276 asserting broke payments 2023-04-04 07:34:51 +02:00
dni ⚡ abfee29676 uncomment webhook_listerner and add TODO 2023-04-04 07:34:50 +02:00
Pavol Rusnakanddni ⚡ 9c8a79eb9b fix pyright lnbits/wallets
Co-authored-by: dni  <office@dnilabs.com>
2023-04-04 07:34:50 +02:00
Pavol Rusnakanddni ⚡ 0607cb1d6e fix pyright lnbits/core
Co-authored-by: dni  <office@dnilabs.com>
2023-04-04 07:34:37 +02:00
Pavol Rusnakanddni ⚡ 02306148df fix pyright lnbits
Co-authored-by: dni  <office@dnilabs.com>
2023-04-04 07:34:17 +02:00
Pavol RusnakandGitHub 3855cf47f3 Merge pull request #1607 from lnbits/f-strings
small followup to #1597
2023-04-03 17:59:50 +02:00
Pavol Rusnak b2bc378a6f small followup to #1597 2023-04-03 17:54:55 +02:00
e59a218912 bugfix: 500 error if same wallet tries to pay an external invoice twice (#1594)
* bugfix: 500 error if same wallet tries to pay an external invoice twice

* fstring

* log the error

---------

Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com>
2023-04-03 15:34:55 +02:00
689c443d72 increase readability by using f-strings (#1597)
* increase readability by using f-strings

* readd merge conflict stuff

---------

Co-authored-by: calle <93376500+callebtc@users.noreply.github.com>
2023-04-03 15:34:17 +02:00
calleandGitHub 58c3b05e29 fix internal payment check (#1604) 2023-04-03 15:15:34 +02:00
8ce84ce592 FEAT: Filters for GET requests, add it to GET /payments (#1557)
* feat filters, add them to GET payments

* add limit and offset to filters (#1563)

* add limit and offset to filters
* move filters example to parse_filters doc string

* black

* add openapi docs

* remove example commentC

* improve typing and make nested filter possible in openapi

* typo in fn name

* readd Type

---------

Co-authored-by: jackstar12 <62219658+jackstar12@users.noreply.github.com>
Co-authored-by: calle <93376500+callebtc@users.noreply.github.com>
2023-04-03 14:55:49 +02:00
fe9e821af5 BUG: proper exception for already paid internal invoices (#1593)
* BUG: proper exception for paid interal invoices

* test should now fail, because we pay invoice twice, and should also work in regtest

* sorting

* unpack check_internal

* introduce another crud fn for checking internal paid payment

* rename

---------

Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com>
2023-04-03 14:44:17 +02:00
dni ⚡andGitHub a9bdf24425 FEAT: use versioning for frontend (npm) and copy it to lnbits/static/vendor for easier updating (#1590)
* remove static/vendor

* add node dependencies

* add bolt11-decoder

* run npm install inside dockerimage

* only use bundle.js and bundle.css

* use node_modules for bundling vendor assets

* remove dead code

* make argument optional

* reintroduce vendor dir

* reintroduce vendor and single javascript files, minification

* wrong moment, remove minification

* lock packages with non critical issues

* black
2023-03-31 12:46:24 +02:00
dni ⚡andGitHub 43c9c9754b FEAT: #1552 add copy payment_hash to payment details (#1591)
* add copy to payment details

* icon for calle
2023-03-30 14:30:50 +02:00
dni ⚡andGitHub 9127ebc621 FIX: admin topup to use payment_hash (#1588)
* FIX: admin topup to use payment_hash

* flake8
2023-03-30 14:21:01 +02:00
dni ⚡andGitHub 407a61cd29 add version to fastapi app and startup log (#1568)
* add version to fastapi app and startup log

* move version into settings

* remove unused dep
2023-03-30 14:19:45 +02:00
calleandGitHub 994b203a8e return fee in status (#1598) 2023-03-29 19:40:52 +02:00
Vlad Stan 239f166a14 fix: format relative path 2023-03-20 16:07:20 +02:00
Vlad Stan e1e34def8c fix: correct method names for Path 2023-03-20 16:07:20 +02:00
Pavol RusnakandVlad Stan a805d2a0e8 refactor extension_manager to use pathlib.Path instead of os.path.join 2023-03-20 16:07:20 +02:00
Pavol RusnakandGitHub 281d37f79c Merge pull request #1582 from lnbits/docs-nginx
docs: add nginx ssl proxy info to installation
2023-03-19 09:27:14 +01:00
Pavol RusnakandGitHub 0039c60a27 Merge pull request #1583 from lnbits/mypy-pyproject-simplify
simplify exclude syntax for mypy in pyproject.toml
2023-03-18 20:49:23 +01:00
Pavol RusnakandGitHub 3317bc1bc5 Merge pull request #1578 from lnbits/flake-update
flake: update to latest nixpkgs
2023-03-17 23:10:55 +01:00
Pavol Rusnak 9cf43dcea1 simplify exclude syntax for mypy in pyproject.toml 2023-03-17 18:05:43 +01:00
Pavol Rusnak b3910da6d7 flake: update to latest nixpkgs 2023-03-17 18:03:33 +01:00
Pavol Rusnak ec1caa6015 docs: fix readability of HTTPS section in installation 2023-03-17 15:13:25 +01:00
Pavol Rusnak 0d8d580fc9 docs: add nginx ssl proxy info to installation 2023-03-17 15:10:15 +01:00
4dd206da2a good bye venv (#1569)
Co-authored-by: calle <93376500+callebtc@users.noreply.github.com>
2023-03-16 16:45:23 +01:00
Vlad StanandGitHub fbe982af36 fix: assume no pre-installed extensions (#1558)
* fix: assume no pre-installed extensions

* chore: code format
2023-03-15 11:26:07 +01:00
94 changed files with 53457 additions and 1384 deletions
-1
View File
@@ -3,7 +3,6 @@ data
docker
docs
tests
venv
lnbits/static/css/*
lnbits/static/bundle.js
+16
View File
@@ -0,0 +1,16 @@
[flake8]
max-line-length = 150
exclude = lnbits/wallets/lnd_grpc_files/, lnbits/extensions/
ignore =
# E203 whitespace before ':' black does not like it
E203
# E402: module level import not at top of file
E402,
# W503: line break before binary operator
W503,
# F821: undefined name - should be addressed in future PR
F821,
# E722 do not use bare 'except' - should be addressed in future PR
E722,
# flake8-requirements import checks
I
+1 -5
View File
@@ -1,10 +1,6 @@
name: formatting
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
on: [push, pull_request]
jobs:
checks:
-23
View File
@@ -3,29 +3,6 @@ name: tests
on: [push, pull_request]
jobs:
venv-sqlite:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9"]
poetry-version: ["1.3.1"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
env:
VIRTUAL_ENV: ./venv
PATH: ${{ env.VIRTUAL_ENV }}/bin:${{ env.PATH }}
run: |
python -m venv ${{ env.VIRTUAL_ENV }}
./venv/bin/python -m pip install --upgrade pip
./venv/bin/pip install -r requirements.txt
./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock
- name: Run tests
run: make test-venv
sqlite:
runs-on: ubuntu-latest
strategy:
-2
View File
@@ -23,8 +23,6 @@ tests/data/*.sqlite3
*.pyc
*.env
.env
.venv
venv
data
*.sqlite3
+2
View File
@@ -5,6 +5,8 @@
*.yml
**/lnbits/extensions/*
**/lnbits/static/vendor
**/lnbits/static/bundle.*
**/lnbits/static/css/*
+20 -9
View File
@@ -1,6 +1,6 @@
.PHONY: test
all: format check requirements.txt
all: format check
format: prettier isort black
@@ -50,14 +50,6 @@ test-real-wallet:
DEBUG=true \
poetry run pytest
test-venv:
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
FAKE_WALLET_SECRET="ToTheMoon1" \
LNBITS_DATA_FOLDER="./tests/data" \
PYTHONUNBUFFERED=1 \
DEBUG=true \
./venv/bin/pytest --durations=1 -s --cov=lnbits --cov-report=xml tests
test-migration:
rm -rf ./migration-data
mkdir -p ./migration-data
@@ -79,3 +71,22 @@ migration:
bak:
# LNBITS_DATABASE_URL=postgres://postgres:postgres@0.0.0.0:5432/postgres
#
updatevendor:
npm install
cp ./node_modules/moment/moment.js ./lnbits/static/vendor/
cp ./node_modules/underscore/underscore.js ./lnbits/static/vendor/
cp ./node_modules/axios/dist/axios.js ./lnbits/static/vendor/
cp ./node_modules/vue/dist/vue.js ./lnbits/static/vendor/
cp ./node_modules/vue-router/dist/vue-router.js ./lnbits/static/vendor/
cp ./node_modules/vue-qrcode-reader/dist/vue-qrcode-reader.browser.js ./lnbits/static/vendor/
cp ./node_modules/@chenfengyuan/vue-qrcode/dist/vue-qrcode.js ./lnbits/static/vendor/
cp ./node_modules/vuex/dist/vuex.js ./lnbits/static/vendor/
cp ./node_modules/quasar/dist/quasar.ie.polyfills.umd.min.js ./lnbits/static/vendor/
cp ./node_modules/quasar/dist/quasar.umd.js ./lnbits/static/vendor/
cp ./node_modules/chart.js/dist/Chart.bundle.js ./lnbits/static/vendor/
cp ./node_modules/quasar/dist/quasar.css ./lnbits/static/vendor/
cp ./node_modules/chart.js/dist/Chart.css ./lnbits/static/vendor/
cp ./node_modules/vue-qrcode-reader/dist/vue-qrcode-reader.css ./lnbits/static/vendor/
cp ./node_modules/vue-i18n/dist/vue-i18n.js ./lnbits/static/vendor/
+1 -1
View File
@@ -33,7 +33,7 @@ LNbits is inspired by all the great work of [opennode.com](https://www.opennode.
## Running LNbits
See the [install guide](https://github.com/lnbits/lnbits/wiki/) for details on installation and setup.
See the [install guide](https://github.com/lnbits/lnbits/wiki/LNbits-Documentation#install-options) for details on installation and setup.
## LNbits as an account system
+3 -5
View File
@@ -31,18 +31,16 @@ Going over the example extension's structure:
Adding new dependencies
-----------------------
DO NOT ADD NEW DEPENDENCIES. Try to use the dependencies that are available in `pyproject.toml`. Getting the LNbits project to accept a new dependency is time consuming and uncertain, and may result in your extension NOT being made available to others.
DO NOT ADD NEW DEPENDENCIES. Try to use the dependencies that are availabe in `pyproject.toml`. Getting the LNbits project to accept a new dependency is time consuming and uncertain, and may result in your extension NOT being made available to others.
If for some reason your extensions must have a new python package to work, and its needs are not met in `pyproject.toml`, you can add a new package using `venv`, or `poerty`:
If for some reason your extensions must have a new python package to work, and its nees are not met in `pyproject.toml`, you can add a new package using `poerty`:
```sh
$ poetry add <package>
# or
$ ./venv/bin/pip install <package>
```
**But we need an extra step to make sure LNbits doesn't break in production.**
Dependencies need to be added to `pyproject.toml` and `requirements.txt`, then tested by running on `venv` and `poetry` compatibility can be tested with `nix build .#checks.x86_64-linux.vmTest`.
Dependencies need to be added to `pyproject.toml`, then tested by running on `poetry` compatability can be tested with `nix build .#checks.x86_64-linux.vmTest`.
SQLite to PostgreSQL migration
+70 -36
View File
@@ -6,7 +6,7 @@ nav_order: 2
# Basic installation
You can choose between four package managers, `poetry`, `nix` and `venv`.
You can choose between four package managers, `poetry` and `nix`
By default, LNbits will use SQLite as its database. You can also use PostgreSQL which is recommended for applications with a high load (see guide below).
@@ -80,30 +80,8 @@ mkdir data
LNBITS_DATA_FOLDER=data LNBITS_BACKEND_WALLET_CLASS=LNbitsWallet LNBITS_ENDPOINT=https://legend.lnbits.com LNBITS_KEY=7b1a78d6c78f48b09a202f2dcb2d22eb ./result/bin/lnbits --port 9000
```
## Option 3: venv
```sh
git clone https://github.com/lnbits/lnbits.git
cd lnbits
# ensure you have virtualenv installed, on debian/ubuntu 'apt install python3.9-venv'
python3.9 -m venv venv
# If you have problems here, try `sudo apt install -y pkg-config libpq-dev`
./venv/bin/pip install -r requirements.txt
# create the data folder and the .env file
mkdir data && cp .env.example .env
# build the static files
./venv/bin/python tools/build.py
```
#### Running the server
```sh
./venv/bin/uvicorn lnbits.__main__:app --port 5000
```
If you want to host LNbits on the internet, run with the option `--host 0.0.0.0`.
## Option 4: Docker
## Option 3: Docker
use latest version from docker hub
```sh
@@ -122,7 +100,7 @@ mkdir data
docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbitsdocker/lnbits-legend
```
## Option 5: Fly.io
## Option 4: Fly.io
Fly.io is a docker container hosting platform that has a generous free tier. You can host LNbits for free on Fly.io for personal use.
@@ -210,9 +188,6 @@ sudo apt install python3.9-dev gcc build-essential
# if the secp256k1 build fails:
# if you used poetry
poetry add setuptools wheel
# if you used venv
./venv/bin/pip install setuptools wheel
```
#### Poetry
@@ -323,7 +298,8 @@ Save the file and run the following commands:
sudo systemctl enable lnbits.service
sudo systemctl start lnbits.service
```
## Reverse proxy with automatic https using Caddy
## Reverse proxy with automatic HTTPS using Caddy
Use Caddy to make your LNbits install accessible over clearnet with a domain and https cert.
@@ -335,11 +311,15 @@ https://caddyserver.com/docs/install#debian-ubuntu-raspbian
```
sudo caddy stop
```
Create a Caddyfile
```
sudo nano Caddyfile
```
Assuming your LNbits is running on port `5000` add:
```
yourdomain.com {
handle /api/v1/payments/sse* {
@@ -356,22 +336,30 @@ yourdomain.com {
}
}
```
Save and exit `CTRL + x`
```
sudo caddy start
```
## Running behind an apache2 reverse proxy over https
Install apache2 and enable apache2 mods
## Running behind an Apache2 reverse proxy over HTTPS
Install Apache2 and enable Apache2 mods:
```sh
apt-get install apache2 certbot
a2enmod headers ssl proxy proxy-http
```
create a ssl certificate with letsencrypt
Create a SSL certificate with LetsEncrypt:
```sh
certbot certonly --webroot --agree-tos --text --non-interactive --webroot-path /var/www/html -d lnbits.org
certbot certonly --webroot --agree-tos --non-interactive --webroot-path /var/www/html -d lnbits.org
```
create a apache2 vhost at: /etc/apache2/sites-enabled/lnbits.conf
Create an Apache2 vhost at: `/etc/apache2/sites-enabled/lnbits.conf`:
```sh
cat <<EOF > /etc/apache2/sites-enabled/lnbits.conf
<VirtualHost *:443>
@@ -396,11 +384,57 @@ cat <<EOF > /etc/apache2/sites-enabled/lnbits.conf
</VirtualHost>
EOF
```
restart apache2
Restart Apache2:
```sh
service restart apache2
```
## Running behind an Nginx reverse proxy over HTTPS
Install nginx:
```sh
apt-get install nginx certbot
```
Create a SSL certificate with LetsEncrypt:
```sh
certbot certonly --nginx --agree-tos -d lnbits.org
```
Create an nginx vhost at `/etc/nginx/sites-enabled/lnbits.org`:
```sh
cat <<EOF > /etc/nginx/sites-enabled/lnbits.org
server {
server_name lnbits.org;
location / {
proxy_pass http://127.0.0.1:5000;
}
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
listen [::]:443 ssl;
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/lnbits.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/lnbits.org/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
EOF
```
Restart nginx:
```sh
service restart nginx
```
## Using https without reverse proxy
The most common way of using LNbits via https is to use a reverse proxy such as Caddy, nginx, or ngriok. However, you can also run LNbits via https without additional software. This is useful for development purposes or if you want to use LNbits in your local network.
@@ -433,7 +467,7 @@ mkcert localhost 127.0.0.1 ::1
You can then pass the certificate files to uvicorn when you start LNbits:
```sh
./venv/bin/uvicorn lnbits.__main__:app --host 0.0.0.0 --port 5000 --ssl-keyfile ./key.pem --ssl-certfile ./cert.pem
poetry run uvicorn lnbits.__main__:app --host 0.0.0.0 --port 5000 --ssl-keyfile ./key.pem --ssl-certfile ./cert.pem
```
+2 -2
View File
@@ -47,7 +47,7 @@ You can also use an AES-encrypted macaroon (more info) instead by using
- `LND_GRPC_MACAROON_ENCRYPTED`: eNcRyPtEdMaCaRoOn
To encrypt your macaroon, run `./venv/bin/python lnbits/wallets/macaroon/macaroon.py`.
To encrypt your macaroon, run `poetry run python lnbits/wallets/macaroon/macaroon.py`.
### LNbits
@@ -57,7 +57,7 @@ To encrypt your macaroon, run `./venv/bin/python lnbits/wallets/macaroon/macaroo
### LNPay
For the invoice listener to work you have a publicly accessible URL in your LNbits and must set up [LNPay webhooks](https://dashboard.lnpay.co/webhook/) pointing to `<your LNbits host>/wallet/webhook` with the "Wallet Receive" event and no secret. For example, `https://mylnbits/wallet/webhook` will be the Endpoint Url that gets notified about the payment.
For the invoice listener to work you have a publicly accessible URL in your LNbits and must set up [LNPay webhooks](https://dashboard.lnpay.co/webhook/) pointing to `<your LNbits host>/wallet/webhook` with the "Wallet Receive" event and no secret. For example, `https://mylnbits/wallet/webhook` will be the Endpoint Url that gets notified about the payment.
- `LNBITS_BACKEND_WALLET_CLASS`: **LNPayWallet**
- `LNPAY_API_ENDPOINT`: https://api.lnpay.co/v1/
Generated
+12 -25
View File
@@ -2,11 +2,11 @@
"nodes": {
"flake-utils": {
"locked": {
"lastModified": 1656928814,
"narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=",
"lastModified": 1676283394,
"narHash": "sha256-XX2f9c3iySLCw54rJ/CZs+ZK6IQy7GXNY4nSOyu2QG4=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249",
"rev": "3db36a8b464d0c4532ba1c7dda728f4576d6d073",
"type": "github"
},
"original": {
@@ -17,11 +17,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1657114324,
"narHash": "sha256-fWuaUNXrHcz/ciHRHlcSO92dvV3EVS0GJQUSBO5JIB4=",
"lastModified": 1678470307,
"narHash": "sha256-OEeMUr3ueLIXyW/OaFUX5jUdimyQwMg/7e+/Q0gC/QE=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "a5c867d9fe9e4380452628e8f171c26b69fa9d3d",
"rev": "0c4800d579af4ed98ecc47d464a5e7b0870c4b1f",
"type": "github"
},
"original": {
@@ -31,32 +31,19 @@
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1657261001,
"narHash": "sha256-sUZeuRYfhG59uD6xafM07bc7bAIkpcGq84Vj4B+cyms=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "0be91cefefde5701f8fa957904618a13e3bb51d8",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"type": "github"
}
},
"poetry2nix": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_2"
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1657149754,
"narHash": "sha256-iSnZoqwNDDVoO175whSuvl4sS9lAb/2zZ3Sa4ywo970=",
"lastModified": 1678513806,
"narHash": "sha256-bEto1lp9bIQ+DlJsXQyRxN5l6B/oy3Skb7DueYRJJBo=",
"owner": "nix-community",
"repo": "poetry2nix",
"rev": "fc1930e011dea149db81863aac22fe701f36f1b5",
"rev": "bf239d53fafb53cf439a72b3a50e86dd6a3984a5",
"type": "github"
},
"original": {
+7 -2
View File
@@ -1,7 +1,12 @@
{
description = "LNbits, free and open-source Lightning wallet and accounts system";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
poetry2nix.url = "github:nix-community/poetry2nix";
poetry2nix = {
url = "github:nix-community/poetry2nix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, poetry2nix }@inputs:
let
@@ -17,6 +22,7 @@
default = pkgs.mkShell {
buildInputs = with pkgs; [
nodePackages.prettier
poetry
];
};
});
@@ -29,7 +35,6 @@
default = self.packages.${system}.${projectName};
${projectName} = pkgs.poetry2nix.mkPoetryApplication {
projectDir = ./.;
python = pkgs.python39;
};
});
nixosModules = {
+8 -22
View File
@@ -33,12 +33,7 @@ from .core import (
from .core.services import check_admin_settings
from .core.views.generic import core_html_routes
from .extension_manager import Extension, InstallableExtension, get_valid_extensions
from .helpers import (
get_css_vendored,
get_js_vendored,
template_renderer,
url_for_vendored,
)
from .helpers import template_renderer
from .middleware import ExtensionsRedirectMiddleware, InstalledExtensionMiddleware
from .requestvars import g
from .tasks import (
@@ -57,6 +52,7 @@ def create_app() -> FastAPI:
app = FastAPI(
title="LNbits API",
description="API for LNbits, the free and open source bitcoin wallet and accounts system with plugins.",
version=settings.version,
license_info={
"name": "MIT License",
"url": "https://raw.githubusercontent.com/lnbits/lnbits/main/LICENSE",
@@ -83,7 +79,6 @@ def create_app() -> FastAPI:
app.add_middleware(ExtensionsRedirectMiddleware)
register_startup(app)
register_assets(app)
register_routes(app)
register_async_tasks(app)
register_exception_handlers(app)
@@ -202,7 +197,7 @@ def check_installed_extension(ext: InstallableExtension) -> bool:
os.path.join(settings.lnbits_data_folder, "extensions", "*.zip")
)
if ext.zip_path not in zip_files:
if f"./{str(ext.zip_path)}" not in zip_files:
ext.download_archive()
ext.extract_archive()
@@ -305,6 +300,7 @@ def register_startup(app: FastAPI):
def log_server_info():
logger.info("Starting LNbits")
logger.info(f"Version: {settings.version}")
logger.info(f"Baseurl: {settings.lnbits_baseurl}")
logger.info(f"Host: {settings.host}")
logger.info(f"Port: {settings.port}")
@@ -328,19 +324,6 @@ def get_db_vendor_name():
)
def register_assets(app: FastAPI):
"""Serve each vendored asset separately or a bundle."""
@app.on_event("startup")
async def vendored_assets_variable():
if settings.debug:
g().VENDORED_JS = map(url_for_vendored, get_js_vendored())
g().VENDORED_CSS = map(url_for_vendored, get_css_vendored())
else:
g().VENDORED_JS = ["/static/bundle.js"]
g().VENDORED_CSS = ["/static/bundle.css"]
def register_async_tasks(app):
@app.route("/wallet/webhook")
async def webhook_listener():
@@ -445,7 +428,10 @@ class Formatter:
self.padding = 0
self.minimal_fmt: str = "<green>{time:YYYY-MM-DD HH:mm:ss.SS}</green> | <level>{level}</level> | <level>{message}</level>\n"
if settings.debug:
self.fmt: str = "<green>{time:YYYY-MM-DD HH:mm:ss.SS}</green> | <level>{level: <4}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> | <level>{message}</level>\n"
self.fmt: str = (
"<green>{time:YYYY-MM-DD HH:mm:ss.SS}</green> | <level>{level: <4}</level> | "
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> | <level>{message}</level>\n"
)
else:
self.fmt: str = self.minimal_fmt
+33 -30
View File
@@ -66,11 +66,12 @@ def decode(pr: str) -> Invoice:
invoice.amount_msat = _unshorten_amount(amountstr)
# pull out date
invoice.date = data.read(35).uint
date_bin = data.read(35)
invoice.date = date_bin.uint # type: ignore
while data.pos != data.len:
tag, tagdata, data = _pull_tagged(data)
data_length = len(tagdata) / 5
data_length = len(tagdata or []) / 5
if tag == "d":
invoice.description = _trim_to_bytes(tagdata).decode()
@@ -79,7 +80,7 @@ def decode(pr: str) -> Invoice:
elif tag == "p" and data_length == 52:
invoice.payment_hash = _trim_to_bytes(tagdata).hex()
elif tag == "x":
invoice.expiry = tagdata.uint
invoice.expiry = tagdata.uint # type: ignore
elif tag == "n":
invoice.payee = _trim_to_bytes(tagdata).hex()
# this won't work in most cases, we must extract the payee
@@ -90,11 +91,11 @@ def decode(pr: str) -> Invoice:
s = bitstring.ConstBitStream(tagdata)
while s.pos + 264 + 64 + 32 + 32 + 16 < s.len:
route = Route(
pubkey=s.read(264).tobytes().hex(),
short_channel_id=_readable_scid(s.read(64).intbe),
base_fee_msat=s.read(32).intbe,
ppm_fee=s.read(32).intbe,
cltv=s.read(16).intbe,
pubkey=s.read(264).tobytes().hex(), # type: ignore
short_channel_id=_readable_scid(s.read(64).intbe), # type: ignore
base_fee_msat=s.read(32).intbe, # type: ignore
ppm_fee=s.read(32).intbe, # type: ignore
cltv=s.read(16).intbe, # type: ignore
)
invoice.route_hints.append(route)
@@ -171,7 +172,7 @@ def lnencode(addr, privkey):
else:
amount = addr.currency if addr.currency else ""
hrp = "ln" + amount + "0n"
hrp = f"ln{amount}0n"
# Start with the timestamp
data = bitstring.pack("uint:35", addr.date)
@@ -202,7 +203,8 @@ def lnencode(addr, privkey):
)
data += tagged("r", route)
elif k == "f":
data += encode_fallback(v, addr.currency)
# NOTE: there was an error fallback here that's now removed
continue
elif k == "d":
data += tagged_bytes("d", v.encode())
elif k == "x":
@@ -244,7 +246,13 @@ def lnencode(addr, privkey):
class LnAddr:
def __init__(
self, paymenthash=None, amount=None, currency="bc", tags=None, date=None
self,
paymenthash=None,
amount=None,
currency="bc",
tags=None,
date=None,
fallback=None,
):
self.date = int(time.time()) if not date else int(date)
self.tags = [] if not tags else tags
@@ -252,12 +260,14 @@ class LnAddr:
self.paymenthash = paymenthash
self.signature = None
self.pubkey = None
self.fallback = fallback
self.currency = currency
self.amount = amount
def __str__(self):
pubkey = bytes.hex(self.pubkey.serialize()).decode()
tags = ", ".join([k + "=" + str(v) for k, v in self.tags])
assert self.pubkey, "LnAddr, pubkey must be set"
pubkey = bytes.hex(self.pubkey.serialize())
tags = ", ".join([f"{k}={v}" for k, v in self.tags])
return f"LnAddr[{pubkey}, amount={self.amount}{self.currency} tags=[{tags}]]"
@@ -266,6 +276,7 @@ def shorten_amount(amount):
# Convert to pico initially
amount = int(amount * 10**12)
units = ["p", "n", "u", "m", ""]
unit = ""
for unit in units:
if amount % 1000 == 0:
amount //= 1000
@@ -304,32 +315,24 @@ def _pull_tagged(stream):
return (CHARSET[tag], stream.read(length * 5), stream)
def is_p2pkh(currency, prefix):
return prefix == base58_prefix_map[currency][0]
def is_p2sh(currency, prefix):
return prefix == base58_prefix_map[currency][1]
# Tagged field containing BitArray
def tagged(char, l):
def tagged(char, bits):
# Tagged fields need to be zero-padded to 5 bits.
while l.len % 5 != 0:
l.append("0b0")
while bits.len % 5 != 0:
bits.append("0b0")
return (
bitstring.pack(
"uint:5, uint:5, uint:5",
CHARSET.find(char),
(l.len / 5) / 32,
(l.len / 5) % 32,
(bits.len / 5) / 32,
(bits.len / 5) % 32,
)
+ l
+ bits
)
def tagged_bytes(char, l):
return tagged(char, bitstring.BitArray(l))
def tagged_bytes(char, bits):
return tagged(char, bitstring.BitArray(bits))
def _trim_to_bytes(barr):
@@ -359,5 +362,5 @@ def bitarray_to_u5(barr):
ret = []
s = bitstring.ConstBitStream(barr)
while s.pos != s.len:
ret.append(s.read(5).uint)
ret.append(s.read(5).uint) # type: ignore
return ret
+1 -15
View File
@@ -13,7 +13,6 @@ from .core.crud import get_dbversions, get_inactive_extensions
from .core.helpers import migrate_extension_database, run_migration
from .db import COCKROACH, POSTGRES, SQLITE
from .extension_manager import get_valid_extensions
from .helpers import get_css_vendored, get_js_vendored, url_for_vendored
@click.command("migrate")
@@ -24,7 +23,6 @@ def db_migrate():
@click.command("assets")
def handle_assets():
transpile_scss()
bundle_vendored()
def transpile_scss():
@@ -39,23 +37,11 @@ def transpile_scss():
css.write(compile_string(scss.read()))
def bundle_vendored():
for getfiles, outputpath in [
(get_js_vendored, os.path.join(settings.lnbits_path, "static/bundle.js")),
(get_css_vendored, os.path.join(settings.lnbits_path, "static/bundle.css")),
]:
output = ""
for path in getfiles():
with open(path) as f:
output += "/* " + url_for_vendored(path) + " */\n" + f.read() + ";\n"
with open(outputpath, "w") as f:
f.write(output)
async def migrate_databases():
"""Creates the necessary databases if they don't exist already; or migrates them."""
async with core_db.connect() as conn:
exists = False
if conn.type == SQLITE:
exists = await conn.fetchone(
"SELECT * FROM sqlite_master WHERE type='table' AND name='dbversions'"
+39 -23
View File
@@ -7,7 +7,7 @@ from uuid import uuid4
import shortuuid
from lnbits import bolt11
from lnbits.db import COCKROACH, POSTGRES, Connection
from lnbits.db import COCKROACH, POSTGRES, Connection, Filters
from lnbits.extension_manager import InstallableExtension
from lnbits.settings import AdminSettings, EditableSettings, SuperSettings, settings
@@ -62,7 +62,9 @@ async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[
return User(
id=user["id"],
email=user["email"],
extensions=[e[0] for e in extensions],
extensions=[
e[0] for e in extensions if User.is_extension_for_user(e[0], user["id"])
],
wallets=[Wallet(**w) for w in wallets],
admin=user["id"] == settings.super_user
or user["id"] in settings.lnbits_admin_users,
@@ -206,7 +208,7 @@ async def create_wallet(
async def update_wallet(
wallet_id: str, new_name: str, conn: Optional[Connection] = None
) -> Optional[Wallet]:
return await (conn or db).execute(
await (conn or db).execute(
"""
UPDATE wallets SET
name = ?
@@ -214,6 +216,9 @@ async def update_wallet(
""",
(new_name, wallet_id),
)
wallet = await get_wallet(wallet_id=wallet_id, conn=conn)
assert wallet, "updated created wallet couldn't be retrieved"
return wallet
async def delete_wallet(
@@ -347,8 +352,7 @@ async def get_payments(
incoming: bool = False,
since: Optional[int] = None,
exclude_uncheckable: bool = False,
limit: Optional[int] = None,
offset: Optional[int] = None,
filters: Optional[Filters[Payment]] = None,
conn: Optional[Connection] = None,
) -> List[Payment]:
"""
@@ -393,29 +397,20 @@ async def get_payments(
clause.append("checking_id NOT LIKE 'temp_%'")
clause.append("checking_id NOT LIKE 'internal_%'")
limit_clause = f"LIMIT {limit}" if type(limit) == int and limit > 0 else ""
offset_clause = f"OFFSET {offset}" if type(offset) == int and offset > 0 else ""
# combine limit and offset clauses
limit_offset_clause = (
f"{limit_clause} {offset_clause}"
if limit_clause and offset_clause
else limit_clause or offset_clause
)
where = ""
if clause:
where = f"WHERE {' AND '.join(clause)}"
if not filters:
filters = Filters(limit=None, offset=None)
rows = await (conn or db).fetchall(
f"""
SELECT *
FROM apipayments
{where}
{filters.where(clause)}
ORDER BY time DESC
{limit_offset_clause}
{filters.pagination()}
""",
tuple(args),
filters.values(args),
)
return [Payment.from_row(row) for row in rows]
@@ -604,6 +599,23 @@ async def check_internal(
return row["checking_id"]
async def check_internal_pending(
payment_hash: str, conn: Optional[Connection] = None
) -> bool:
"""Returns False if the internal payment is not pending anymore (and thus paid), otherwise True"""
row = await (conn or db).fetchone(
"""
SELECT pending FROM apipayments
WHERE hash = ? AND amount > 0
""",
(payment_hash,),
)
if not row:
return True
else:
return row["pending"]
# balance_check
# -------------
@@ -705,15 +717,19 @@ async def update_admin_settings(data: EditableSettings):
await db.execute("UPDATE settings SET editable_settings = ?", (json.dumps(data),))
async def update_super_user(super_user: str):
async def update_super_user(super_user: str) -> SuperSettings:
await db.execute("UPDATE settings SET super_user = ?", (super_user,))
return await get_super_settings()
settings = await get_super_settings()
assert settings, "updated super_user settings could not be retrieved"
return settings
async def create_admin_settings(super_user: str, new_settings: dict):
sql = "INSERT INTO settings (super_user, editable_settings) VALUES (?, ?)"
await db.execute(sql, (super_user, json.dumps(new_settings)))
return await get_super_settings()
settings = await get_super_settings()
assert settings, "created admin settings could not be retrieved"
return settings
# db versions
+3 -2
View File
@@ -107,7 +107,7 @@ async def m002_add_fields_to_apipayments(db):
continue
for ext in ["withdraw", "events", "lnticket", "paywall", "tpos"]:
prefix = "#" + ext + " "
prefix = f"#{ext} "
if row["memo"].startswith(prefix):
new = row["memo"][len(prefix) :]
await db.execute(
@@ -168,7 +168,8 @@ async def m004_ensure_fees_are_always_negative(db):
async def m005_balance_check_balance_notify(db):
"""
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.
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.
"""
await db.execute(
+11 -1
View File
@@ -13,7 +13,7 @@ from pydantic import BaseModel
from lnbits.db import Connection
from lnbits.helpers import url_for
from lnbits.settings import get_wallet_class
from lnbits.settings import get_wallet_class, settings
from lnbits.wallets.base import PaymentStatus
@@ -75,6 +75,16 @@ class User(BaseModel):
w = [wallet for wallet in self.wallets if wallet.id == wallet_id]
return w[0] if w else None
@classmethod
def is_extension_for_user(cls, ext: str, user: str) -> bool:
if ext not in settings.lnbits_admin_extensions:
return True
if user == settings.super_user:
return True
if user in settings.lnbits_admin_users:
return True
return False
class Payment(BaseModel):
checking_id: str
+45 -38
View File
@@ -1,7 +1,7 @@
import asyncio
import json
from io import BytesIO
from typing import Dict, List, Optional, Tuple
from typing import Dict, List, Optional, Tuple, TypedDict
from urllib.parse import parse_qs, urlparse
import httpx
@@ -13,10 +13,11 @@ from loguru import logger
from lnbits import bolt11
from lnbits.db import Connection
from lnbits.decorators import WalletTypeInfo, require_admin_key
from lnbits.helpers import url_for, urlsafe_short_hash
from lnbits.helpers import url_for
from lnbits.settings import (
FAKE_WALLET,
EditableSettings,
SuperSettings,
get_wallet_class,
readonly_variables,
send_admin_user_to_saas,
@@ -27,6 +28,7 @@ from lnbits.wallets.base import PaymentResponse, PaymentStatus
from . import db
from .crud import (
check_internal,
check_internal_pending,
create_account,
create_admin_settings,
create_payment,
@@ -42,11 +44,6 @@ from .crud import (
)
from .models import Payment
try:
from typing import TypedDict
except ImportError: # pragma: nocover
from typing_extensions import TypedDict
class PaymentFailure(Exception):
pass
@@ -153,7 +150,11 @@ async def pay_invoice(
extra=extra,
)
# check_internal() returns the checking_id of the invoice we're waiting for
# we check if an internal invoice exists that has already been paid (not pending anymore)
if not await check_internal_pending(invoice.payment_hash, conn=conn):
raise PaymentFailure("Internal invoice already paid.")
# check_internal() returns the checking_id of the invoice we're waiting for (pending only)
internal_checking_id = await check_internal(invoice.payment_hash, conn=conn)
if internal_checking_id:
logger.debug(f"creating temporary internal payment with id {internal_id}")
@@ -169,16 +170,21 @@ async def pay_invoice(
logger.debug(f"creating temporary payment with id {temp_id}")
# create a temporary payment here so we can check if
# the balance is enough in the next step
await create_payment(
checking_id=temp_id,
fee=-fee_reserve_msat,
conn=conn,
**payment_kwargs,
)
try:
await create_payment(
checking_id=temp_id,
fee=-fee_reserve_msat,
conn=conn,
**payment_kwargs,
)
except Exception as e:
logger.error(f"could not create temporary payment: {e}")
# happens if the same wallet tries to pay an invoice twice
raise PaymentFailure("Could not make payment.")
# do the balance check
wallet = await get_wallet(wallet_id, conn=conn)
assert wallet
assert wallet, "Wallet for balancecheck could not be fetched"
if wallet.balance_msat < 0:
logger.debug("balance is too low, deleting temporary payment")
if not internal_checking_id and wallet.balance_msat > -fee_reserve_msat:
@@ -326,19 +332,20 @@ async def perform_lnurlauth(
return b
def encode_strict_der(r_int, s_int, order):
def encode_strict_der(r: int, s: int, order: int):
# if s > order/2 verification will fail sometimes
# so we must fix it here (see https://github.com/indutny/elliptic/blob/e71b2d9359c5fe9437fbf46f1f05096de447de57/lib/elliptic/ec/index.js#L146-L147)
if s_int > order // 2:
s_int = order - s_int
# so we must fix it here see:
# https://github.com/indutny/elliptic/blob/e71b2d9359c5fe9437fbf46f1f05096de447de57/lib/elliptic/ec/index.js#L146-L147
if s > order // 2:
s = order - s
# now we do the strict DER encoding copied from
# https://github.com/KiriKiri/bip66 (without any checks)
r = int_to_bytes_suitable_der(r_int)
s = int_to_bytes_suitable_der(s_int)
r_temp = int_to_bytes_suitable_der(r)
s_temp = int_to_bytes_suitable_der(s)
r_len = len(r)
s_len = len(s)
r_len = len(r_temp)
s_len = len(s_temp)
sign_len = 6 + r_len + s_len
signature = BytesIO()
@@ -346,16 +353,17 @@ async def perform_lnurlauth(
signature.write((sign_len - 2).to_bytes(1, "big", signed=False))
signature.write(0x02.to_bytes(1, "big", signed=False))
signature.write(r_len.to_bytes(1, "big", signed=False))
signature.write(r)
signature.write(r_temp)
signature.write(0x02.to_bytes(1, "big", signed=False))
signature.write(s_len.to_bytes(1, "big", signed=False))
signature.write(s)
signature.write(s_temp)
return signature.getvalue()
sig = key.sign_digest_deterministic(k1, sigencode=encode_strict_der)
async with httpx.AsyncClient() as client:
assert key.verifying_key, "LNURLauth verifying_key does not exist"
r = await client.get(
callback,
params={
@@ -386,7 +394,7 @@ async def check_transaction_status(
return PaymentStatus(None)
if not payment.pending:
# note: before, we still checked the status of the payment again
return PaymentStatus(True)
return PaymentStatus(True, fee_msat=payment.fee)
status: PaymentStatus = await payment.check_status()
return status
@@ -400,21 +408,20 @@ def fee_reserve(amount_msat: int) -> int:
async def update_wallet_balance(wallet_id: str, amount: int):
internal_id = f"internal_{urlsafe_short_hash()}"
payment = await create_payment(
payment_hash, _ = await create_invoice(
wallet_id=wallet_id,
checking_id=internal_id,
payment_request="admin_internal",
payment_hash="admin_internal",
amount=amount * 1000,
amount=amount,
memo="Admin top up",
pending=False,
internal=True,
)
# manually send this for now
from lnbits.tasks import internal_invoice_queue
async with db.connect() as conn:
checking_id = await check_internal(payment_hash, conn=conn)
assert checking_id, "newly created checking_id cannot be retrieved"
await update_payment_status(checking_id=checking_id, pending=False, conn=conn)
# notify receiver asynchronously
from lnbits.tasks import internal_invoice_queue
await internal_invoice_queue.put(internal_id)
return payment
await internal_invoice_queue.put(checking_id)
async def check_admin_settings():
@@ -460,7 +467,7 @@ def update_cached_settings(sets_dict: dict):
setattr(settings, "super_user", sets_dict["super_user"])
async def init_admin_settings(super_user: str = None):
async def init_admin_settings(super_user: Optional[str] = None) -> SuperSettings:
account = None
if super_user:
account = await get_account(super_user)
+15 -5
View File
@@ -148,27 +148,27 @@ new Vue({
{
name: 'memo',
align: 'left',
label: 'Memo',
label: this.$t('memo'),
field: 'memo'
},
{
name: 'date',
align: 'left',
label: 'Date',
label: this.$t('date'),
field: 'date',
sortable: true
},
{
name: 'sat',
align: 'right',
label: 'Amount (' + LNBITS_DENOMINATION + ')',
label: this.$t('amount') + ' (' + LNBITS_DENOMINATION + ')',
field: 'sat',
sortable: true
},
{
name: 'fee',
align: 'right',
label: 'Fee (m' + LNBITS_DENOMINATION + ')',
label: this.$t('fee') + ' (m' + LNBITS_DENOMINATION + ')',
field: 'fee'
}
],
@@ -466,6 +466,16 @@ new Vue({
return
}
// BIP-21 support
if (this.parse.data.request.toLowerCase().includes('lightning')) {
this.parse.data.request = this.parse.data.request.split('lightning=')[1]
// fail safe to check there's nothing after the lightning= part
if (this.parse.data.request.includes('&')) {
this.parse.data.request = this.parse.data.request.split('&')[0]
}
}
let invoice
try {
invoice = decode(this.parse.data.request)
@@ -510,7 +520,7 @@ new Vue({
payInvoice: function () {
let dismissPaymentMsg = this.$q.notify({
timeout: 0,
message: 'Processing payment...'
message: this.$t('processing_payment')
})
LNbits.api
+1 -1
View File
@@ -63,7 +63,7 @@
multiple
hint="Extensions only user with admin privileges can use"
label="Admin extensions"
:options="g.extensions.map(e => e.name)"
:options="g.extensions.map(e => e.code)"
></q-select>
<br />
</div>
+32 -40
View File
@@ -3,12 +3,14 @@
<div class="row q-col-gutter-md justify-center">
<div class="col q-my-md">
<q-btn
label="Save"
:label="$t('save')"
color="primary"
@click="updateSettings"
:disabled="!checkChanges"
>
<q-tooltip v-if="checkChanges"> Save your changes </q-tooltip>
<q-tooltip v-if="checkChanges"
>{%raw%}{{ $t('save_tooltip') }}{%endraw%}</q-tooltip
>
<q-badge
v-if="checkChanges"
@@ -21,13 +23,13 @@
<q-btn
v-if="isSuperUser"
label="Restart server"
:label="$t('restart')"
color="primary"
@click="restartServer"
>
<q-tooltip v-if="needsRestart">
Restart the server for changes to take effect
</q-tooltip>
<q-tooltip v-if="needsRestart"
>{%raw%}{{ $t('restart_tooltip') }}{%endraw%}</q-tooltip
>
<q-badge
v-if="needsRestart"
@@ -40,28 +42,24 @@
<q-btn
v-if="isSuperUser"
label="Topup"
:label="$t('topup')"
color="primary"
@click="topUpDialog.show = true"
>
<q-tooltip> Add funds to a wallet. </q-tooltip>
<q-tooltip>{%raw%}{{ $t('add_funds_tooltip') }}{%endraw%}</q-tooltip>
</q-btn>
<q-btn
label="Download Database Backup"
flat
@click="downloadBackup"
></q-btn>
<q-btn :label="$t('download_backup')" flat @click="downloadBackup"></q-btn>
<q-btn
flat
v-if="isSuperUser"
label="Reset to defaults"
:label="$t('reset_defaults')"
color="primary"
@click="deleteSettings"
class="float-right"
>
<q-tooltip> Delete all settings and reset to defaults. </q-tooltip>
<q-tooltip>{%raw%}{{ $t('reset_defaults_tooltip') }}{%endraw%}</q-tooltip>
</q-btn>
</div>
</div>
@@ -74,25 +72,25 @@
<q-tabs v-model="tab" active-color="primary" align="justify">
<q-tab
name="funding"
label="Funding"
:label="$t('funding')"
@update="val => tab = val.name"
></q-tab>
<q-tab
name="users"
label="Users"
:label="$t('users')"
@update="val => tab = val.name"
></q-tab>
<q-tab
name="server"
label="Server"
:label="$t('server')"
@update="val => tab = val.name"
></q-tab>
<q-tab
name="theme"
label="Theme"
:label="$t('theme')"
@update="val => tab = val.name"
></q-tab>
</q-tabs>
@@ -113,7 +111,7 @@
<q-dialog v-if="isSuperUser" 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>TopUp a wallet</p>
<p v-text="$t('topup_wallet')"></p>
<div class="row">
<div class="col-12">
@@ -123,7 +121,7 @@
filled
v-model="wallet.id"
label="Wallet ID"
hint="Use the wallet ID to topup any wallet"
:hint="$t('topup_hint')"
></q-input>
<br />
@@ -135,15 +133,25 @@
type="number"
filled
v-model="wallet.amount"
label="Topup amount"
:label="$t('amount')"
></q-input>
</div>
</div>
<div class="row q-mt-lg">
<q-btn label="Topup" color="primary" @click="topupWallet"></q-btn>
<q-btn
:label="$t('topup')"
color="primary"
@click="topupWallet"
></q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-form>
</q-card>
@@ -217,18 +225,6 @@
lnd_rest_macaroon_encrypted: {
value: null,
label: 'Encrypted Macaroon'
},
lnd_cert: {
value: null,
label: 'Certificate'
},
lnd_rest_admin_macaroon: {
value: null,
label: 'Admin Macaroon'
},
lnd_rest_invoice_macaroon: {
value: null,
label: 'Invoice Macaroon'
}
}
],
@@ -251,10 +247,6 @@
value: null,
label: 'Admin Macaroon'
},
lnd_grpc_invoice_macaroon: {
value: null,
label: 'Invoice Macaroon'
},
lnd_grpc_macaroon_encrypted: {
value: null,
label: 'Encrypted Macaroon'
+1 -1
View File
@@ -1,7 +1,7 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="API info"
:label="$t('api_docs')"
:content-inset-level="0.5"
>
<q-card-section>
+3 -5
View File
@@ -11,8 +11,8 @@
color="primary"
type="a"
:href="['/install?usr=', user.id].join('')"
>Manage Extensions</q-btn
>
:label="$t('manage_extensions')"
></q-btn>
</p>
</div>
@@ -27,9 +27,7 @@
</q-input>
</div>
</div>
<p v-if="!filteredExtensions.length">
You don't have any extensions installed :(
</p>
<p v-if="!filteredExtensions.length" v-text="$t('no_extensions')"></p>
<div class="row q-col-gutter-md">
<div
class="col-6 col-md-4 col-lg-3"
+11 -32
View File
@@ -12,24 +12,23 @@
@click="processing"
type="a"
href="{{ url_for('core.lnurlwallet') }}?lightning={{ lnurl }}"
>
Press to claim bitcoin
</q-btn>
v-text="$t('press_to_claim')"
></q-btn>
{% else %}
<q-form @submit="createWallet" class="q-gutter-md">
<q-input
filled
dense
v-model="walletName"
label="Name your {{SITE_TITLE}} wallet *"
:label='$t("name_your_wallet", { name: "{{ SITE_TITLE }} *" })'
></q-input>
<q-btn
unelevated
color="primary"
:disable="walletName == ''"
type="submit"
>Add a new wallet</q-btn
>
:label="$t('add_wallet')"
></q-btn>
</q-form>
{% endif %}
</q-card-section>
@@ -40,27 +39,7 @@
<h3 class="q-my-none">{{SITE_TITLE}}</h3>
<h5 class="q-my-md">{{SITE_TAGLINE}}</h5>
<div v-if="'{{SITE_TITLE}}' == 'LNbits'">
<p>
Easy to set up and lightweight, LNbits can run on any
lightning-network funding source, currently supporting LND,
c-lightning, OpenNode, LNPay and even LNbits itself!
</p>
<p>
You can run LNbits for yourself, or easily offer a custodian
solution for others.
</p>
<p>
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.
</p>
<p>
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.
</p>
<p v-text="$t('lnbits_description')"></p>
<div class="row q-mt-md q-gutter-sm">
<q-btn
outline
@@ -69,8 +48,8 @@
href="https://github.com/lnbits/lnbits"
target="_blank"
rel="noopener"
>View project in GitHub</q-btn
>
:label="$t('view_github')"
></q-btn>
<q-btn
outline
color="grey"
@@ -78,8 +57,8 @@
href="https://legend.lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK"
target="_blank"
rel="noopener"
>Donate</q-btn
>
:label="$t('donate')"
></q-btn>
</div>
</div>
<p v-else>{{SITE_DESCRIPTION | safe}}</p>
@@ -94,7 +73,7 @@
<q-btn
flat
color="secondary"
label="Runs on"
:label="$t('runs_on')"
class="full-width"
></q-btn>
<div class="row">
+149 -114
View File
@@ -37,8 +37,8 @@
>
<q-input
filled
label="{{LNBITS_DENOMINATION}} to credit"
hint="Press Enter to credit account"
:label='$t("credit_label", { denomination: "{{LNBITS_DENOMINATION}}"})'
:hint="$t('credit_hint')"
v-model="scope.value"
dense
autofocus
@@ -63,8 +63,8 @@
color="primary"
class="full-width"
@click="showParseDialog"
>Paste Request</q-btn
>
:label="$t('paste_request')"
></q-btn>
</div>
<div class="col">
<q-btn
@@ -72,8 +72,8 @@
color="primary"
class="full-width"
@click="showReceiveDialog"
>Create Invoice</q-btn
>
:label="$t('create_invoice')"
></q-btn>
</div>
<div class="col">
<q-btn
@@ -81,8 +81,11 @@
color="secondary"
icon="photo_camera"
@click="showCamera"
>scan
<q-tooltip>Use camera to scan an invoice/QR</q-tooltip>
:label="$t('scan')"
>
<q-tooltip
>{% raw %}{{$t('camera_tooltip')}}{% endraw %}</q-tooltip
>
</q-btn>
</div>
</div>
@@ -92,12 +95,18 @@
<q-card-section>
<div class="row items-center no-wrap q-mb-sm">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Transactions</h5>
<h5
class="text-subtitle1 q-my-none"
:v-text="$t('transactions')"
></h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportCSV"
>Export to CSV</q-btn
>
<q-btn
flat
color="grey"
@click="exportCSV"
:label="$t('export_csv')"
></q-btn>
<!--<q-btn v-if="pendingPaymentsExist" dense flat round icon="update" color="grey" @click="checkPendingPayments">
<q-tooltip>Check pending</q-tooltip>
</q-btn>-->
@@ -109,7 +118,9 @@
color="grey"
@click="showChart"
>
<q-tooltip>Show chart</q-tooltip>
<q-tooltip
>{% raw %}{{$t('chart_tooltip')}}{% endraw %}</q-tooltip
>
</q-btn>
</div>
</div>
@@ -120,7 +131,7 @@
clearable
v-model="paymentsTable.filter"
debounce="300"
placeholder="Search by tag, memo, amount"
:placeholder="$t('search_by_tag_memo_amount')"
class="q-mb-md"
>
</q-input>
@@ -131,7 +142,7 @@
:row-key="paymentTableRowKey"
:columns="paymentsTable.columns"
:pagination.sync="paymentsTable.pagination"
no-data-label="No transactions made yet"
:no-data-label="$t('no_transactions')"
:filter="paymentsTable.filter"
>
{% raw %}
@@ -159,7 +170,7 @@
color="grey"
@click="props.expand = !props.expand"
>
<q-tooltip>Pending</q-tooltip>
<q-tooltip>{{$t('pending')}}</q-tooltip>
</q-icon>
</q-td>
<q-td
@@ -209,7 +220,7 @@
<div class="text-center q-mb-lg">
<div v-if="props.row.isIn && props.row.pending">
<q-icon name="settings_ethernet" color="grey"></q-icon>
Invoice waiting to be paid
<span v-text="$t('invoice_waiting')"></span>
<lnbits-payment-details
:payment="props.row"
></lnbits-payment-details>
@@ -232,15 +243,15 @@
outline
color="grey"
@click="copyText(props.row.bolt11)"
>Copy invoice</q-btn
>
:label="$t('copy_invoice')"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
>Close</q-btn
>
:label="$t('close')"
></q-btn>
</div>
</div>
<div v-else-if="props.row.isPaid && props.row.isIn">
@@ -249,7 +260,7 @@
:name="'call_received'"
:color="'green'"
></q-icon>
Payment Received
<span v-text="$t('payment_received')"></span>
<lnbits-payment-details
:payment="props.row"
></lnbits-payment-details>
@@ -260,14 +271,14 @@
:name="'call_made'"
:color="'pink'"
></q-icon>
Payment Sent
<span v-text="$t('payment_sent')"></span>
<lnbits-payment-details
:payment="props.row"
></lnbits-payment-details>
</div>
<div v-else-if="props.row.isOut && props.row.pending">
<q-icon name="settings_ethernet" color="grey"></q-icon>
Outgoing payment pending
<span v-text="$t('outgoing_payment_pending')"></span>
<lnbits-payment-details
:payment="props.row"
></lnbits-payment-details>
@@ -304,26 +315,17 @@
<q-expansion-item
group="extras"
icon="crop_free"
label="Drain Funds"
:label="$t('drain_funds')"
>
<q-card>
<q-card-section class="text-center">
<p>
This is an LNURL-withdraw QR code for slurping
everything from this wallet. Do not share with anyone.
</p>
<a href="lightning:{{wallet.lnurlwithdraw_full}}">
<qrcode
value="lightning:{{wallet.lnurlwithdraw_full}}"
:options="{width:240}"
></qrcode>
</a>
<p>
It is compatible with <code>balanceCheck</code> and
<code>balanceNotify</code> so your wallet may keep
pulling the funds continuously from here after the first
withdraw.
</p>
<p v-text="$('drain_funds_desc')"></p>
</q-card-section>
</q-card>
</q-expansion-item>
@@ -333,15 +335,11 @@
<q-expansion-item
group="extras"
icon="settings_cell"
label="Export to Phone with QR Code"
:label="$t('export_to_phone')"
>
<q-card>
<q-card-section class="text-center">
<p>
This QR code contains your wallet URL with full access.
You can scan it from your phone to open your wallet from
there.
</p>
<p v-text="$t('export_to_phone_desc')"></p>
<qrcode
:value="'{{request.base_url}}' +'wallet?usr={{user.id}}&wal={{wallet.id}}'"
:options="{width:240}"
@@ -353,7 +351,7 @@
<q-expansion-item
group="extras"
icon="edit"
label="Rename wallet"
:label="$t('rename_wallet')"
>
<q-card>
<q-card-section>
@@ -371,9 +369,9 @@
unelevated
class="q-mt-sm"
color="primary"
:label="$t('update_name')"
@click="updateWalletName()"
>Update name</q-btn
>
></q-btn>
</q-card-section>
</q-card>
</q-expansion-item>
@@ -381,20 +379,17 @@
<q-expansion-item
group="extras"
icon="remove_circle"
label="Delete wallet"
:label="$t('delete_wallet')"
>
<q-card>
<q-card-section>
<p>
This whole wallet will be deleted, the funds will be
<strong>UNRECOVERABLE</strong>.
</p>
<p v-text="$t('delete_wallet_desc')"></p>
<q-btn
unelevated
color="red-10"
@click="deleteWallet('{{ wallet.id }}', '{{ user.id }}')"
>Delete wallet</q-btn
>
:label="$t('delete_wallet')"
></q-btn>
</q-card-section>
</q-card>
</q-expansion-item>
@@ -445,7 +440,7 @@
filled
dense
v-model.number="receive.data.amount"
label="Amount ({{LNBITS_DENOMINATION}}) *"
:label="$('amount')+' ({{LNBITS_DENOMINATION}}) *'"
mask="#.##"
fill-mask="0"
reverse-fill-mask
@@ -459,7 +454,7 @@
dense
v-model="receive.unit"
type="text"
label="Unit"
:label="$t('unit')"
:options="receive.units"
></q-select>
<q-input
@@ -467,7 +462,7 @@
filled
dense
v-model.number="receive.data.amount"
:label="'Amount (' + receive.unit + ') *'"
:label="$t('amount') + ' (' + receive.unit + ') *'"
:mask="receive.unit != 'sat' ? '#.##' : '#'"
fill-mask="0"
reverse-fill-mask
@@ -482,7 +477,7 @@
filled
dense
v-model.trim="receive.data.memo"
label="Memo"
:label="$t('memo')"
></q-input>
{% raw %}
<div v-if="receive.status == 'pending'" class="row q-mt-lg">
@@ -493,13 +488,17 @@
type="submit"
>
<span v-if="receive.lnurl">
Withdraw from {{receive.lnurl.domain}}
{{$t('withdraw_from')}} {{receive.lnurl.domain}}
</span>
<span v-else> Create invoice </span>
<span v-else v-text="$t('create_invoice')"></span>
</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
<q-spinner
v-if="receive.status == 'loading'"
@@ -521,12 +520,19 @@
</a>
</div>
<div class="row q-mt-lg">
<q-btn outline color="grey" @click="copyText(receive.paymentReq)"
>Copy invoice</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Close</q-btn
>
<q-btn
outline
color="grey"
@click="copyText(receive.paymentReq)"
:label="$t('copy_invoice')"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('close')"
></q-btn>
</div>
</q-card>
{% endraw %}
@@ -545,24 +551,42 @@
</h6>
<q-separator class="q-my-sm"></q-separator>
<p class="text-wrap">
<strong>Description:</strong> {{ parse.invoice.description }}<br />
<strong v-text="$t('description')">:</strong> {{
parse.invoice.description }}<br />
<strong>Expire date:</strong> {{ parse.invoice.expireDate }}<br />
<strong>Hash:</strong> {{ parse.invoice.hash }}
</p>
{% endraw %}
<div v-if="canPay" class="row q-mt-lg">
<q-btn unelevated color="primary" @click="payInvoice">Pay</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
<q-btn
unelevated
color="primary"
@click="payInvoice"
:label="$t('pay')"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
<div v-else class="row q-mt-lg">
<q-btn unelevated disabled color="yellow" text-color="black"
>Not enough funds!</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
<q-btn
:label="$t('not_enough_funds')"
unelevated
disabled
color="yellow"
text-color="black"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</div>
<div v-else-if="parse.lnurlauth">
@@ -584,9 +608,13 @@
</p>
<div class="row q-mt-lg">
<q-btn unelevated color="primary" type="submit">Login</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
<q-btn
:label="$t('cancel')"
v-close-popup
flat
color="grey"
class="q-ml-auto"
></q-btn>
</div>
</q-form>
{% endraw %}
@@ -657,9 +685,13 @@
<q-btn unelevated color="primary" type="submit"
>Send {{LNBITS_DENOMINATION}}</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
<q-btn
:label="$t('cancel')"
v-close-popup
flat
color="grey"
class="q-ml-auto"
></q-btn>
</div>
</q-form>
{% endraw %}
@@ -675,7 +707,7 @@
dense
v-model.trim="parse.data.request"
type="textarea"
label="Paste an invoice, payment request or lnurl code *"
:label="$t('paste_invoice_label')"
>
</q-input>
<div class="row q-mt-lg">
@@ -684,11 +716,15 @@
color="primary"
:disable="parse.data.request == ''"
type="submit"
>Read</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
:label="$t('read')"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-form>
<div v-else>
@@ -700,8 +736,13 @@
></qrcode-stream>
</q-responsive>
<div class="row q-mt-lg">
<q-btn @click="closeCamera" flat color="grey" class="q-ml-auto">
Cancel
<q-btn
:label="$t('cancel')"
@click="closeCamera"
flat
color="grey"
class="q-ml-auto"
>
</q-btn>
</div>
</div>
@@ -719,9 +760,13 @@
></qrcode-stream>
</div>
<div class="row q-mt-lg">
<q-btn @click="closeCamera" flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
<q-btn
@click="closeCamera"
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-card>
</q-dialog>
@@ -755,31 +800,21 @@
<q-dialog v-model="disclaimerDialog.show">
<q-card class="q-pa-lg">
<h6 class="q-my-md text-primary">Warning</h6>
<p>
Login functionality to be released in a future update, for now,
<strong
>make sure you bookmark this page for future access to your
wallet</strong
>!
</p>
<p>
This service is in BETA, and we hold no responsibility for people
losing access to funds. {% if service_fee > 0 %} To encourage you to
run your own LNbits installation, any balance on {% raw %}{{
disclaimerDialog.location.host }}{% endraw %} will incur a charge of
<strong>{{ service_fee }}% service fee</strong> per week. {% endif
%}
</p>
<p v-text="$t('disclaimer_dialog')"></p>
<div class="row q-mt-lg">
<q-btn
outline
color="grey"
@click="copyText(disclaimerDialog.location.href)"
>Copy wallet URL</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>I understand</q-btn
>
:label="$t('copy_wallet_url')"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('i_understand')"
></q-btn>
</div>
</q-card>
</q-dialog>
+27 -17
View File
@@ -34,10 +34,12 @@ from lnbits.core.helpers import (
stop_extension_background_work,
)
from lnbits.core.models import Payment, User, Wallet
from lnbits.db import Filters
from lnbits.decorators import (
WalletTypeInfo,
check_admin,
get_key_type,
parse_filters,
require_admin_key,
require_invoice_key,
)
@@ -48,7 +50,7 @@ from lnbits.extension_manager import (
InstallableExtension,
get_valid_extensions,
)
from lnbits.helpers import url_for
from lnbits.helpers import generate_filter_params_openapi, url_for
from lnbits.settings import get_wallet_class, settings
from lnbits.utils.exchange_rates import (
currencies,
@@ -114,18 +116,23 @@ async def api_update_wallet(
}
@core_app.get("/api/v1/payments")
@core_app.get(
"/api/v1/payments",
name="Payment List",
summary="get list of payments",
response_description="list of payments",
response_model=List[Payment],
openapi_extra=generate_filter_params_openapi(Payment),
)
async def api_payments(
limit: Optional[int] = None,
offset: Optional[int] = None,
wallet: WalletTypeInfo = Depends(get_key_type),
filters: Filters = Depends(parse_filters(Payment)),
):
pendingPayments = await get_payments(
wallet_id=wallet.wallet.id,
pending=True,
exclude_uncheckable=True,
limit=limit,
offset=offset,
filters=filters,
)
for payment in pendingPayments:
await check_transaction_status(
@@ -135,8 +142,7 @@ async def api_payments(
wallet_id=wallet.wallet.id,
pending=True,
complete=True,
limit=limit,
offset=offset,
filters=filters,
)
@@ -405,8 +411,7 @@ async def subscribe_wallet_invoices(request: Request, wallet: Wallet):
typ, data = await send_queue.get()
if data:
jdata = json.dumps(dict(data.dict(), pending=False))
yield dict(data=jdata, event=typ)
yield dict(data=jdata, event=typ)
except asyncio.CancelledError:
logger.debug(f"removing listener for wallet {uid}")
api_invoice_listeners.pop(uid)
@@ -425,11 +430,12 @@ async def api_payments_sse(
)
# TODO: refactor this route into a public and admin one
@core_app.get("/api/v1/payments/{payment_hash}")
async def api_payment(payment_hash, X_Api_Key: Optional[str] = Header(None)):
# We use X_Api_Key here because we want this call to work with and without keys
# If a valid key is given, we also return the field "details", otherwise not
wallet = await get_wallet_for_key(X_Api_Key) if type(X_Api_Key) == str else None
wallet = await get_wallet_for_key(X_Api_Key) if type(X_Api_Key) == str else None # type: ignore
# we have to specify the wallet id here, because postgres and sqlite return internal payments in different order
# and get_standalone_payment otherwise just fetches the first one, causing unpredictable results
@@ -499,6 +505,7 @@ async def api_lnurlscan(code: str, wallet: WalletTypeInfo = Depends(get_key_type
params.update(callback=url) # with k1 already in it
lnurlauth_key = wallet.wallet.lnurlauth_key(domain)
assert lnurlauth_key.verifying_key
params.update(pubkey=lnurlauth_key.verifying_key.to_string("compressed").hex())
else:
async with httpx.AsyncClient() as client:
@@ -564,7 +571,7 @@ async def api_lnurlscan(code: str, wallet: WalletTypeInfo = Depends(get_key_type
if k == "text/plain":
params.update(description=v)
if k in ("image/jpeg;base64", "image/png;base64"):
data_uri = "data:" + k + "," + v
data_uri = f"data:{k},{v}"
params.update(image=data_uri)
if k in ("text/email", "text/identifier"):
params.update(targetUser=v)
@@ -687,7 +694,7 @@ async def api_auditor():
if not error_message:
delta = node_balance - total_balance
else:
node_balance, delta = None, None
node_balance, delta = 0, 0
return {
"node_balance_msats": int(node_balance),
@@ -697,7 +704,7 @@ async def api_auditor():
}
##################UNIVERSAL WEBSOCKET MANAGER########################
# UNIVERSAL WEBSOCKET MANAGER
@core_app.websocket("/api/v1/ws/{item_id}")
@@ -739,6 +746,7 @@ async def api_install_extension(
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Release not found"
)
ext_info = InstallableExtension(
id=data.ext_id, name=data.ext_id, installed_release=release, icon=release.icon
)
@@ -818,8 +826,10 @@ async def api_uninstall_extension(ext_id: str, user: User = Depends(check_admin)
)
@core_app.get("/api/v1/extension/{ext_id}/releases")
async def get_extension_releases(ext_id: str, user: User = Depends(check_admin)):
@core_app.get(
"/api/v1/extension/{ext_id}/releases", dependencies=[Depends(check_admin)]
)
async def get_extension_releases(ext_id: str):
try:
extension_releases: List[
ExtensionRelease
@@ -833,7 +843,7 @@ async def get_extension_releases(ext_id: str, user: User = Depends(check_admin))
)
############################TINYURL##################################
# TINYURL
@core_app.post("/api/v1/tinyurl")
+2 -2
View File
@@ -369,7 +369,7 @@ async def manifest(usr: str):
"sizes": "900x900",
}
],
"start_url": "/wallet?usr=" + usr + "&wal=" + user.wallets[0].id,
"start_url": f"/wallet?usr={usr}&wal={user.wallets[0].id}",
"background_color": "#1F2234",
"description": "Bitcoin Lightning Wallet",
"display": "standalone",
@@ -380,7 +380,7 @@ async def manifest(usr: str):
"name": wallet.name,
"short_name": wallet.name,
"description": wallet.name,
"url": "/wallet?usr=" + usr + "&wal=" + wallet.id,
"url": f"/wallet?usr={usr}&wal={wallet.id}",
}
for wallet in user.wallets
],
+4 -5
View File
@@ -40,19 +40,18 @@ async def api_public_payment_longpolling(payment_hash):
response = None
async def payment_info_receiver(cancel_scope):
async for payment in payment_queue.get():
async def payment_info_receiver():
for payment in await payment_queue.get():
if payment.payment_hash == payment_hash:
nonlocal response
response = {"status": "paid"}
cancel_scope.cancel()
async def timeouter(cancel_scope):
await asyncio.sleep(45)
cancel_scope.cancel()
asyncio.create_task(payment_info_receiver())
asyncio.create_task(timeouter())
cancel_scope = asyncio.create_task(payment_info_receiver())
asyncio.create_task(timeouter(cancel_scope))
if response:
return response
+132 -14
View File
@@ -4,9 +4,11 @@ import os
import re
import time
from contextlib import asynccontextmanager
from typing import Optional
from enum import Enum
from typing import Any, Generic, List, Optional, Tuple, Type, TypeVar
from loguru import logger
from pydantic import BaseModel, ValidationError
from sqlalchemy import create_engine
from sqlalchemy_aio.base import AsyncConnection
from sqlalchemy_aio.strategy import ASYNCIO_STRATEGY
@@ -94,7 +96,7 @@ class Connection(Compat):
# tuple to list and back to tuple
value_list = [values] if isinstance(values, str) else list(values)
values = tuple([cleanhtml(l) for l in value_list])
values = tuple([cleanhtml(val) for val in value_list])
return values
async def fetchall(self, query: str, values: tuple = ()) -> list:
@@ -129,7 +131,7 @@ class Database(Compat):
else:
self.type = POSTGRES
import psycopg2
from psycopg2.extensions import DECIMAL, new_type, register_type
def _parse_timestamp(value, _):
if value is None:
@@ -139,15 +141,15 @@ class Database(Compat):
f = "%Y-%m-%d %H:%M:%S"
return time.mktime(datetime.datetime.strptime(value, f).timetuple())
psycopg2.extensions.register_type(
psycopg2.extensions.new_type(
psycopg2.extensions.DECIMAL.values,
register_type(
new_type(
DECIMAL.values,
"DEC2FLOAT",
lambda value, curs: float(value) if value is not None else None,
)
)
psycopg2.extensions.register_type(
psycopg2.extensions.new_type(
register_type(
new_type(
(1082, 1083, 1266),
"DATE2INT",
lambda value, curs: time.mktime(value.timetuple())
@@ -156,11 +158,7 @@ class Database(Compat):
)
)
psycopg2.extensions.register_type(
psycopg2.extensions.new_type(
(1184, 1114), "TIMESTAMP2INT", _parse_timestamp
)
)
register_type(new_type((1184, 1114), "TIMESTAMP2INT", _parse_timestamp))
else:
if os.path.isdir(settings.lnbits_data_folder):
self.path = os.path.join(
@@ -187,7 +185,7 @@ class Database(Compat):
async def connect(self):
await self.lock.acquire()
try:
async with self.engine.connect() as conn:
async with self.engine.connect() as conn: # type: ignore
async with conn.begin() as txn:
wconn = Connection(conn, txn, self.type, self.name, self.schema)
@@ -224,3 +222,123 @@ class Database(Compat):
@asynccontextmanager
async def reuse_conn(self, conn: Connection):
yield conn
class Operator(Enum):
GT = "gt"
LT = "lt"
EQ = "eq"
NE = "ne"
INCLUDE = "in"
EXCLUDE = "ex"
@property
def as_sql(self):
if self == Operator.EQ:
return "="
elif self == Operator.NE:
return "!="
elif self == Operator.INCLUDE:
return "IN"
elif self == Operator.EXCLUDE:
return "NOT IN"
elif self == Operator.GT:
return ">"
elif self == Operator.LT:
return "<"
else:
raise ValueError("Unknown SQL Operator")
TModel = TypeVar("TModel", bound=BaseModel)
class Filter(BaseModel, Generic[TModel]):
field: str
nested: Optional[list[str]]
op: Operator = Operator.EQ
values: list[Any]
@classmethod
def parse_query(cls, key: str, raw_values: list[Any], model: Type[TModel]):
# Key format:
# key[operator]
# e.g. name[eq]
if key.endswith("]"):
split = key[:-1].split("[")
if len(split) != 2:
raise ValueError("Invalid key")
field_names = split[0].split(".")
op = Operator(split[1])
else:
field_names = key.split(".")
op = Operator("eq")
field = field_names[0]
nested = field_names[1:]
if field in model.__fields__:
compare_field = model.__fields__[field]
values = []
for raw_value in raw_values:
# If there is a nested field, pydantic expects a dict, so the raw value is turned into a dict before
# and the converted value is extracted afterwards
for name in reversed(nested):
raw_value = {name: raw_value}
validated, errors = compare_field.validate(raw_value, {}, loc="none")
if errors:
raise ValidationError(errors=[errors], model=model)
for name in nested:
if isinstance(validated, dict):
validated = validated[name]
else:
validated = getattr(validated, name)
values.append(validated)
else:
raise ValueError("Unknown filter field")
return cls(field=field, op=op, nested=nested, values=values)
@property
def statement(self):
accessor = self.field
if self.nested:
for name in self.nested:
accessor = f"({accessor} ->> '{name}')"
if self.op in (Operator.INCLUDE, Operator.EXCLUDE):
placeholders = ", ".join(["?"] * len(self.values))
stmt = [f"{accessor} {self.op.as_sql} ({placeholders})"]
else:
stmt = [f"{accessor} {self.op.as_sql} ?"] * len(self.values)
return " OR ".join(stmt)
class Filters(BaseModel, Generic[TModel]):
filters: List[Filter[TModel]] = []
limit: Optional[int]
offset: Optional[int]
def pagination(self) -> str:
stmt = ""
if self.limit:
stmt += f"LIMIT {self.limit} "
if self.offset:
stmt += f"OFFSET {self.offset}"
return stmt
def where(self, where_stmts: List[str]) -> str:
if self.filters:
for filter in self.filters:
where_stmts.append(filter.statement)
if where_stmts:
return "WHERE " + " AND ".join(where_stmts)
return ""
def values(self, values: List[str]) -> Tuple:
if self.filters:
for filter in self.filters:
values.extend(filter.values)
return tuple(values)
+46 -9
View File
@@ -1,22 +1,27 @@
from http import HTTPStatus
from typing import Optional, Type
from fastapi import Security, status
from fastapi.exceptions import HTTPException
from fastapi import HTTPException, Request, Security, status
from fastapi.openapi.models import APIKey, APIKeyIn
from fastapi.security.api_key import APIKeyHeader, APIKeyQuery
from fastapi.security import APIKeyHeader, APIKeyQuery
from fastapi.security.base import SecurityBase
from pydantic import BaseModel
from pydantic.types import UUID4
from starlette.requests import Request
from lnbits.core.crud import get_user, get_wallet_for_key
from lnbits.core.models import User, Wallet
from lnbits.db import Filter, Filters
from lnbits.requestvars import g
from lnbits.settings import settings
# TODO: fix type ignores
class KeyChecker(SecurityBase):
def __init__(
self, scheme_name: str = None, auto_error: bool = True, api_key: str = None
self,
scheme_name: Optional[str] = None,
auto_error: bool = True,
api_key: Optional[str] = None,
):
self.scheme_name = scheme_name or self.__class__.__name__
self.auto_error = auto_error
@@ -24,13 +29,13 @@ class KeyChecker(SecurityBase):
self._api_key = api_key
if api_key:
key = APIKey(
**{"in": APIKeyIn.query},
**{"in": APIKeyIn.query}, # type: ignore
name="X-API-KEY",
description="Wallet API Key - QUERY",
)
else:
key = APIKey(
**{"in": APIKeyIn.header},
**{"in": APIKeyIn.header}, # type: ignore
name="X-API-KEY",
description="Wallet API Key - HEADER",
)
@@ -70,7 +75,10 @@ class WalletInvoiceKeyChecker(KeyChecker):
"""
def __init__(
self, scheme_name: str = None, auto_error: bool = True, api_key: str = None
self,
scheme_name: Optional[str] = None,
auto_error: bool = True,
api_key: Optional[str] = None,
):
super().__init__(scheme_name, auto_error, api_key)
self._key_type = "invoice"
@@ -86,7 +94,10 @@ class WalletAdminKeyChecker(KeyChecker):
"""
def __init__(
self, scheme_name: str = None, auto_error: bool = True, api_key: str = None
self,
scheme_name: Optional[str] = None,
auto_error: bool = True,
api_key: Optional[str] = None,
):
super().__init__(scheme_name, auto_error, api_key)
self._key_type = "admin"
@@ -266,3 +277,29 @@ async def check_super_user(usr: UUID4) -> User:
detail="User not authorized. No super user privileges.",
)
return user
def parse_filters(model: Type[BaseModel]):
"""
Parses the query params as filters.
:param model: model used for validation of filter values
"""
def dependency(
request: Request, limit: Optional[int] = None, offset: Optional[int] = None
):
params = request.query_params
filters = []
for key in params.keys():
try:
filters.append(Filter.parse_query(key, params.getlist(key), model))
except ValueError:
continue
return Filters(
filters=filters,
limit=limit,
offset=offset,
)
return dependency
+155 -164
View File
@@ -3,20 +3,145 @@ import json
import os
import shutil
import sys
import urllib.request
import zipfile
from http import HTTPStatus
from pathlib import Path
from typing import Any, List, NamedTuple, Optional, Tuple
from urllib import request
import httpx
from fastapi.exceptions import HTTPException
from fastapi import HTTPException
from loguru import logger
from pydantic import BaseModel
from lnbits.settings import settings
class ExplicitRelease(BaseModel):
id: str
name: str
version: str
archive: str
hash: str
dependencies: List[str] = []
icon: Optional[str]
short_description: Optional[str]
html_url: Optional[str]
details: Optional[str]
info_notification: Optional[str]
critical_notification: Optional[str]
class GitHubRelease(BaseModel):
id: str
organisation: str
repository: str
class Manifest(BaseModel):
featured: List[str] = []
extensions: List["ExplicitRelease"] = []
repos: List["GitHubRelease"] = []
class GitHubRepoRelease(BaseModel):
name: str
tag_name: str
zipball_url: str
html_url: str
class GitHubRepo(BaseModel):
stargazers_count: str
html_url: str
default_branch: str
class ExtensionConfig(BaseModel):
name: str
short_description: str
tile: str = ""
def download_url(url, save_path):
with request.urlopen(url) as dl_file:
with open(save_path, "wb") as out_file:
out_file.write(dl_file.read())
def file_hash(filename):
h = hashlib.sha256()
b = bytearray(128 * 1024)
mv = memoryview(b)
with open(filename, "rb", buffering=0) as f:
while n := f.readinto(mv):
h.update(mv[:n])
return h.hexdigest()
async def fetch_github_repo_info(
org: str, repository: str
) -> Tuple[GitHubRepo, GitHubRepoRelease, ExtensionConfig]:
repo_url = f"https://api.github.com/repos/{org}/{repository}"
error_msg = "Cannot fetch extension repo"
repo = await gihub_api_get(repo_url, error_msg)
github_repo = GitHubRepo.parse_obj(repo)
lates_release_url = (
f"https://api.github.com/repos/{org}/{repository}/releases/latest"
)
error_msg = "Cannot fetch extension releases"
latest_release: Any = await gihub_api_get(lates_release_url, error_msg)
config_url = f"https://raw.githubusercontent.com/{org}/{repository}/{github_repo.default_branch}/config.json"
error_msg = "Cannot fetch config for extension"
config = await gihub_api_get(config_url, error_msg)
return (
github_repo,
GitHubRepoRelease.parse_obj(latest_release),
ExtensionConfig.parse_obj(config),
)
async def fetch_manifest(url) -> Manifest:
error_msg = "Cannot fetch extensions manifest"
manifest = await gihub_api_get(url, error_msg)
return Manifest.parse_obj(manifest)
async def fetch_github_releases(org: str, repo: str) -> List[GitHubRepoRelease]:
releases_url = f"https://api.github.com/repos/{org}/{repo}/releases"
error_msg = "Cannot fetch extension releases"
releases = await gihub_api_get(releases_url, error_msg)
return [GitHubRepoRelease.parse_obj(r) for r in releases]
async def gihub_api_get(url: str, error_msg: Optional[str]) -> Any:
async with httpx.AsyncClient() as client:
headers = (
{"Authorization": "Bearer " + settings.lnbits_ext_github_token}
if settings.lnbits_ext_github_token
else None
)
resp = await client.get(
url,
headers=headers,
)
if resp.status_code != 200:
logger.warning(f"{error_msg} ({url}): {resp.text}")
resp.raise_for_status()
return resp.json()
def icon_to_github_url(source_repo: str, path: Optional[str]) -> str:
if not path:
return ""
_, _, *rest = path.split("/")
tail = "/".join(rest)
return f"https://github.com/{source_repo}/raw/main/{tail}"
class Extension(NamedTuple):
code: str
is_valid: bool
@@ -97,12 +222,12 @@ class ExtensionRelease(BaseModel):
version: str
archive: str
source_repo: str
is_github_release = False
hash: Optional[str]
html_url: Optional[str]
description: Optional[str]
is_github_release: bool = False
hash: Optional[str] = None
html_url: Optional[str] = None
description: Optional[str] = None
details_html: Optional[str] = None
icon: Optional[str]
icon: Optional[str] = None
@classmethod
def from_github_release(
@@ -132,52 +257,6 @@ class ExtensionRelease(BaseModel):
return []
class ExplicitRelease(BaseModel):
id: str
name: str
version: str
archive: str
hash: str
dependencies: List[str] = []
icon: Optional[str]
short_description: Optional[str]
html_url: Optional[str]
details: Optional[str]
info_notification: Optional[str]
critical_notification: Optional[str]
class GitHubRelease(BaseModel):
id: str
organisation: str
repository: str
class Manifest(BaseModel):
featured: List[str] = []
extensions: List["ExplicitRelease"] = []
repos: List["GitHubRelease"] = []
class GitHubRepoRelease(BaseModel):
name: str
tag_name: str
zipball_url: str
html_url: str
class GitHubRepo(BaseModel):
stargazers_count: str
html_url: str
default_branch: str
class ExtensionConfig(BaseModel):
name: str
short_description: str
tile: str = ""
class InstallableExtension(BaseModel):
id: str
name: str
@@ -187,8 +266,9 @@ class InstallableExtension(BaseModel):
is_admin_only: bool = False
stars: int = 0
featured = False
latest_release: Optional[ExtensionRelease]
installed_release: Optional[ExtensionRelease]
latest_release: Optional[ExtensionRelease] = None
installed_release: Optional[ExtensionRelease] = None
archive: Optional[str] = None
@property
def hash(self) -> str:
@@ -201,18 +281,18 @@ class InstallableExtension(BaseModel):
return "not-installed"
@property
def zip_path(self) -> str:
extensions_data_dir = os.path.join(settings.lnbits_data_folder, "extensions")
def zip_path(self) -> Path:
extensions_data_dir = Path(settings.lnbits_data_folder, "extensions")
os.makedirs(extensions_data_dir, exist_ok=True)
return os.path.join(extensions_data_dir, f"{self.id}.zip")
return Path(extensions_data_dir, f"{self.id}.zip")
@property
def ext_dir(self) -> str:
return os.path.join("lnbits", "extensions", self.id)
def ext_dir(self) -> Path:
return Path(settings.lnbits_path, "extensions", self.id)
@property
def ext_upgrade_dir(self) -> str:
return os.path.join("lnbits", "upgrades", f"{self.id}-{self.hash}")
def ext_upgrade_dir(self) -> Path:
return Path("lnbits", "upgrades", f"{self.id}-{self.hash}")
@property
def module_name(self) -> str:
@@ -224,21 +304,17 @@ class InstallableExtension(BaseModel):
@property
def has_installed_version(self) -> bool:
if not Path(self.ext_dir).is_dir():
if not self.ext_dir.is_dir():
return False
config_file = os.path.join(self.ext_dir, "config.json")
if not Path(config_file).is_file():
return False
with open(config_file, "r") as json_file:
config_json = json.load(json_file)
return config_json.get("is_installed") is True
return Path(self.ext_dir, "config.json").is_file()
def download_archive(self):
logger.info(f"Downloading extension {self.name}.")
ext_zip_file = self.zip_path
if os.path.isfile(ext_zip_file):
if ext_zip_file.is_file():
os.remove(ext_zip_file)
try:
assert self.installed_release, "installed_release is none."
download_url(self.installed_release.archive, ext_zip_file)
except Exception as ex:
logger.warning(ex)
@@ -250,7 +326,7 @@ class InstallableExtension(BaseModel):
archive_hash = file_hash(ext_zip_file)
if self.installed_release.hash and self.installed_release.hash != archive_hash:
# remove downloaded archive
if os.path.isfile(ext_zip_file):
if ext_zip_file.is_file():
os.remove(ext_zip_file)
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
@@ -259,26 +335,22 @@ class InstallableExtension(BaseModel):
def extract_archive(self):
logger.info(f"Extracting extension {self.name}.")
os.makedirs(os.path.join("lnbits", "upgrades"), exist_ok=True)
os.makedirs(Path("lnbits", "upgrades"), exist_ok=True)
shutil.rmtree(self.ext_upgrade_dir, True)
with zipfile.ZipFile(self.zip_path, "r") as zip_ref:
zip_ref.extractall(self.ext_upgrade_dir)
generated_dir_name = os.listdir(self.ext_upgrade_dir)[0]
os.rename(
os.path.join(self.ext_upgrade_dir, generated_dir_name),
os.path.join(self.ext_upgrade_dir, self.id),
Path(self.ext_upgrade_dir, generated_dir_name),
Path(self.ext_upgrade_dir, self.id),
)
# Pre-packed extensions can be upgraded
# Mark the extension as installed so we know it is not the pre-packed version
with open(
os.path.join(self.ext_upgrade_dir, self.id, "config.json"), "r+"
Path(self.ext_upgrade_dir, self.id, "config.json"), "r+"
) as json_file:
config_json = json.load(json_file)
config_json["is_installed"] = True
json_file.seek(0)
json.dump(config_json, json_file)
json_file.truncate()
self.name = config_json.get("name")
self.short_description = config_json.get("short_description")
@@ -294,8 +366,8 @@ class InstallableExtension(BaseModel):
shutil.rmtree(self.ext_dir, True)
shutil.copytree(
os.path.join(self.ext_upgrade_dir, self.id),
os.path.join("lnbits", "extensions", self.id),
Path(self.ext_upgrade_dir, self.id),
Path(settings.lnbits_path, "extensions", self.id),
)
logger.success(f"Extension {self.name} installed.")
@@ -314,7 +386,7 @@ class InstallableExtension(BaseModel):
def clean_extension_files(self):
# remove downloaded archive
if os.path.isfile(self.zip_path):
if self.zip_path.is_file():
os.remove(self.zip_path)
# remove module from extensions
@@ -343,8 +415,7 @@ class InstallableExtension(BaseModel):
id=github_release.id,
name=config.name,
short_description=config.short_description,
version="0",
stars=repo.stargazers_count,
stars=int(repo.stargazers_count),
icon=icon_to_github_url(
f"{github_release.organisation}/{github_release.repository}",
config.tile,
@@ -363,7 +434,6 @@ class InstallableExtension(BaseModel):
id=e.id,
name=e.name,
archive=e.archive,
hash=e.hash,
short_description=e.short_description,
icon=e.icon,
dependencies=e.dependencies,
@@ -462,82 +532,3 @@ def get_valid_extensions() -> List[Extension]:
return [
extension for extension in ExtensionManager().extensions if extension.is_valid
]
def download_url(url, save_path):
with urllib.request.urlopen(url) as dl_file:
with open(save_path, "wb") as out_file:
out_file.write(dl_file.read())
def file_hash(filename):
h = hashlib.sha256()
b = bytearray(128 * 1024)
mv = memoryview(b)
with open(filename, "rb", buffering=0) as f:
while n := f.readinto(mv):
h.update(mv[:n])
return h.hexdigest()
def icon_to_github_url(source_repo: str, path: Optional[str]) -> str:
if not path:
return ""
_, _, *rest = path.split("/")
tail = "/".join(rest)
return f"https://github.com/{source_repo}/raw/main/{tail}"
async def fetch_github_repo_info(
org: str, repository: str
) -> Tuple[GitHubRepo, GitHubRepoRelease, ExtensionConfig]:
repo_url = f"https://api.github.com/repos/{org}/{repository}"
error_msg = "Cannot fetch extension repo"
repo = await gihub_api_get(repo_url, error_msg)
github_repo = GitHubRepo.parse_obj(repo)
lates_release_url = (
f"https://api.github.com/repos/{org}/{repository}/releases/latest"
)
error_msg = "Cannot fetch extension releases"
latest_release: Any = await gihub_api_get(lates_release_url, error_msg)
config_url = f"https://raw.githubusercontent.com/{org}/{repository}/{github_repo.default_branch}/config.json"
error_msg = "Cannot fetch config for extension"
config = await gihub_api_get(config_url, error_msg)
return (
github_repo,
GitHubRepoRelease.parse_obj(latest_release),
ExtensionConfig.parse_obj(config),
)
async def fetch_manifest(url) -> Manifest:
error_msg = "Cannot fetch extensions manifest"
manifest = await gihub_api_get(url, error_msg)
return Manifest.parse_obj(manifest)
async def fetch_github_releases(org: str, repo: str) -> List[GitHubRepoRelease]:
releases_url = f"https://api.github.com/repos/{org}/{repo}/releases"
error_msg = "Cannot fetch extension releases"
releases = await gihub_api_get(releases_url, error_msg)
return [GitHubRepoRelease.parse_obj(r) for r in releases]
async def gihub_api_get(url: str, error_msg: Optional[str]) -> Any:
async with httpx.AsyncClient() as client:
headers = (
{"Authorization": "Bearer " + settings.lnbits_ext_github_token}
if settings.lnbits_ext_github_token
else None
)
resp = await client.get(
url,
headers=headers,
)
if resp.status_code != 200:
logger.warning(f"{error_msg} ({url}): {resp.text}")
resp.raise_for_status()
return resp.json()
+68 -71
View File
@@ -1,9 +1,13 @@
import glob
import os
from typing import Any, List, Optional
from typing import Any, List, Optional, Type
import jinja2
import shortuuid # type: ignore
import shortuuid
from pydantic import BaseModel
from pydantic.schema import (
field_schema,
get_flat_models_from_fields,
get_model_name_map,
)
from lnbits.jinja2_templating import Jinja2Templates
from lnbits.requestvars import g
@@ -11,75 +15,32 @@ from lnbits.settings import settings
from .extension_manager import get_valid_extensions
vendored_js = [
"/static/vendor/moment.js",
"/static/vendor/underscore.js",
"/static/vendor/axios.js",
"/static/vendor/vue.js",
"/static/vendor/vue-router.js",
"/static/vendor/vue-qrcode-reader.browser.js",
"/static/vendor/vue-qrcode.js",
"/static/vendor/vue-i18n.js",
"/static/vendor/vuex.js",
"/static/vendor/quasar.ie.polyfills.umd.min.js",
"/static/vendor/quasar.umd.js",
"/static/vendor/Chart.bundle.js",
]
vendored_css = [
"/static/vendor/quasar.css",
"/static/vendor/Chart.css",
"/static/vendor/vue-qrcode-reader.css",
]
def urlsafe_short_hash() -> str:
return shortuuid.uuid()
def get_js_vendored(prefer_minified: bool = False) -> List[str]:
paths = get_vendored(".js", prefer_minified)
def sorter(key: str):
if "moment@" in key:
return 1
if "vue@" in key:
return 2
if "vue-router@" in key:
return 3
if "polyfills" in key:
return 4
return 9
return sorted(paths, key=sorter)
def get_css_vendored(prefer_minified: bool = False) -> List[str]:
paths = get_vendored(".css", prefer_minified)
def sorter(key: str):
if "quasar@" in key:
return 1
if "vue@" in key:
return 2
if "chart.js@" in key:
return 100
return 9
return sorted(paths, key=sorter)
def get_vendored(ext: str, prefer_minified: bool = False) -> List[str]:
paths: List[str] = []
for path in glob.glob(
os.path.join(settings.lnbits_path, "static/vendor/**"), recursive=True
):
if path.endswith(".min" + ext):
# path is minified
unminified = path.replace(".min" + ext, ext)
if prefer_minified:
paths.append(path)
if unminified in paths:
paths.remove(unminified)
elif unminified not in paths:
paths.append(path)
elif path.endswith(ext):
# path is not minified
minified = path.replace(ext, ".min" + ext)
if not prefer_minified:
paths.append(path)
if minified in paths:
paths.remove(minified)
elif minified not in paths:
paths.append(path)
return sorted(paths)
def url_for_vendored(abspath: str) -> str:
return "/" + os.path.relpath(abspath, settings.lnbits_path)
def url_for(endpoint: str, external: Optional[bool] = False, **params: Any) -> str:
base = g().base_url if external else ""
url_params = "?"
@@ -89,7 +50,7 @@ def url_for(endpoint: str, external: Optional[bool] = False, **params: Any) -> s
return url
def template_renderer(additional_folders: List = None) -> Jinja2Templates:
def template_renderer(additional_folders: Optional[List] = None) -> Jinja2Templates:
folders = ["lnbits/templates", "lnbits/core/templates"]
if additional_folders:
@@ -118,8 +79,8 @@ def template_renderer(additional_folders: List = None) -> Jinja2Templates:
t.env.globals["USE_CUSTOM_LOGO"] = settings.lnbits_custom_logo
if settings.debug:
t.env.globals["VENDORED_JS"] = map(url_for_vendored, get_js_vendored())
t.env.globals["VENDORED_CSS"] = map(url_for_vendored, get_css_vendored())
t.env.globals["VENDORED_JS"] = vendored_js
t.env.globals["VENDORED_CSS"] = vendored_css
else:
t.env.globals["VENDORED_JS"] = ["/static/bundle.js"]
t.env.globals["VENDORED_CSS"] = ["/static/bundle.css"]
@@ -148,3 +109,39 @@ def get_current_extension_name() -> str:
except:
ext_name = extension_director_name
return ext_name
def generate_filter_params_openapi(model: Type[BaseModel], keep_optional=False):
"""
Generate openapi documentation for Filters. This is intended to be used along parse_filters (see example)
:param model: Filter model
:param keep_optional: If false, all parameters will be optional, otherwise inferred from model
"""
fields = list(model.__fields__.values())
models = get_flat_models_from_fields(fields, set())
namemap = get_model_name_map(models)
params = []
for field in fields:
schema, definitions, _ = field_schema(field, model_name_map=namemap)
# Support nested definition
if "$ref" in schema:
name = schema["$ref"].split("/")[-1]
schema = definitions[name]
description = "Supports Filtering"
if schema["type"] == "object":
description += f". Nested attributes can be filtered too, e.g. `{field.alias}.[additional].[attributes]`"
parameter = {
"name": field.alias,
"in": "query",
"required": field.required if keep_optional else False,
"schema": schema,
"description": description,
}
params.append(parameter)
return {
"parameters": params,
}
+8 -15
View File
@@ -1,25 +1,18 @@
# Borrowed from the excellent accent-starlette
# https://github.com/accent-starlette/starlette-core/blob/master/starlette_core/templating.py
import typing
from starlette import templating
from jinja2 import BaseLoader, Environment, pass_context
from starlette.datastructures import QueryParams
from starlette.requests import Request
try:
import jinja2
except ImportError: # pragma: nocover
jinja2 = None # type: ignore
from starlette.templating import Jinja2Templates as SuperJinja2Templates
class Jinja2Templates(templating.Jinja2Templates):
def __init__(self, loader: jinja2.BaseLoader) -> None: # pylint: disable=W0231
assert jinja2 is not None, "jinja2 must be installed to use Jinja2Templates"
class Jinja2Templates(SuperJinja2Templates):
def __init__(self, loader: BaseLoader) -> None:
super().__init__("")
self.env = self.get_environment(loader)
def get_environment(self, loader: "jinja2.BaseLoader") -> "jinja2.Environment":
@jinja2.pass_context
def get_environment(self, loader: BaseLoader) -> Environment:
@pass_context
def url_for(context: dict, name: str, **path_params: typing.Any) -> str:
request: Request = context["request"]
return request.app.url_path_for(name, **path_params)
@@ -29,7 +22,7 @@ class Jinja2Templates(templating.Jinja2Templates):
values.update(new)
return QueryParams(**values)
env = jinja2.Environment(loader=loader, autoescape=True)
env = Environment(loader=loader, autoescape=True)
env.globals["url_for"] = url_for
env.globals["url_params_update"] = url_params_update
return env
+64 -6
View File
@@ -1,9 +1,11 @@
from http import HTTPStatus
from typing import List, Tuple
from typing import Any, List, Tuple, Union
from urllib.parse import parse_qs
from fastapi.responses import JSONResponse
from fastapi.responses import HTMLResponse, JSONResponse
from starlette.types import ASGIApp, Receive, Scope, Send
from lnbits.helpers import template_renderer
from lnbits.settings import settings
@@ -26,12 +28,21 @@ class InstalledExtensionMiddleware:
else:
_, path_name = path_elements
path_type = None
rest = []
headers = scope.get("headers", [])
# block path for all users if the extension is disabled
if path_name in settings.lnbits_deactivated_extensions:
response = JSONResponse(
status_code=HTTPStatus.NOT_FOUND,
content={"detail": f"Extension '{path_name}' disabled"},
response = self._response_by_accepted_type(
headers, f"Extension '{path_name}' disabled", HTTPStatus.NOT_FOUND
)
await response(scope, receive, send)
return
if not self._user_allowed_to_extension(path_name, scope):
response = self._response_by_accepted_type(
headers, "User not authorized.", HTTPStatus.FORBIDDEN
)
await response(scope, receive, send)
return
@@ -51,6 +62,53 @@ class InstalledExtensionMiddleware:
await self.app(scope, receive, send)
def _user_allowed_to_extension(self, ext_name: str, scope) -> bool:
if ext_name not in settings.lnbits_admin_extensions:
return True
if "query_string" not in scope:
return True
# parse the URL query string into a `dict`
q = parse_qs(scope["query_string"].decode("UTF-8"))
user = q.get("usr", [""])[0]
if not user:
return True
if user == settings.super_user or user in settings.lnbits_admin_users:
return True
return False
def _response_by_accepted_type(
self, headers: List[Any], msg: str, status_code: HTTPStatus
) -> Union[HTMLResponse, JSONResponse]:
"""
Build an HTTP response containing the `msg` as HTTP body and the `status_code` as HTTP code.
If the `accept` HTTP header is present int the request and contains the value of `text/html`
then return an `HTMLResponse`, otherwise return an `JSONResponse`.
"""
accept_header: str = next(
(
h[1].decode("UTF-8")
for h in headers
if len(h) >= 2 and h[0].decode("UTF-8") == "accept"
),
"",
)
if "text/html" in [a for a in accept_header.split(",")]:
return HTMLResponse(
status_code=status_code,
content=template_renderer()
.TemplateResponse("error.html", {"request": {}, "err": msg})
.body,
)
return JSONResponse(
status_code=status_code,
content={"detail": msg},
)
class ExtensionsRedirectMiddleware:
# Extensions are allowed to specify redirect paths.
@@ -88,7 +146,7 @@ class ExtensionsRedirectMiddleware:
if "from_path" not in redirect:
return False
header_filters = (
redirect["header_filters"] if "header_filters" in redirect else []
redirect["header_filters"] if "header_filters" in redirect else {}
)
return self._has_common_path(redirect["from_path"], path) and self._has_headers(
header_filters, req_headers
+13 -4
View File
@@ -1,4 +1,5 @@
import importlib
import importlib.metadata
import inspect
import json
import subprocess
@@ -23,6 +24,7 @@ def list_parse_fallback(v):
class LNbitsSettings(BaseSettings):
@classmethod
def validate(cls, val):
if type(val) == str:
val = val.split(",") if val else []
@@ -102,6 +104,8 @@ class FakeWalletFundingSource(LNbitsSettings):
class LNbitsFundingSource(LNbitsSettings):
lnbits_endpoint: str = Field(default="https://legend.lnbits.com")
lnbits_key: Optional[str] = Field(default=None)
lnbits_admin_key: Optional[str] = Field(default=None)
lnbits_invoice_key: Optional[str] = Field(default=None)
class ClicheFundingSource(LNbitsSettings):
@@ -144,11 +148,14 @@ class LnPayFundingSource(LNbitsSettings):
lnpay_api_endpoint: Optional[str] = Field(default=None)
lnpay_api_key: Optional[str] = Field(default=None)
lnpay_wallet_key: Optional[str] = Field(default=None)
lnpay_admin_key: Optional[str] = Field(default=None)
class OpenNodeFundingSource(LNbitsSettings):
opennode_api_endpoint: Optional[str] = Field(default=None)
opennode_key: Optional[str] = Field(default=None)
opennode_admin_key: Optional[str] = Field(default=None)
opennode_invoice_key: Optional[str] = Field(default=None)
class SparkFundingSource(LNbitsSettings):
@@ -207,8 +214,9 @@ class EditableSettings(
"lnbits_admin_extensions",
pre=True,
)
@classmethod
def validate_editable_settings(cls, val):
return super().validate(cls, val)
return super().validate(val)
@classmethod
def from_dict(cls, d: dict):
@@ -225,6 +233,7 @@ class EnvSettings(LNbitsSettings):
lnbits_path: str = Field(default=".")
lnbits_commit: str = Field(default="unknown")
super_user: str = Field(default="")
version: str = Field(default="0.0.0")
class SaaSSettings(LNbitsSettings):
@@ -279,8 +288,9 @@ class ReadOnlySettings(
"lnbits_allowed_funding_sources",
pre=True,
)
@classmethod
def validate_readonly_settings(cls, val):
return super().validate(cls, val)
return super().validate(val)
@classmethod
def readonly_fields(cls):
@@ -345,8 +355,6 @@ def send_admin_user_to_saas():
)
############### INIT #################
readonly_variables = ReadOnlySettings.readonly_fields()
transient_variables = TransientSettings.readonly_fields()
@@ -366,6 +374,7 @@ try:
except:
settings.lnbits_commit = "docker"
settings.version = importlib.metadata.version("lnbits")
# printing environment variable for debugging
if not settings.lnbits_admin_ui:
+90
View File
@@ -0,0 +1,90 @@
window.localisation.de = {
server: 'Server',
theme: 'Theme',
funding: 'Funding',
users: 'Benutzer',
unit: 'Einheit',
restart: 'Server neu starten',
save: 'Speichern',
save_tooltip: 'Änderungen speichern',
topup: 'Aufladen',
topup_wallet: 'Wallet aufladen',
topup_hint: 'Nutze die Wallet-ID um eine beliebige Wallet aufzuladen',
restart_tooltip: 'Starte den Server neu um die Änderungen zu übernehmen',
add_funds_tooltip: 'Füge Geld zu einer Wallet hinzu.',
reset_defaults: 'Zurücksetzen',
reset_defaults_tooltip:
'Lösche alle Einstellungen und setze auf die Standardeinstellungen zurück.',
download_backup: 'Datenbank-Backup herunterladen',
name_your_wallet: 'Benenne deine %{name} Wallet',
paste_invoice_label:
'Füge eine Rechnung, Zahlungsanforderung oder lnurl ein *',
lnbits_description:
'Einfach zu installieren und leichtgewichtig, LNbits kann auf jeder Lightning-Netzwerk-Funding-Quelle laufen, derzeit unterstützt LND, c-lightning, OpenNode, LNPay und sogar LNbits selbst! Du kannst LNbits für dich selbst laufen lassen oder anderen eine kundige Lösung anbieten. Jede Wallet hat ihre eigenen API-Schlüssel und es gibt keine Begrenzung für die Anzahl der Wallets, die du erstellen kannst. Die Möglichkeit, Gelder zu partitionieren, macht LNbits zu einem nützlichen Werkzeug für die Geldverwaltung und als Entwicklungswerkzeug. Erweiterungen fügen der LNbits zusätzliche Funktionalität hinzu, so dass du mit einer Reihe von vorderen Technologien auf dem Lightning-Netzwerk experimentieren kannst. Wir haben es so einfach wie möglich gemacht, Erweiterungen zu entwickeln, und als freies und Open-Source-Projekt ermutigen wir Menschen, sich selbst zu entwickeln und ihre eigenen Beiträge einzureichen.',
export_to_phone: 'Exportieren Sie auf das Telefon mit QR-Code',
export_to_phone_desc:
'Dieser QR-Code enthält Ihre Wallet-URL mit vollständigem Zugriff. Sie können es von Ihrem Telefon aus scannen, um Ihre Wallet dort zu öffnen.',
wallets: 'Wallets',
add_wallet: 'Füge eine neue Wallet hinzu',
delete_wallet: 'Wallet löschen',
delete_wallet_desc:
'Diese ganze Wallet wird gelöscht, die Mittel sind UNWIEDERBRINGLICH.',
rename_wallet: 'Wallet umbenennen',
update_name: 'Name aktualisieren',
press_to_claim: 'Drücken Sie, um Bitcoin zu beanspruchen',
donate: 'Spenden',
view_github: 'Auf GitHub anzeigen',
voidwallet_active: 'VoidWallet ist aktiv! Zahlungen deaktiviert',
use_with_caution:
'VERWENDEN SIE MIT VORSICHT - %{name} Wallet ist noch in der BETA',
toggle_darkmode: 'Dark Mode umschalten',
view_swagger_docs: 'LNbits Swagger API-Dokumente anzeigen',
api_docs: 'Api docs',
commit_version: 'Commit version',
runs_on: 'Läuft auf',
credit_hint: 'Drücken Sie Enter, um das Konto zu belasten',
credit_label: '%{denomination} zu belasten',
paste_request: 'Anfrage einfügen',
create_invoice: 'Rechnung erstellen',
camera_tooltip:
'Verwenden Sie die Kamera, um eine Rechnung / QR-Code zu scannen',
export_csv: 'Exportieren Sie nach CSV',
transactions: 'Transaktionen',
chart_tooltip: 'Diagramm anzeigen',
pending: 'Ausstehend',
copy_invoice: 'Rechnung kopieren',
close: 'Schließen',
cancel: 'Stornieren',
scan: 'Scannen',
read: 'Lesen',
pay: 'Zahlen',
memo: 'Memo',
date: 'Datum',
processing_payment: 'Zahlung wird bearbeitet ...',
not_enough_funds: 'Nicht genug Geld!',
search_by_tag_memo_amount: 'Suche nach Tag, Memo, Betrag',
invoice_waiting: 'Rechnung wartet auf Zahlung',
payment_received: 'Zahlung erhalten',
payment_sent: 'Zahlung gesendet',
outgoing_payment_pending: 'Ausgehende Zahlung ausstehend',
drain_funds: 'Leeren Sie die Mittel',
drain_funds_desc:
'Dies ist ein LNURL-withdraw QR-Code zum Absaugen aller Mittel aus dieser Brieftasche. Teilen Sie es nicht mit jemandem. Es ist mit balanceCheck und balanceNotify kompatibel, so dass Ihre Brieftasche die Mittel nach dem ersten Abzug kontinuierlich von hier ziehen kann.',
i_understand: 'Ich verstehe',
copy_wallet_url: 'Wallet-URL kopieren',
disclaimer_dialog:
'Login-Funktionalität wird in einem zukünftigen Update veröffentlicht, für jetzt, stellen Sie sicher, dass Sie diese Seite als Lesezeichen speichern, um zukünftigen Zugriff auf Ihre Wallet zu erhalten! Dieser Service ist in BETA und wir übernehmen keine Verantwortung für Personen, die den Zugriff auf ihre Mittel verlieren.',
no_transactions: 'Noch keine Transaktionen',
manage_extensions: 'Erweiterungen verwalten',
manage_server: 'Server verwalten',
extensions: 'Erweiterungen',
no_extensions: 'Sie haben keine Erweiterungen installiert :(',
created: 'Erstellt',
payment_hash: 'Zahlungs-Hash',
fee: 'Gebühr',
amount: 'Menge',
description: 'Beschreibung',
expiry: 'Ablauf',
webhook: 'Webhook',
payment_proof: 'Zahlungsbeleg'
}
+86
View File
@@ -0,0 +1,86 @@
window.localisation.en = {
server: 'Server',
theme: 'Theme',
funding: 'Funding',
users: 'Users',
restart: 'Restart server',
save: 'Save',
save_tooltip: 'Save your changes',
topup: 'Topup',
topup_wallet: 'Topup a wallet',
topup_hint: 'Use the wallet ID to topup any wallet',
restart_tooltip: 'Restart the server for changes to take effect',
add_funds_tooltip: 'Add funds to a wallet.',
reset_defaults: 'Reset to defaults',
reset_defaults_tooltip: 'Delete all settings and reset to defaults.',
download_backup: 'Download database backup',
name_your_wallet: 'Name your %{name} wallet',
paste_invoice_label: 'Paste an invoice, payment request or lnurl code *',
lnbits_description:
'Easy to set up and lightweight, LNbits can run on any lightning-network funding source, currently supporting LND, c-lightning, OpenNode, LNPay 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.',
export_to_phone: 'Export to Phone with QR Code',
export_to_phone_desc:
'This QR code contains your wallet URL with full access. You can scan it from your phone to open your wallet from there.',
wallets: 'Wallets',
add_wallet: 'Add a new wallet',
delete_wallet: 'Delete wallet',
delete_wallet_desc:
'This whole wallet will be deleted, the funds will be UNRECOVERABLE.',
rename_wallet: 'Rename wallet',
update_name: 'Update name',
press_to_claim: 'Press to claim bitcoin',
donate: 'Donate',
view_github: 'View on GitHub',
voidwallet_active: 'VoidWallet is active! Payments disabled',
use_with_caution: 'USE WITH CAUTION - %{name} wallet is still in BETA',
toggle_darkmode: 'Toggle Dark Mode',
view_swagger_docs: 'View LNbits Swagger API docs',
api_docs: 'Api docs',
commit_version: 'Commit version',
runs_on: 'Runs on',
credit_hint: 'Press Enter to credit account',
credit_label: '%{denomination} to credit',
paste_request: 'Paste Request',
create_invoice: 'Create Invoice',
camera_tooltip: 'Use camera to scan an invoice/QR',
export_csv: 'Export to CSV',
transactions: 'Transactions',
chart_tooltip: 'Show chart',
pending: 'Pending',
copy_invoice: 'Copy invoice',
close: 'Close',
cancel: 'Cancel',
scan: 'Scan',
read: 'Read',
pay: 'Pay',
memo: 'Memo',
date: 'Date',
processing_payment: 'Processing payment...',
not_enough_funds: 'Not enough funds!',
search_by_tag_memo_amount: 'Search by tag, memo, amount',
invoice_waiting: 'Invoice waiting to be paid',
payment_received: 'Payment Received',
payment_sent: 'Payment Sent',
outgoing_payment_pending: 'Outgoing payment pending',
drain_funds: 'Drain Funds',
drain_funds_desc:
'This is an LNURL-withdraw QR code for slurping everything from this wallet. Do not share with anyone. It is compatible with balanceCheck and balanceNotify so your wallet may keep pulling the funds continuously from here after the first withdraw.',
i_understand: 'I understand',
copy_wallet_url: 'Copy wallet URL',
disclaimer_dialog:
'Login functionality to be released in a future update, for now, make sure you bookmark this page for future access to your wallet! This service is in BETA, and we hold no responsibility for people losing access to funds.',
no_transactions: 'No transactions made yet',
manage_extensions: 'Manage Extensions',
manage_server: 'Manage Server',
extensions: 'Extensions',
no_extensions: "You don't have any extensions installed :(",
created: 'Created',
payment_hash: 'Payment Hash',
fee: 'Fee',
amount: 'Amount',
unit: 'Unit',
description: 'Description',
expiry: 'Expiry',
webhook: 'Webhook',
payment_proof: 'Payment Proof'
}
+87
View File
@@ -0,0 +1,87 @@
window.localisation.es = {
server: 'Servidor',
theme: 'Tema',
funding: 'Financiación',
unit: 'Unidad',
users: 'Usuarios',
restart: 'Reiniciar el servidor',
save: 'Guardar',
save_tooltip: 'Guardar cambios',
topup: 'Recargar',
topup_wallet: 'Recargar billetera',
topup_hint: 'Utilice el ID de billetera para recargar cualquier billetera',
restart_tooltip: 'Reinicie el servidor para aplicar los cambios',
add_funds_tooltip: 'Agregue fondos a una billetera.',
reset_defaults: 'Restablecer',
reset_defaults_tooltip:
'Borrar todas las configuraciones y restablecer a los valores predeterminados.',
download_backup: 'Descargar copia de seguridad de la base de datos',
name_your_wallet: 'Nombre de su billetera %{name}',
paste_invoice_label: 'Pegue la factura aquí',
lnbits_description:
'Fácil de instalar y liviano, LNbits puede ejecutarse en cualquier fuente de financiación de la red Lightning, actualmente compatible con LND, c-lightning, OpenNode, LNPay y hasta LNbits mismo! Puede ejecutar LNbits para usted mismo o ofrecer una solución competente a otros. Cada billetera tiene su propia clave API y no hay límite para la cantidad de billeteras que puede crear. La capacidad de particionar fondos hace de LNbits una herramienta útil para la administración de fondos y como herramienta de desarrollo. Las extensiones agregan funcionalidad adicional a LNbits, por lo que puede experimentar con una variedad de tecnologías de vanguardia en la red Lightning. Lo hemos hecho lo más simple posible para desarrollar extensiones y, como un proyecto gratuito y de código abierto, animamos a las personas a que se desarrollen a sí mismas y envíen sus propios contribuciones.',
export_to_phone: 'Exportar a teléfono con código QR',
export_to_phone_desc:
'Este código QR contiene su URL de billetera con acceso completo. Puede escanearlo desde su teléfono para abrir su billetera allí.',
wallets: 'Billeteras',
add_wallet: 'Agregar nueva billetera',
delete_wallet: 'Eliminar billetera',
delete_wallet_desc:
'Esta billetera completa se eliminará, los fondos son IRREVERSIBLES.',
rename_wallet: 'Cambiar el nombre de la billetera',
update_name: 'Actualizar nombre',
press_to_claim: 'Presione para reclamar Bitcoin',
donate: 'Donar',
view_github: 'Ver en GitHub',
voidwallet_active: '¡VoidWallet está activo! Pagos desactivados',
use_with_caution: 'USAR CON CUIDADO - %{name} Wallet aún está en BETA',
toggle_darkmode: 'Cambiar modo oscuro',
view_swagger_docs: 'Ver documentos de API de LNbits Swagger',
api_docs: 'Documentos de API',
commit_version: 'Versión de compromiso',
runs_on: 'Corre en',
credit_hint: 'Presione Enter para cargar la cuenta',
credit_label: 'Cargar %{denomination}',
paste_request: 'Pegar solicitud',
create_invoice: 'Crear factura',
camera_tooltip: 'Utilice la cámara para escanear una factura / código QR',
export_csv: 'Exportar a CSV',
transactions: 'Transacciones',
chart_tooltip: 'Mostrar gráfico',
pending: 'Pendiente',
copy_invoice: 'Copiar factura',
close: 'Cerrar',
cancel: 'Cancelar',
scan: 'Escanear',
read: 'Leer',
pay: 'Pagar',
memo: 'Memo',
date: 'Fecha',
processing_payment: 'Procesando pago ...',
not_enough_funds: '¡No hay suficientes fondos!',
search_by_tag_memo_amount: 'Buscar por etiqueta, memo, cantidad',
invoice_waiting: 'Factura esperando pago',
payment_received: 'Pago recibido',
payment_sent: 'Pago enviado',
outgoing_payment_pending: 'Pago saliente pendiente',
drain_funds: 'Drenar fondos',
drain_funds_desc:
'Este es un código QR LNURL-withdraw para drenar todos los fondos de esta billetera. No lo comparta con nadie. Es compatible con balanceCheck y balanceNotify, por lo que su billetera puede continuar drenando los fondos de aquí después del primer drenaje.',
i_understand: 'Lo entiendo',
copy_wallet_url: 'Copiar URL de billetera',
disclaimer_dialog:
'La funcionalidad de inicio de sesión se lanzará en una actualización futura, por ahora, asegúrese de guardar esta página como marcador para acceder a su billetera en el futuro. Este servicio está en BETA y no asumimos ninguna responsabilidad por personas que pierdan el acceso a sus fondos.',
no_transactions: 'No hay transacciones todavía',
manage_extensions: 'Administrar extensiones',
manage_server: 'Administrar servidor',
extensions: 'Extensiones',
no_extensions: 'No tienes extensiones instaladas :(',
created: 'Creado',
payment_hash: 'Hash de pago',
fee: 'Cuota',
amount: 'Cantidad',
description: 'Descripción',
expiry: 'Expiración',
webhook: 'Webhook',
payment_proof: 'Prueba de pago'
}
+1
View File
@@ -0,0 +1 @@
window.localisation = {}
+85
View File
@@ -0,0 +1,85 @@
window.localisation.jp = {
server: 'サーバー',
theme: 'テーマ',
funding: '資金調達',
users: 'ユーザー',
restart: 'サーバーを再起動する',
save: '保存',
unit: '単位',
save_tooltip: '変更を保存する',
topup: 'トップアップ',
topup_wallet: 'ウォレットをトップアップする',
topup_hint: 'ウォレットIDを使用して、任意のウォレットをトップアップできます',
restart_tooltip: 'サーバーを再起動して変更を適用します',
add_funds_tooltip: 'ウォレットに資金を追加します。',
reset_defaults: 'リセット',
reset_defaults_tooltip: 'すべての設定を削除してデフォルトに戻します。',
download_backup: 'データベースのバックアップをダウンロードする',
name_your_wallet: 'あなたのウォレットの名前 %{name}',
paste_invoice_label: '請求書を貼り付けてください',
lnbits_description:
'簡単にインストールでき、軽量で、LNbitsは現在LND、c-lightning、OpenNode、LNPay、さらにLNbits自身で動作する任意のLightningネットワークの資金源で実行できます! LNbitsを自分で実行することも、他の人に優れたソリューションを提供することもできます。各ウォレットには独自のAPIキーがあり、作成できるウォレットの数に制限はありません。資金を分割する機能は、LNbitsを資金管理ツールとして使用したり、開発ツールとして使用したりするための便利なツールです。拡張機能は、LNbitsに追加の機能を追加します。そのため、LNbitsは最先端の技術をネットワークLightningで試すことができます。拡張機能を開発するのは簡単で、無料でオープンソースのプロジェクトであるため、人々が自分で開発し、自分の貢献を送信することを奨励しています。',
export_to_phone: '電話にエクスポート',
export_to_phone_desc:
'ウォレットを電話にエクスポートすると、ウォレットを削除する前にウォレットを復元できます。ウォレットを削除すると、ウォレットの秘密鍵が削除され、ウォレットを復元することはできません。',
wallets: 'ウォレット',
add_wallet: 'ウォレットを追加',
delete_wallet: 'ウォレットを削除',
delete_wallet_desc:
'ウォレットを削除すると、ウォレットの秘密鍵が削除され、ウォレットを復元することはできません。',
rename_wallet: 'ウォレットの名前を変更',
update_name: '名前を更新',
press_to_claim: 'クレームするには押してください',
donate: '寄付',
voidwallet_active: 'Voidwalletアクティブ',
use_with_caution: '注意して使用してください',
toggle_dark_mode: 'ダークモードを切り替える',
view_swagger_docs: 'Swaggerドキュメントを表示',
api_docs: 'APIドキュメント',
commit_version: 'コミットバージョン',
runs_on: 'で実行',
credit_hint:
'クレジットカードを使用して資金を追加するには、LNbitsを使用してください。',
credit_label: 'クレジットカード',
paste_request: 'リクエストを貼り付ける',
create_invoice: '請求書を作成する',
camera_tooltip: 'QRコードを読み取る',
export_csv: 'CSVでエクスポート',
transactions: 'トランザクション',
chart_tooltip: 'チャートを表示するには、グラフの上にカーソルを合わせます',
pending: '保留中',
copy_invoice: '請求書をコピー',
close: '閉じる',
cancel: 'キャンセル',
scan: 'スキャン',
read: '読む',
pay: '支払う',
memo: 'メモ',
date: '日付',
processing_payment: '支払い処理中',
not_enough_funds: '資金が不足しています',
search_by_tag_memo_amount: 'タグ、メモ、金額で検索',
invoice_waiting: '請求書を待っています',
payment_received: 'お支払いありがとうございます',
payment_sent: '支払いが完了しました',
outgoing_payment_pending: '支払い保留中',
drain_funds: '資金を排出する',
drain_funds_desc: 'ウォレットの残高をすべて他のウォレットに送金します',
i_understand: '理解した',
copy_wallet_url: 'ウォレットURLをコピー',
disclaimer_dialog:
'ウォレットを削除すると、ウォレットの秘密鍵が削除され、ウォレットを復元することはできません。ウォレットを削除する前に、ウォレットをエクスポートしてください。',
no_transactions: 'トランザクションはありません',
manage_extensions: '拡張機能を管理する',
manage_server: 'サーバーを管理する',
extensions: '拡張機能',
no_extensions: '拡張機能はありません',
created: '作成済み',
payment_hash: '支払いハッシュ',
fee: '料金',
amount: '量',
description: '説明',
expiry: '有効期限',
webhook: 'ウェブフック',
payment_proof: '支払い証明'
}
+19
View File
@@ -1,6 +1,14 @@
/* globals crypto, moment, Vue, axios, Quasar, _ */
Vue.use(VueI18n)
window.LOCALE = 'en'
window.i18n = new VueI18n({
locale: window.LOCALE,
fallbackLocale: window.LOCALE,
messages: window.localisation
})
window.EventHub = new Vue()
window.LNbits = {
api: {
@@ -320,6 +328,7 @@ window.LNbits = {
}
window.windowMixin = {
i18n: window.i18n,
data: function () {
return {
g: {
@@ -335,6 +344,10 @@ window.windowMixin = {
},
methods: {
changeLanguage: function (newValue) {
window.i18n.locale = newValue
this.$q.localStorage.set('lnbits.lang', newValue)
},
changeColor: function (newValue) {
document.body.setAttribute('data-theme', newValue)
this.$q.localStorage.set('lnbits.theme', newValue)
@@ -364,6 +377,12 @@ window.windowMixin = {
}
this.g.allowedThemes = window.allowedThemes ?? ['bitcoin']
let locale = this.$q.localStorage.getItem('lnbits.lang')
if (locale) {
window.LOCALE = locale
window.i18n.locale = locale
}
addEventListener('offline', event => {
this.g.offline = true
})
+20 -16
View File
@@ -28,7 +28,7 @@ Vue.component('lnbits-wallet-list', {
},
template: `
<q-list v-if="user && user.wallets.length" dense class="lnbits-drawer__q-list">
<q-item-label header>Wallets</q-item-label>
<q-item-label header v-text="$t('wallets')"></q-item-label>
<q-item v-for="wallet in wallets" :key="wallet.id"
clickable
:active="activeWallet && activeWallet.id === wallet.id"
@@ -56,7 +56,7 @@ Vue.component('lnbits-wallet-list', {
<q-icon :name="(showForm) ? 'remove' : 'add'" color="grey-5" size="md"></q-icon>
</q-item-section>
<q-item-section>
<q-item-label lines="1" class="text-caption">Add a wallet</q-item-label>
<q-item-label lines="1" class="text-caption" v-text="$t('add_wallet')"></q-item-label>
</q-item-section>
</q-item>
<q-item v-if="showForm">
@@ -112,7 +112,7 @@ Vue.component('lnbits-extension-list', {
},
template: `
<q-list v-if="user" dense class="lnbits-drawer__q-list">
<q-item-label header>Extensions</q-item-label>
<q-item-label header v-text="$t('extensions')"></q-item-label>
<q-item v-for="extension in userExtensions" :key="extension.code"
clickable
:active="extension.isActive"
@@ -137,7 +137,7 @@ Vue.component('lnbits-extension-list', {
<q-icon name="clear_all" color="grey-5" size="md"></q-icon>
</q-item-section>
<q-item-section>
<q-item-label lines="1" class="text-caption">Extensions</q-item-label>
<q-item-label lines="1" class="text-caption" v-text="$t('extensions')"></q-item-label>
</q-item-section>
</q-item>
<q-item clickable tag="a" :href="['/install?usr=', user.id].join('')">
@@ -145,7 +145,7 @@ Vue.component('lnbits-extension-list', {
<q-icon name="playlist_add" color="grey-5" size="md"></q-icon>
</q-item-section>
<q-item-section>
<q-item-label lines="1" class="text-caption">Manage Extensions</q-item-label>
<q-item-label lines="1" class="text-caption" v-text="$t('manage_extensions')"></q-item-label>
</q-item-section>
</q-item>
</q-list>
@@ -199,7 +199,7 @@ Vue.component('lnbits-admin-ui', {
<q-icon name="admin_panel_settings" color="grey-5" size="md"></q-icon>
</q-item-section>
<q-item-section>
<q-item-label lines="1" class="text-caption">Manage Server</q-item-label>
<q-item-label lines="1" class="text-caption" v-text="$t('manage_server')"></q-item-label>
</q-item-section>
</q-item>
</q-list>
@@ -214,6 +214,7 @@ Vue.component('lnbits-admin-ui', {
Vue.component('lnbits-payment-details', {
props: ['payment'],
mixins: [windowMixin],
data: function () {
return {
LNBITS_DENOMINATION: LNBITS_DENOMINATION
@@ -227,31 +228,34 @@ Vue.component('lnbits-payment-details', {
</q-badge>
</div>
<div class="row">
<div class="col-3"><b>Created</b>:</div>
<div class="col-3"><b v-text="$t('created')"></b>:</div>
<div class="col-9">{{ payment.date }} ({{ payment.dateFrom }})</div>
</div>
<div class="row">
<div class="col-3"><b>Expiry</b>:</div>
<div class="col-3"><b v-text="$t('expiry')"></b>:</div>
<div class="col-9">{{ payment.expirydate }} ({{ payment.expirydateFrom }})</div>
</div>
</div>
<div class="row">
<div class="col-3"><b>Description</b>:</div>
<div class="col-3"><b v-text="$t('description')"></b>:</div>
<div class="col-9">{{ payment.memo }}</div>
</div>
<div class="row">
<div class="col-3"><b>Amount</b>:</div>
<div class="col-3"><b v-text="$t('amount')"></b>:</div>
<div class="col-9">{{ (payment.amount / 1000).toFixed(3) }} {{LNBITS_DENOMINATION}}</div>
</div>
<div class="row">
<div class="col-3"><b>Fee</b>:</div>
<div class="col-3"><b v-text="$t('fee')"></b>:</div>
<div class="col-9">{{ (payment.fee / 1000).toFixed(3) }} {{LNBITS_DENOMINATION}}</div>
</div>
<div class="row">
<div class="col-3"><b>Payment hash</b>:</div>
<div class="col-9 text-wrap mono">{{ payment.payment_hash }}</div>
<div class="col-3"><b v-text="$t('payment_hash')"></b>:</div>
<div class="col-9 text-wrap mono">
{{ payment.payment_hash }}
<q-icon name="content_copy" @click="copyText(payment.payment_hash)" size="1em" color="grey" class="q-mb-xs cursor-pointer" />
</div>
</div>
<div class="row" v-if="payment.webhook">
<div class="col-3"><b>Webhook</b>:</div>
<div class="col-3"><b v-text="$t('webhook')"></b>:</div>
<div class="col-9 text-wrap mono">
{{ payment.webhook }}
<q-badge :color="webhookStatusColor" text-color="white">
@@ -260,7 +264,7 @@ Vue.component('lnbits-payment-details', {
</div>
</div>
<div class="row" v-if="hasPreimage">
<div class="col-3"><b>Payment proof</b>:</div>
<div class="col-3"><b v-text="$t('payment_proof')"></b>:</div>
<div class="col-9 text-wrap mono">{{ payment.preimage }}</div>
</div>
<div class="row" v-for="entry in extras">
+20776
View File
File diff suppressed because it is too large Load Diff
+47
View File
@@ -0,0 +1,47 @@
/*
* DOM element rendering detection
* https://davidwalsh.name/detect-node-insertion
*/
@keyframes chartjs-render-animation {
from { opacity: 0.99; }
to { opacity: 1; }
}
.chartjs-render-monitor {
animation: chartjs-render-animation 0.001s;
}
/*
* DOM element resizing detection
* https://github.com/marcj/css-element-queries
*/
.chartjs-size-monitor,
.chartjs-size-monitor-expand,
.chartjs-size-monitor-shrink {
position: absolute;
direction: ltr;
left: 0;
top: 0;
right: 0;
bottom: 0;
overflow: hidden;
pointer-events: none;
visibility: hidden;
z-index: -1;
}
.chartjs-size-monitor-expand > div {
position: absolute;
width: 1000000px;
height: 1000000px;
left: 0;
top: 0;
}
.chartjs-size-monitor-shrink > div {
position: absolute;
width: 200%;
height: 200%;
left: 0;
top: 0;
}
+2964
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
@keyframes chartjs-render-animation{from{opacity:.99}to{opacity:1}}.chartjs-render-monitor{animation:chartjs-render-animation 1ms}.chartjs-size-monitor,.chartjs-size-monitor-expand,.chartjs-size-monitor-shrink{position:absolute;direction:ltr;left:0;top:0;right:0;bottom:0;overflow:hidden;pointer-events:none;visibility:hidden;z-index:-1}.chartjs-size-monitor-expand>div{position:absolute;width:1000000px;height:1000000px;left:0;top:0}.chartjs-size-monitor-shrink>div{position:absolute;width:200%;height:200%;left:0;top:0}
File diff suppressed because one or more lines are too long
+5685
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+11680
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2042
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+2312
View File
File diff suppressed because it is too large Load Diff
+5509
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+4 -4
View File
@@ -3,7 +3,7 @@ import time
import traceback
import uuid
from http import HTTPStatus
from typing import Dict
from typing import Dict, Optional
from fastapi.exceptions import HTTPException
from loguru import logger
@@ -42,7 +42,7 @@ class SseListenersDict(dict):
A dict of sse listeners.
"""
def __init__(self, name: str = None):
def __init__(self, name: Optional[str] = None):
self.name = name or f"sse_listener_{str(uuid.uuid4())[:8]}"
def __setitem__(self, key, value):
@@ -65,7 +65,7 @@ class SseListenersDict(dict):
invoice_listeners: Dict[str, asyncio.Queue] = SseListenersDict("invoice_listeners")
def register_invoice_listener(send_chan: asyncio.Queue, name: str = None):
def register_invoice_listener(send_chan: asyncio.Queue, name: Optional[str] = None):
"""
A method intended for extensions (and core/tasks.py) to call when they want to be notified about
new invoice payments incoming. Will emit all incoming payments.
@@ -164,7 +164,7 @@ async def check_pending_payments():
async def perform_balance_checks():
while True:
for bc in await get_balance_checks():
redeem_lnurl_withdraw(bc.wallet, bc.url)
await redeem_lnurl_withdraw(bc.wallet, bc.url)
await asyncio.sleep(60 * 60 * 6) # every 6 hours
+52 -11
View File
@@ -46,15 +46,14 @@
</q-toolbar-title>
{% block beta %} {% if VOIDWALLET %}
<q-badge color="red" text-color="black" class="q-mr-md">
<span>VoidWallet is active! Payments disabled</span>
{% raw %}{{ $t('voidwallet_active') }}{% endraw %}
</q-badge>
{%endif%}
<q-badge color="yellow" text-color="black" class="q-mr-md">
<span
><span v-show="$q.screen.gt.sm"
>USE WITH CAUTION - {{SITE_TITLE}} wallet is still in </span
>BETA</span
>
v-show="$q.screen.gt.sm"
v-text='$t("use_with_caution", { name: "{{ SITE_TITLE }}" })'
></span>
</q-badge>
{% endblock %}
<q-badge
@@ -63,8 +62,39 @@
text-color="white"
class="q-mr-md"
>
<span> OFFLINE </span>
<span>OFFLINE</span>
</q-badge>
<q-btn-dropdown
dense
flat
round
size="sm"
icon="language"
class="q-pl-md"
>
<q-list>
<q-item clickable v-close-popup @click="changeLanguage('en')">
<q-item-section>
<q-item-label>EN</q-item-label>
</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="changeLanguage('de')">
<q-item-section>
<q-item-label>DE</q-item-label>
</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="changeLanguage('es')">
<q-item-section>
<q-item-label>ES</q-item-label>
</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="changeLanguage('jp')">
<q-item-section>
<q-item-label>JP</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
<q-btn-dropdown
v-if="g.allowedThemes && g.allowedThemes.length > 1"
dense
@@ -175,7 +205,9 @@
:icon="($q.dark.isActive) ? 'brightness_3' : 'wb_sunny'"
size="sm"
>
<q-tooltip>Toggle Dark Mode</q-tooltip>
<q-tooltip
>{% raw %}{{ $t('toggle_darkmode') }}{% endraw %}</q-tooltip
>
</q-btn>
</q-toolbar>
</q-header>
@@ -213,7 +245,8 @@
<br />
<small v-if="'{{SITE_TITLE}}' == 'LNbits'"
>Commit version: {{LNBITS_VERSION}}</small
>{% raw %}{{ $t('commit_version') }}{% endraw %}:
{{LNBITS_VERSION}}</small
>
</q-toolbar-title>
<q-space></q-space>
@@ -226,8 +259,10 @@
target="_blank"
rel="noopener"
>
API DOCS
<q-tooltip>View LNbits Swagger API docs</q-tooltip>
{% raw %}{{ $t('api_docs') }}{% endraw %}
<q-tooltip
>{% raw %}{{ $t('view_swagger_docs') }}{% endraw %}</q-tooltip
>
</q-btn>
<q-btn
flat
@@ -239,7 +274,7 @@
target="_blank"
rel="noopener"
>
<q-tooltip>View project in GitHub</q-tooltip>
<q-tooltip>{% raw %}{{ $t('view_github') }}{% endraw %}</q-tooltip>
</q-btn>
</q-toolbar>
</q-footer>
@@ -253,8 +288,14 @@
<script src="{{ url }}"></script>
{% endfor %}
<!---->
<script src="/static/i18n/i18n.js"></script>
<script src="/static/i18n/en.js"></script>
<script src="/static/i18n/de.js"></script>
<script src="/static/i18n/es.js"></script>
<script src="/static/i18n/jp.js"></script>
<script src="/static/js/base.js"></script>
<script src="/static/js/components.js"></script>
<script src="/static/js/bolt11-decoder.js"></script>
<script type="text/javascript">
const themes = {{ LNBITS_THEME_OPTIONS | tojson }}
const LNBITS_DENOMINATION = {{ LNBITS_DENOMINATION | tojson}}
+1 -3
View File
@@ -1,8 +1,6 @@
# flake8: noqa: F401
from .cliche import ClicheWallet
from .cln import CoreLightningWallet # legacy .env support
from .cln import CoreLightningWallet
from .cln import CoreLightningWallet as CLightningWallet
from .eclair import EclairWallet
from .fake import FakeWallet
+12 -3
View File
@@ -22,6 +22,8 @@ class ClicheWallet(Wallet):
def __init__(self):
self.endpoint = settings.cliche_endpoint
if not self.endpoint:
raise Exception("cannot initialize cliche")
async def status(self) -> StatusResponse:
try:
@@ -36,7 +38,7 @@ class ClicheWallet(Wallet):
data = json.loads(r)
except:
return StatusResponse(
f"Failed to connect to {self.endpoint}, got: '{r.text[:200]}...'", 0
f"Failed to connect to {self.endpoint}, got: '{r[:200]}...'", 0
)
return StatusResponse(None, data["result"]["wallets"][0]["balance"])
@@ -89,6 +91,13 @@ class ClicheWallet(Wallet):
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
ws = create_connection(self.endpoint)
ws.send(f"pay-invoice --invoice {bolt11}")
checking_id, fee_msat, preimage, error_message, payment_ok = (
None,
None,
None,
None,
None,
)
for _ in range(2):
r = ws.recv()
data = json.loads(r)
@@ -151,9 +160,9 @@ class ClicheWallet(Wallet):
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
while True:
try:
ws = await create_connection(self.endpoint)
ws = create_connection(self.endpoint)
while True:
r = await ws.recv()
r = ws.recv()
data = json.loads(r)
print(data)
try:
+14 -12
View File
@@ -7,10 +7,7 @@ from typing import AsyncGenerator, Dict, Optional
import httpx
from loguru import logger
# TODO: https://github.com/lnbits/lnbits/issues/764
# mypy https://github.com/aaugustin/websockets/issues/940
from websockets import connect # type: ignore
from websockets.client import connect
from lnbits.settings import settings
@@ -34,11 +31,13 @@ class UnknownError(Exception):
class EclairWallet(Wallet):
def __init__(self):
url = settings.eclair_url
self.url = url[:-1] if url.endswith("/") else url
passw = settings.eclair_pass
if not url or not passw:
raise Exception("cannot initialize eclair")
self.url = url[:-1] if url.endswith("/") else url
self.ws_url = f"ws://{urllib.parse.urlsplit(self.url).netloc}/ws"
passw = settings.eclair_pass
encodedAuth = base64.b64encode(f":{passw}".encode())
auth = str(encodedAuth, "utf-8")
self.auth = {"Authorization": f"Basic {auth}"}
@@ -71,7 +70,11 @@ class EclairWallet(Wallet):
**kwargs,
) -> InvoiceResponse:
data: Dict = {"amountMsat": amount * 1000}
data: Dict = {
"amountMsat": amount * 1000,
"description_hash": b"",
"description": memo,
}
if kwargs.get("expiry"):
data["expireIn"] = kwargs["expiry"]
@@ -79,8 +82,6 @@ class EclairWallet(Wallet):
data["descriptionHash"] = description_hash.hex()
elif unhashed_description:
data["descriptionHash"] = hashlib.sha256(unhashed_description).hexdigest()
else:
data["description"] = memo or ""
async with httpx.AsyncClient() as client:
r = await client.post(
@@ -149,6 +150,7 @@ class EclairWallet(Wallet):
}
data = r.json()[-1]
fee_msat = 0
if data["status"]["type"] == "sent":
fee_msat = -data["status"]["feesPaid"]
preimage = data["status"]["paymentPreimage"]
@@ -223,10 +225,10 @@ class EclairWallet(Wallet):
) as ws:
while True:
message = await ws.recv()
message = json.loads(message)
message_json = json.loads(message)
if message and message["type"] == "payment-received":
yield message["paymentHash"]
if message_json and message_json["type"] == "payment-received":
yield message_json["paymentHash"]
except Exception as exc:
logger.error(
+12 -15
View File
@@ -45,19 +45,18 @@ class FakeWallet(Wallet):
) -> InvoiceResponse:
data: Dict = {
"out": False,
"amount": amount,
"amount": amount * 1000,
"currency": "bc",
"privkey": self.privkey,
"memo": None,
"description_hash": None,
"memo": memo,
"description_hash": b"",
"description": "",
"fallback": None,
"expires": None,
"expires": kwargs.get("expiry"),
"timestamp": datetime.now().timestamp(),
"route": None,
"tags_set": [],
}
data["expires"] = kwargs.get("expiry")
data["amount"] = amount * 1000
data["timestamp"] = datetime.now().timestamp()
if description_hash:
data["tags_set"] = ["h"]
data["description_hash"] = description_hash
@@ -69,7 +68,7 @@ class FakeWallet(Wallet):
data["memo"] = memo
data["description"] = memo
randomHash = (
data["privkey"][:6]
self.privkey[:6]
+ hashlib.sha256(str(random.getrandbits(256)).encode()).hexdigest()[6:]
)
data["paymenthash"] = randomHash
@@ -78,12 +77,10 @@ class FakeWallet(Wallet):
return InvoiceResponse(True, checking_id, payment_request)
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
async def pay_invoice(self, bolt11: str, _: int) -> PaymentResponse:
invoice = decode(bolt11)
if (
hasattr(invoice, "checking_id")
and invoice.checking_id[:6] == self.privkey[:6] # type: ignore
):
if invoice.payment_hash[:6] == self.privkey[:6]:
await self.queue.put(invoice)
return PaymentResponse(True, invoice.payment_hash, 0)
else:
@@ -91,10 +88,10 @@ class FakeWallet(Wallet):
ok=False, error_message="Only internal invoices can be used!"
)
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
async def get_invoice_status(self, _: str) -> PaymentStatus:
return PaymentStatus(None)
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
async def get_payment_status(self, _: str) -> PaymentStatus:
return PaymentStatus(None)
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
+3 -4
View File
@@ -21,12 +21,13 @@ class LNbitsWallet(Wallet):
def __init__(self):
self.endpoint = settings.lnbits_endpoint
key = (
settings.lnbits_key
or settings.lnbits_admin_key
or settings.lnbits_invoice_key
)
if not self.endpoint or not key:
raise Exception("cannot initialize lnbits wallet")
self.key = {"X-Api-Key": key}
async def status(self) -> StatusResponse:
@@ -60,7 +61,7 @@ class LNbitsWallet(Wallet):
unhashed_description: Optional[bytes] = None,
**kwargs,
) -> InvoiceResponse:
data: Dict = {"out": False, "amount": amount}
data: Dict = {"out": False, "amount": amount, "memo": memo or ""}
if kwargs.get("expiry"):
data["expiry"] = kwargs["expiry"]
if description_hash:
@@ -68,8 +69,6 @@ class LNbitsWallet(Wallet):
if unhashed_description:
data["unhashed_description"] = unhashed_description.hex()
data["memo"] = memo or ""
async with httpx.AsyncClient() as client:
r = await client.post(
url=f"{self.endpoint}/api/v1/payments", headers=self.key, json=data
+20 -15
View File
@@ -105,9 +105,6 @@ class LndWallet(Wallet):
)
endpoint = settings.lnd_grpc_endpoint
self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
self.port = int(settings.lnd_grpc_port)
self.cert_path = settings.lnd_grpc_cert or settings.lnd_cert
macaroon = (
settings.lnd_grpc_macaroon
@@ -122,8 +119,17 @@ class LndWallet(Wallet):
macaroon = AESCipher(description="macaroon decryption").decrypt(
encrypted_macaroon
)
self.macaroon = load_macaroon(macaroon)
cert_path = settings.lnd_grpc_cert or settings.lnd_cert
if not endpoint or not macaroon or not cert_path or not settings.lnd_grpc_port:
raise Exception("cannot initialize lndrest")
self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
self.port = int(settings.lnd_grpc_port)
self.cert_path = settings.lnd_grpc_cert or settings.lnd_cert
self.macaroon = load_macaroon(macaroon)
self.cert_path = cert_path
cert = open(self.cert_path, "rb").read()
creds = grpc.ssl_channel_credentials(cert)
auth_creds = grpc.metadata_call_credentials(self.metadata_callback)
@@ -140,8 +146,6 @@ class LndWallet(Wallet):
async def status(self) -> StatusResponse:
try:
resp = await self.rpc.ChannelBalance(ln.ChannelBalanceRequest())
except RpcError as exc:
return StatusResponse(str(exc._details), 0)
except Exception as exc:
return StatusResponse(str(exc), 0)
@@ -155,20 +159,23 @@ class LndWallet(Wallet):
unhashed_description: Optional[bytes] = None,
**kwargs,
) -> InvoiceResponse:
params: Dict = {"value": amount, "private": True}
data: Dict = {
"description_hash": b"",
"value": amount,
"private": True,
"memo": memo or "",
}
if kwargs.get("expiry"):
params["expiry"] = kwargs["expiry"]
data["expiry"] = kwargs["expiry"]
if description_hash:
params["description_hash"] = description_hash
data["description_hash"] = description_hash
elif unhashed_description:
params["description_hash"] = hashlib.sha256(
data["description_hash"] = hashlib.sha256(
unhashed_description
).digest() # as bytes directly
else:
params["memo"] = memo or ""
try:
req = ln.Invoice(**params)
req = ln.Invoice(**data)
resp = await self.rpc.AddInvoice(req)
except Exception as exc:
error_message = str(exc)
@@ -188,8 +195,6 @@ class LndWallet(Wallet):
)
try:
resp = await self.routerpc.SendPaymentV2(req).read()
except RpcError as exc:
return PaymentResponse(False, None, None, None, exc._details)
except Exception as exc:
return PaymentResponse(False, None, None, None, str(exc))
+29 -13
View File
@@ -24,11 +24,7 @@ class LndRestWallet(Wallet):
def __init__(self):
endpoint = settings.lnd_rest_endpoint
endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
endpoint = (
"https://" + endpoint if not endpoint.startswith("http") else endpoint
)
self.endpoint = endpoint
cert = settings.lnd_rest_cert
macaroon = (
settings.lnd_rest_macaroon
@@ -43,10 +39,31 @@ class LndRestWallet(Wallet):
macaroon = AESCipher(description="macaroon decryption").decrypt(
encrypted_macaroon
)
if not endpoint:
raise Exception("cannot initialize lndrest: no endpoint")
if not macaroon:
raise Exception("cannot initialize lndrest: no macaroon")
if not cert:
logger.warning(
"no certificate for lndrest provided, this only works if you have a publicly issued certificate"
)
endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
endpoint = (
f"https://{endpoint}" if not endpoint.startswith("http") else endpoint
)
self.endpoint = endpoint
self.macaroon = load_macaroon(macaroon)
# if no cert provided it should be public so we set verify to True
# and it will still check for validity of certificate and fail if its not valid
# even on startup
self.cert = cert or True
self.auth = {"Grpc-Metadata-macaroon": self.macaroon}
self.cert = settings.lnd_rest_cert
async def status(self) -> StatusResponse:
try:
@@ -54,8 +71,9 @@ class LndRestWallet(Wallet):
r = await client.get(
f"{self.endpoint}/v1/balance/channels", headers=self.auth
)
except (httpx.ConnectError, httpx.RequestError):
return StatusResponse(f"Unable to connect to {self.endpoint}.", 0)
r.raise_for_status()
except (httpx.ConnectError, httpx.RequestError) as exc:
return StatusResponse(f"Unable to connect to {self.endpoint}. {exc}", 0)
try:
data = r.json()
@@ -74,7 +92,7 @@ class LndRestWallet(Wallet):
unhashed_description: Optional[bytes] = None,
**kwargs,
) -> InvoiceResponse:
data: Dict = {"value": amount, "private": True}
data: Dict = {"value": amount, "private": True, "memo": memo or ""}
if kwargs.get("expiry"):
data["expiry"] = kwargs["expiry"]
if description_hash:
@@ -85,8 +103,6 @@ class LndRestWallet(Wallet):
data["description_hash"] = base64.b64encode(
hashlib.sha256(unhashed_description).digest()
).decode("ascii")
else:
data["memo"] = memo or ""
async with httpx.AsyncClient(verify=self.cert) as client:
r = await client.post(
@@ -171,9 +187,9 @@ class LndRestWallet(Wallet):
timeout=None, headers=self.auth, verify=self.cert
) as client:
async with client.stream("GET", url) as r:
async for l in r.aiter_lines():
async for json_line in r.aiter_lines():
try:
line = json.loads(l)
line = json.loads(json_line)
if line.get("error"):
logger.error(
line["error"]["message"]
+10 -3
View File
@@ -5,7 +5,7 @@ from http import HTTPStatus
from typing import AsyncGenerator, Dict, Optional
import httpx
from fastapi.exceptions import HTTPException
from fastapi import HTTPException
from loguru import logger
from lnbits.settings import settings
@@ -24,8 +24,13 @@ class LNPayWallet(Wallet):
def __init__(self):
endpoint = settings.lnpay_api_endpoint
wallet_key = settings.lnpay_wallet_key or settings.lnpay_admin_key
if not endpoint or not wallet_key or not settings.lnpay_api_key:
raise Exception("cannot initialize lnpay")
self.wallet_key = wallet_key
self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
self.wallet_key = settings.lnpay_wallet_key or settings.lnpay_admin_key
self.auth = {"X-Api-Key": settings.lnpay_api_key}
async def status(self) -> StatusResponse:
@@ -134,7 +139,9 @@ class LNPayWallet(Wallet):
yield value
async def webhook_listener(self):
text: str = await request.get_data()
# TODO: request.get_data is undefined, was it something with Flask or quart?
# probably issue introduced when refactoring?
text: str = await request.get_data() # type: ignore
try:
data = json.loads(text)
except json.decoder.JSONDecodeError:
+4 -5
View File
@@ -21,13 +21,14 @@ from .base import (
class LnTipsWallet(Wallet):
def __init__(self):
endpoint = settings.lntips_api_endpoint
self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
key = (
settings.lntips_api_key
or settings.lntips_admin_key
or settings.lntips_invoice_key
)
if not endpoint or not key:
raise Exception("cannot initialize lntxbod")
self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
self.auth = {"Authorization": f"Basic {key}"}
async def status(self) -> StatusResponse:
@@ -55,13 +56,11 @@ class LnTipsWallet(Wallet):
unhashed_description: Optional[bytes] = None,
**kwargs,
) -> InvoiceResponse:
data: Dict = {"amount": amount}
data: Dict = {"amount": amount, "description_hash": "", "memo": memo or ""}
if description_hash:
data["description_hash"] = description_hash.hex()
elif unhashed_description:
data["description_hash"] = hashlib.sha256(unhashed_description).hexdigest()
else:
data["memo"] = memo or ""
async with httpx.AsyncClient() as client:
r = await client.post(
+8 -4
View File
@@ -4,7 +4,7 @@ from http import HTTPStatus
from typing import AsyncGenerator, Optional
import httpx
from fastapi.exceptions import HTTPException
from fastapi import HTTPException
from loguru import logger
from lnbits.settings import settings
@@ -24,13 +24,15 @@ class OpenNodeWallet(Wallet):
def __init__(self):
endpoint = settings.opennode_api_endpoint
self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
key = (
settings.opennode_key
or settings.opennode_admin_key
or settings.opennode_invoice_key
)
if not endpoint or not key:
raise Exception("cannot initialize opennode")
self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
self.auth = {"Authorization": key}
async def status(self) -> StatusResponse:
@@ -140,7 +142,9 @@ class OpenNodeWallet(Wallet):
yield value
async def webhook_listener(self):
data = await request.form
# TODO: request.form is undefined, was it something with Flask or quart?
# probably issue introduced when refactoring?
data = await request.form # type: ignore
if "status" not in data or data["status"] != "paid":
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
+40 -27
View File
@@ -28,6 +28,7 @@ class UnknownError(Exception):
class SparkWallet(Wallet):
def __init__(self):
assert settings.spark_url, "spark url does not exist"
self.url = settings.spark_url.replace("/rpc", "")
self.token = settings.spark_token
@@ -46,6 +47,7 @@ class SparkWallet(Wallet):
try:
async with httpx.AsyncClient() as client:
assert self.token, "spark wallet token does not exist"
r = await client.post(
self.url + "/rpc",
headers={"X-Access": self.token},
@@ -60,7 +62,7 @@ class SparkWallet(Wallet):
httpx.HTTPError,
httpx.TimeoutException,
) as exc:
raise UnknownError("error connecting to spark: " + str(exc))
raise UnknownError(f"error connecting to spark: {exc}")
try:
data = r.json()
@@ -133,38 +135,49 @@ class SparkWallet(Wallet):
bolt11=bolt11,
maxfee=fee_limit_msat,
)
fee_msat = -int(r["msatoshi_sent"] - r["msatoshi"])
preimage = r["payment_preimage"]
return PaymentResponse(True, r["payment_hash"], fee_msat, preimage, None)
except (SparkError, UnknownError) as exc:
listpays = await self.listpays(bolt11)
if listpays:
pays = listpays["pays"]
if not listpays:
return PaymentResponse(False, None, None, None, str(exc))
if len(pays) == 0:
return PaymentResponse(False, None, None, None, str(exc))
pays = listpays["pays"]
pay = pays[0]
payment_hash = pay["payment_hash"]
if len(pays) == 0:
return PaymentResponse(False, None, None, None, str(exc))
if len(pays) > 1:
raise SparkError(
f"listpays({payment_hash}) returned an unexpected response: {listpays}"
)
pay = pays[0]
payment_hash = pay["payment_hash"]
if pay["status"] == "failed":
return PaymentResponse(False, None, None, None, str(exc))
elif pay["status"] == "pending":
return PaymentResponse(None, payment_hash, None, None, None)
elif pay["status"] == "complete":
r = pay
r["payment_preimage"] = pay["preimage"]
r["msatoshi"] = int(pay["amount_msat"][0:-4])
r["msatoshi_sent"] = int(pay["amount_sent_msat"][0:-4])
# this may result in an error if it was paid previously
# our database won't allow the same payment_hash to be added twice
# this is good
if len(pays) > 1:
raise SparkError(
f"listpays({payment_hash}) returned an unexpected response: {listpays}"
)
fee_msat = -int(r["msatoshi_sent"] - r["msatoshi"])
preimage = r["payment_preimage"]
return PaymentResponse(True, r["payment_hash"], fee_msat, preimage, None)
if pay["status"] == "failed":
return PaymentResponse(False, None, None, None, str(exc))
if pay["status"] == "pending":
return PaymentResponse(None, payment_hash, None, None, None)
if pay["status"] == "complete":
r = pay
r["payment_preimage"] = pay["preimage"]
r["msatoshi"] = int(pay["amount_msat"][0:-4])
r["msatoshi_sent"] = int(pay["amount_sent_msat"][0:-4])
# this may result in an error if it was paid previously
# our database won't allow the same payment_hash to be added twice
# this is good
fee_msat = -int(r["msatoshi_sent"] - r["msatoshi"])
preimage = r["payment_preimage"]
return PaymentResponse(
True, r["payment_hash"], fee_msat, preimage, None
)
else:
return PaymentResponse(False, None, None, None, str(exc))
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
try:
@@ -205,7 +218,7 @@ class SparkWallet(Wallet):
- int(r["pays"][0]["amount_msat"][0:-4])
)
return PaymentStatus(True, fee_msat, r["pays"][0]["preimage"])
elif status == "failed":
if status == "failed":
return PaymentStatus(False)
return PaymentStatus(None)
raise KeyError("supplied an invalid checking_id")
+4 -1
View File
@@ -24,7 +24,10 @@ class VoidWallet(Wallet):
async def status(self) -> StatusResponse:
logger.warning(
"This backend does nothing, it is here just as a placeholder, you must configure an actual backend before being able to do anything useful with LNbits."
(
"This backend does nothing, it is here just as a placeholder, you must configure an "
"actual backend before being able to do anything useful with LNbits."
)
)
return StatusResponse(None, 0)
+606 -15
View File
@@ -1,19 +1,399 @@
{
"name": "main",
"lockfileVersion": 2,
"name": "lnbits",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "lnbits",
"dependencies": {
"@chenfengyuan/vue-qrcode": "1.0.2",
"axios": "^1.3.4",
"chart.js": "2.9",
"moment": "^2.29.4",
"quasar": "1.13.2",
"underscore": "^1.13.6",
"vue": "2.6.12",
"vue-i18n": "^8.28.2",
"vue-qrcode-reader": "2.2",
"vue-router": "3.4.3",
"vuex": "3.5.1"
},
"devDependencies": {
"prettier": "2.8.3",
"pyright": "1.1.289"
}
},
"node_modules/@chenfengyuan/vue-qrcode": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@chenfengyuan/vue-qrcode/-/vue-qrcode-1.0.2.tgz",
"integrity": "sha512-hwy1d4YMJAyEh+V7dLPG8eAKACRvugzSB4ylwb6QNqo84KHTF50/5EJcBYdUhTRPfAqrxG0i6jDAXONWOGyQbQ==",
"license": "MIT",
"dependencies": {
"qrcode": "^1.4.4"
},
"peerDependencies": {
"vue": "^2.6.0"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/ansi-styles/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz",
"integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/babel-runtime": {
"version": "6.26.0",
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
"integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==",
"license": "MIT",
"dependencies": {
"core-js": "^2.4.0",
"regenerator-runtime": "^0.11.0"
}
},
"node_modules/callforth": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/callforth/-/callforth-0.3.1.tgz",
"integrity": "sha512-Q2zPfqnwoKsb1DTVCr4lmhe49wKNBsMmNlbudjleu3/co+Nw1pOqFHYJHrW3VZ253ou9AAr+xauQR0C55NPdzA==",
"license": "MIT"
},
"node_modules/camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/chart.js": {
"version": "2.9.4",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.9.4.tgz",
"integrity": "sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==",
"license": "MIT",
"dependencies": {
"chartjs-color": "^2.1.0",
"moment": "^2.10.2"
}
},
"node_modules/chartjs-color": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.4.1.tgz",
"integrity": "sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==",
"license": "MIT",
"dependencies": {
"chartjs-color-string": "^0.6.0",
"color-convert": "^1.9.3"
}
},
"node_modules/chartjs-color-string": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz",
"integrity": "sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==",
"license": "MIT",
"dependencies": {
"color-name": "^1.0.0"
}
},
"node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^6.2.0"
}
},
"node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"license": "MIT",
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/color-convert/node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"license": "MIT"
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/core-js": {
"version": "2.6.12",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz",
"integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==",
"hasInstallScript": true,
"license": "MIT"
},
"node_modules/decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dijkstrajs": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.2.tgz",
"integrity": "sha512-QV6PMaHTCNmKSeP6QoXhVTw9snc9VD8MulTT0Bd99Pacp4SS1cjcrYPgBPmibqKVtMJJfqC6XvOXgPMEEPH/fg==",
"license": "MIT"
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/encode-utf8": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz",
"integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==",
"license": "MIT"
},
"node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"license": "MIT",
"dependencies": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/jsqr": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/jsqr/-/jsqr-1.4.0.tgz",
"integrity": "sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A==",
"license": "Apache-2.0"
},
"node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"license": "MIT",
"dependencies": {
"p-locate": "^4.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/moment": {
"version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
"engines": {
"node": "*"
}
},
"node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"license": "MIT",
"dependencies": {
"p-try": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"license": "MIT",
"dependencies": {
"p-limit": "^2.2.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/pngjs": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
"license": "MIT",
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/prettier": {
"version": "2.8.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.3.tgz",
"integrity": "sha512-tJ/oJ4amDihPoufT5sM0Z1SKEuKay8LfVAMlbbhnnkvt6BUserZylqo2PN+p9KeljLr0OHa2rXHU1T8reeoTrw==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin-prettier.js"
},
@@ -24,11 +404,17 @@
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/pyright": {
"version": "1.1.289",
"resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.289.tgz",
"integrity": "sha512-fG3STxnwAt3i7bxbXUPJdYNFrcOWHLwCSEOySH2foUqtYdzWLcxDez0Kgl1X8LMQx0arMJ6HRkKghxfRD1/z6g==",
"dev": true,
"license": "MIT",
"bin": {
"pyright": "index.js",
"pyright-langserver": "langserver.index.js"
@@ -36,20 +422,225 @@
"engines": {
"node": ">=12.0.0"
}
}
},
"dependencies": {
"prettier": {
"version": "2.8.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.3.tgz",
"integrity": "sha512-tJ/oJ4amDihPoufT5sM0Z1SKEuKay8LfVAMlbbhnnkvt6BUserZylqo2PN+p9KeljLr0OHa2rXHU1T8reeoTrw==",
"dev": true
},
"pyright": {
"version": "1.1.289",
"resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.289.tgz",
"integrity": "sha512-fG3STxnwAt3i7bxbXUPJdYNFrcOWHLwCSEOySH2foUqtYdzWLcxDez0Kgl1X8LMQx0arMJ6HRkKghxfRD1/z6g==",
"dev": true
"node_modules/qrcode": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.1.tgz",
"integrity": "sha512-nS8NJ1Z3md8uTjKtP+SGGhfqmTCs5flU/xR623oI0JX+Wepz9R8UrRVCTBTJm3qGw3rH6jJ6MUHjkDx15cxSSg==",
"license": "MIT",
"dependencies": {
"dijkstrajs": "^1.0.1",
"encode-utf8": "^1.0.3",
"pngjs": "^5.0.0",
"yargs": "^15.3.1"
},
"bin": {
"qrcode": "bin/qrcode"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/quasar": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/quasar/-/quasar-1.13.2.tgz",
"integrity": "sha512-HJtWz38rLXLvTJqZ5c9GIS6SSwLjN0t8iTwAwD2AQ5FOsUTji6sq3XzzoPll5degXFevnVJlyFH5YTqMYnawVg==",
"engines": {
"node": ">= 10.0.0",
"npm": ">= 5.6.0",
"yarn": ">= 1.6.0"
},
"funding": {
"type": "github",
"url": "https://donate.quasar.dev"
}
},
"node_modules/regenerator-runtime": {
"version": "0.11.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
"integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==",
"license": "MIT"
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"license": "ISC"
},
"node_modules/rtcpeerconnection-shim": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.15.tgz",
"integrity": "sha512-C6DxhXt7bssQ1nHb154lqeL0SXz5Dx4RczXZu2Aa/L1NJFnEVDxFwCBo3fqtuljhHIGceg5JKBV4XJ0gW5JKyw==",
"license": "BSD-3-Clause",
"dependencies": {
"sdp": "^2.6.0"
},
"engines": {
"node": ">=6.0.0",
"npm": ">=3.10.0"
}
},
"node_modules/sdp": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/sdp/-/sdp-2.12.0.tgz",
"integrity": "sha512-jhXqQAQVM+8Xj5EjJGVweuEzgtGWb3tmEEpl3CLP3cStInSbVHSg0QWOGQzNq8pSID4JkpeV2mPqlMDLrm0/Vw==",
"license": "MIT"
},
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"license": "ISC"
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/underscore": {
"version": "1.13.6",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz",
"integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A=="
},
"node_modules/vue": {
"version": "2.6.12",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.12.tgz",
"integrity": "sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg=="
},
"node_modules/vue-i18n": {
"version": "8.28.2",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-8.28.2.tgz",
"integrity": "sha512-C5GZjs1tYlAqjwymaaCPDjCyGo10ajUphiwA922jKt9n7KPpqR7oM1PCwYzhB/E7+nT3wfdG3oRre5raIT1rKA=="
},
"node_modules/vue-qrcode-reader": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/vue-qrcode-reader/-/vue-qrcode-reader-2.2.0.tgz",
"integrity": "sha512-xsNptrJGTFFjwfksIK6KlbjsyIGBLTFgO9KH3JcmZzABY19c2d0+zG//LXkp8K2Jl+4APidktVtH/aTbmcgX7g==",
"license": "MIT",
"dependencies": {
"babel-runtime": "^6.26.0",
"callforth": "^0.3.0",
"jsqr": "^1.2.0",
"webrtc-adapter": "^6.2.1"
}
},
"node_modules/vue-router": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.4.3.tgz",
"integrity": "sha512-BADg1mjGWX18Dpmy6bOGzGNnk7B/ZA0RxuA6qedY/YJwirMfKXIDzcccmHbQI0A6k5PzMdMloc0ElHfyOoX35A=="
},
"node_modules/vuex": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/vuex/-/vuex-3.5.1.tgz",
"integrity": "sha512-w7oJzmHQs0FM9LXodfskhw9wgKBiaB+totOdb8sNzbTB2KDCEEwEs29NzBZFh/lmEK1t5tDmM1vtsO7ubG1DFw==",
"peerDependencies": {
"vue": "^2.0.0"
}
},
"node_modules/webrtc-adapter": {
"version": "6.4.8",
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-6.4.8.tgz",
"integrity": "sha512-YM8yl545c/JhYcjGHgaCoA7jRK/KZuMwEDFeP2AcP0Auv5awEd+gZE0hXy9z7Ed3p9HvAXp8jdbe+4ESb1zxAw==",
"license": "BSD-3-Clause",
"dependencies": {
"rtcpeerconnection-shim": "^1.2.14",
"sdp": "^2.9.0"
},
"engines": {
"node": ">=6.0.0",
"npm": ">=3.10.0"
}
},
"node_modules/which-module": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
"integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==",
"license": "ISC"
},
"node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
"license": "ISC"
},
"node_modules/yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
"license": "MIT",
"dependencies": {
"cliui": "^6.0.0",
"decamelize": "^1.2.0",
"find-up": "^4.1.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^4.2.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^18.1.2"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs-parser": {
"version": "18.1.3",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"license": "ISC",
"dependencies": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
},
"engines": {
"node": ">=6"
}
}
}
}
+14
View File
@@ -1,6 +1,20 @@
{
"name": "lnbits",
"devDependencies": {
"prettier": "2.8.3",
"pyright": "1.1.289"
},
"dependencies": {
"@chenfengyuan/vue-qrcode": "1.0.2",
"axios": "^1.3.4",
"chart.js": "2.9",
"moment": "^2.29.4",
"quasar": "1.13.2",
"underscore": "^1.13.6",
"vue": "2.6.12",
"vue-i18n": "^8.28.2",
"vue-qrcode-reader": "2.2",
"vue-router": "3.4.3",
"vuex": "3.5.1"
}
}
Generated
+154 -196
View File
@@ -15,7 +15,6 @@ files = [
[package.dependencies]
idna = ">=2.8"
sniffio = ">=1.1"
typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
[package.extras]
doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
@@ -36,19 +35,18 @@ files = [
[[package]]
name = "astroid"
version = "2.14.1"
version = "2.15.2"
description = "An abstract syntax tree for Python with inference support."
category = "dev"
optional = false
python-versions = ">=3.7.2"
files = [
{file = "astroid-2.14.1-py3-none-any.whl", hash = "sha256:23c718921acab5f08cbbbe9293967f1f8fec40c336d19cd75dc12a9ea31d2eb2"},
{file = "astroid-2.14.1.tar.gz", hash = "sha256:bd1aa4f9915c98e8aaebcd4e71930154d4e8c9aaf05d35ac0a63d1956091ae3f"},
{file = "astroid-2.15.2-py3-none-any.whl", hash = "sha256:dea89d9f99f491c66ac9c04ebddf91e4acf8bd711722175fe6245c0725cc19bb"},
{file = "astroid-2.15.2.tar.gz", hash = "sha256:6e61b85c891ec53b07471aec5878f4ac6446a41e590ede0f2ce095f39f7d49dd"},
]
[package.dependencies]
lazy-object-proxy = ">=1.4.0"
typed-ast = {version = ">=1.4.0,<2.0", markers = "implementation_name == \"cpython\" and python_version < \"3.8\""}
typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""}
wrapt = [
{version = ">=1.11,<2", markers = "python_version < \"3.11\""},
@@ -67,9 +65,6 @@ files = [
{file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"},
]
[package.dependencies]
typing-extensions = {version = ">=3.6.5", markers = "python_version < \"3.8\""}
[[package]]
name = "attrs"
version = "22.2.0"
@@ -157,7 +152,6 @@ mypy-extensions = ">=0.4.3"
pathspec = ">=0.9.0"
platformdirs = ">=2"
tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""}
typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""}
typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}
[package.extras]
@@ -378,7 +372,6 @@ files = [
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
[[package]]
name = "coincurve"
@@ -442,63 +435,63 @@ files = [
[[package]]
name = "coverage"
version = "7.1.0"
version = "7.2.3"
description = "Code coverage measurement for Python"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "coverage-7.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3b946bbcd5a8231383450b195cfb58cb01cbe7f8949f5758566b881df4b33baf"},
{file = "coverage-7.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ec8e767f13be637d056f7e07e61d089e555f719b387a7070154ad80a0ff31801"},
{file = "coverage-7.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4a5a5879a939cb84959d86869132b00176197ca561c664fc21478c1eee60d75"},
{file = "coverage-7.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b643cb30821e7570c0aaf54feaf0bfb630b79059f85741843e9dc23f33aaca2c"},
{file = "coverage-7.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32df215215f3af2c1617a55dbdfb403b772d463d54d219985ac7cd3bf124cada"},
{file = "coverage-7.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:33d1ae9d4079e05ac4cc1ef9e20c648f5afabf1a92adfaf2ccf509c50b85717f"},
{file = "coverage-7.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:29571503c37f2ef2138a306d23e7270687c0efb9cab4bd8038d609b5c2393a3a"},
{file = "coverage-7.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:63ffd21aa133ff48c4dff7adcc46b7ec8b565491bfc371212122dd999812ea1c"},
{file = "coverage-7.1.0-cp310-cp310-win32.whl", hash = "sha256:4b14d5e09c656de5038a3f9bfe5228f53439282abcab87317c9f7f1acb280352"},
{file = "coverage-7.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:8361be1c2c073919500b6601220a6f2f98ea0b6d2fec5014c1d9cfa23dd07038"},
{file = "coverage-7.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:da9b41d4539eefd408c46725fb76ecba3a50a3367cafb7dea5f250d0653c1040"},
{file = "coverage-7.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5b15ed7644ae4bee0ecf74fee95808dcc34ba6ace87e8dfbf5cb0dc20eab45a"},
{file = "coverage-7.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d12d076582507ea460ea2a89a8c85cb558f83406c8a41dd641d7be9a32e1274f"},
{file = "coverage-7.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2617759031dae1bf183c16cef8fcfb3de7617f394c813fa5e8e46e9b82d4222"},
{file = "coverage-7.1.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4e4881fa9e9667afcc742f0c244d9364d197490fbc91d12ac3b5de0bf2df146"},
{file = "coverage-7.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9d58885215094ab4a86a6aef044e42994a2bd76a446dc59b352622655ba6621b"},
{file = "coverage-7.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ffeeb38ee4a80a30a6877c5c4c359e5498eec095878f1581453202bfacc8fbc2"},
{file = "coverage-7.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3baf5f126f30781b5e93dbefcc8271cb2491647f8283f20ac54d12161dff080e"},
{file = "coverage-7.1.0-cp311-cp311-win32.whl", hash = "sha256:ded59300d6330be27bc6cf0b74b89ada58069ced87c48eaf9344e5e84b0072f7"},
{file = "coverage-7.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:6a43c7823cd7427b4ed763aa7fb63901ca8288591323b58c9cd6ec31ad910f3c"},
{file = "coverage-7.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7a726d742816cb3a8973c8c9a97539c734b3a309345236cd533c4883dda05b8d"},
{file = "coverage-7.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc7c85a150501286f8b56bd8ed3aa4093f4b88fb68c0843d21ff9656f0009d6a"},
{file = "coverage-7.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5b4198d85a3755d27e64c52f8c95d6333119e49fd001ae5798dac872c95e0f8"},
{file = "coverage-7.1.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddb726cb861c3117a553f940372a495fe1078249ff5f8a5478c0576c7be12050"},
{file = "coverage-7.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:51b236e764840a6df0661b67e50697aaa0e7d4124ca95e5058fa3d7cbc240b7c"},
{file = "coverage-7.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7ee5c9bb51695f80878faaa5598040dd6c9e172ddcf490382e8aedb8ec3fec8d"},
{file = "coverage-7.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c31b75ae466c053a98bf26843563b3b3517b8f37da4d47b1c582fdc703112bc3"},
{file = "coverage-7.1.0-cp37-cp37m-win32.whl", hash = "sha256:3b155caf3760408d1cb903b21e6a97ad4e2bdad43cbc265e3ce0afb8e0057e73"},
{file = "coverage-7.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2a60d6513781e87047c3e630b33b4d1e89f39836dac6e069ffee28c4786715f5"},
{file = "coverage-7.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f2cba5c6db29ce991029b5e4ac51eb36774458f0a3b8d3137241b32d1bb91f06"},
{file = "coverage-7.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:beeb129cacea34490ffd4d6153af70509aa3cda20fdda2ea1a2be870dfec8d52"},
{file = "coverage-7.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c45948f613d5d18c9ec5eaa203ce06a653334cf1bd47c783a12d0dd4fd9c851"},
{file = "coverage-7.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef382417db92ba23dfb5864a3fc9be27ea4894e86620d342a116b243ade5d35d"},
{file = "coverage-7.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c7c0d0827e853315c9bbd43c1162c006dd808dbbe297db7ae66cd17b07830f0"},
{file = "coverage-7.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e5cdbb5cafcedea04924568d990e20ce7f1945a1dd54b560f879ee2d57226912"},
{file = "coverage-7.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9817733f0d3ea91bea80de0f79ef971ae94f81ca52f9b66500c6a2fea8e4b4f8"},
{file = "coverage-7.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:218fe982371ac7387304153ecd51205f14e9d731b34fb0568181abaf7b443ba0"},
{file = "coverage-7.1.0-cp38-cp38-win32.whl", hash = "sha256:04481245ef966fbd24ae9b9e537ce899ae584d521dfbe78f89cad003c38ca2ab"},
{file = "coverage-7.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:8ae125d1134bf236acba8b83e74c603d1b30e207266121e76484562bc816344c"},
{file = "coverage-7.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2bf1d5f2084c3932b56b962a683074a3692bce7cabd3aa023c987a2a8e7612f6"},
{file = "coverage-7.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:98b85dd86514d889a2e3dd22ab3c18c9d0019e696478391d86708b805f4ea0fa"},
{file = "coverage-7.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38da2db80cc505a611938d8624801158e409928b136c8916cd2e203970dde4dc"},
{file = "coverage-7.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3164d31078fa9efe406e198aecd2a02d32a62fecbdef74f76dad6a46c7e48311"},
{file = "coverage-7.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db61a79c07331e88b9a9974815c075fbd812bc9dbc4dc44b366b5368a2936063"},
{file = "coverage-7.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ccb092c9ede70b2517a57382a601619d20981f56f440eae7e4d7eaafd1d1d09"},
{file = "coverage-7.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:33ff26d0f6cc3ca8de13d14fde1ff8efe1456b53e3f0273e63cc8b3c84a063d8"},
{file = "coverage-7.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d47dd659a4ee952e90dc56c97d78132573dc5c7b09d61b416a9deef4ebe01a0c"},
{file = "coverage-7.1.0-cp39-cp39-win32.whl", hash = "sha256:d248cd4a92065a4d4543b8331660121b31c4148dd00a691bfb7a5cdc7483cfa4"},
{file = "coverage-7.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:7ed681b0f8e8bcbbffa58ba26fcf5dbc8f79e7997595bf071ed5430d8c08d6f3"},
{file = "coverage-7.1.0-pp37.pp38.pp39-none-any.whl", hash = "sha256:755e89e32376c850f826c425ece2c35a4fc266c081490eb0a841e7c1cb0d3bda"},
{file = "coverage-7.1.0.tar.gz", hash = "sha256:10188fe543560ec4874f974b5305cd1a8bdcfa885ee00ea3a03733464c4ca265"},
{file = "coverage-7.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e58c0d41d336569d63d1b113bd573db8363bc4146f39444125b7f8060e4e04f5"},
{file = "coverage-7.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:344e714bd0fe921fc72d97404ebbdbf9127bac0ca1ff66d7b79efc143cf7c0c4"},
{file = "coverage-7.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:974bc90d6f6c1e59ceb1516ab00cf1cdfbb2e555795d49fa9571d611f449bcb2"},
{file = "coverage-7.2.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0743b0035d4b0e32bc1df5de70fba3059662ace5b9a2a86a9f894cfe66569013"},
{file = "coverage-7.2.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d0391fb4cfc171ce40437f67eb050a340fdbd0f9f49d6353a387f1b7f9dd4fa"},
{file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a42e1eff0ca9a7cb7dc9ecda41dfc7cbc17cb1d02117214be0561bd1134772b"},
{file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:be19931a8dcbe6ab464f3339966856996b12a00f9fe53f346ab3be872d03e257"},
{file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:72fcae5bcac3333a4cf3b8f34eec99cea1187acd55af723bcbd559adfdcb5535"},
{file = "coverage-7.2.3-cp310-cp310-win32.whl", hash = "sha256:aeae2aa38395b18106e552833f2a50c27ea0000122bde421c31d11ed7e6f9c91"},
{file = "coverage-7.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:83957d349838a636e768251c7e9979e899a569794b44c3728eaebd11d848e58e"},
{file = "coverage-7.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dfd393094cd82ceb9b40df4c77976015a314b267d498268a076e940fe7be6b79"},
{file = "coverage-7.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:182eb9ac3f2b4874a1f41b78b87db20b66da6b9cdc32737fbbf4fea0c35b23fc"},
{file = "coverage-7.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bb1e77a9a311346294621be905ea8a2c30d3ad371fc15bb72e98bfcfae532df"},
{file = "coverage-7.2.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca0f34363e2634deffd390a0fef1aa99168ae9ed2af01af4a1f5865e362f8623"},
{file = "coverage-7.2.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55416d7385774285b6e2a5feca0af9652f7f444a4fa3d29d8ab052fafef9d00d"},
{file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:06ddd9c0249a0546997fdda5a30fbcb40f23926df0a874a60a8a185bc3a87d93"},
{file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:fff5aaa6becf2c6a1699ae6a39e2e6fb0672c2d42eca8eb0cafa91cf2e9bd312"},
{file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ea53151d87c52e98133eb8ac78f1206498c015849662ca8dc246255265d9c3c4"},
{file = "coverage-7.2.3-cp311-cp311-win32.whl", hash = "sha256:8f6c930fd70d91ddee53194e93029e3ef2aabe26725aa3c2753df057e296b925"},
{file = "coverage-7.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:fa546d66639d69aa967bf08156eb8c9d0cd6f6de84be9e8c9819f52ad499c910"},
{file = "coverage-7.2.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2317d5ed777bf5a033e83d4f1389fd4ef045763141d8f10eb09a7035cee774c"},
{file = "coverage-7.2.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be9824c1c874b73b96288c6d3de793bf7f3a597770205068c6163ea1f326e8b9"},
{file = "coverage-7.2.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c3b2803e730dc2797a017335827e9da6da0e84c745ce0f552e66400abdfb9a1"},
{file = "coverage-7.2.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f69770f5ca1994cb32c38965e95f57504d3aea96b6c024624fdd5bb1aa494a1"},
{file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1127b16220f7bfb3f1049ed4a62d26d81970a723544e8252db0efde853268e21"},
{file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:aa784405f0c640940595fa0f14064d8e84aff0b0f762fa18393e2760a2cf5841"},
{file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3146b8e16fa60427e03884301bf8209221f5761ac754ee6b267642a2fd354c48"},
{file = "coverage-7.2.3-cp37-cp37m-win32.whl", hash = "sha256:1fd78b911aea9cec3b7e1e2622c8018d51c0d2bbcf8faaf53c2497eb114911c1"},
{file = "coverage-7.2.3-cp37-cp37m-win_amd64.whl", hash = "sha256:0f3736a5d34e091b0a611964c6262fd68ca4363df56185902528f0b75dbb9c1f"},
{file = "coverage-7.2.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:981b4df72c93e3bc04478153df516d385317628bd9c10be699c93c26ddcca8ab"},
{file = "coverage-7.2.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0045f8f23a5fb30b2eb3b8a83664d8dc4fb58faddf8155d7109166adb9f2040"},
{file = "coverage-7.2.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f760073fcf8f3d6933178d67754f4f2d4e924e321f4bb0dcef0424ca0215eba1"},
{file = "coverage-7.2.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c86bd45d1659b1ae3d0ba1909326b03598affbc9ed71520e0ff8c31a993ad911"},
{file = "coverage-7.2.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:172db976ae6327ed4728e2507daf8a4de73c7cc89796483e0a9198fd2e47b462"},
{file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d2a3a6146fe9319926e1d477842ca2a63fe99af5ae690b1f5c11e6af074a6b5c"},
{file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f649dd53833b495c3ebd04d6eec58479454a1784987af8afb77540d6c1767abd"},
{file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7c4ed4e9f3b123aa403ab424430b426a1992e6f4c8fd3cb56ea520446e04d152"},
{file = "coverage-7.2.3-cp38-cp38-win32.whl", hash = "sha256:eb0edc3ce9760d2f21637766c3aa04822030e7451981ce569a1b3456b7053f22"},
{file = "coverage-7.2.3-cp38-cp38-win_amd64.whl", hash = "sha256:63cdeaac4ae85a179a8d6bc09b77b564c096250d759eed343a89d91bce8b6367"},
{file = "coverage-7.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:20d1a2a76bb4eb00e4d36b9699f9b7aba93271c9c29220ad4c6a9581a0320235"},
{file = "coverage-7.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ea748802cc0de4de92ef8244dd84ffd793bd2e7be784cd8394d557a3c751e21"},
{file = "coverage-7.2.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21b154aba06df42e4b96fc915512ab39595105f6c483991287021ed95776d934"},
{file = "coverage-7.2.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd214917cabdd6f673a29d708574e9fbdb892cb77eb426d0eae3490d95ca7859"},
{file = "coverage-7.2.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c2e58e45fe53fab81f85474e5d4d226eeab0f27b45aa062856c89389da2f0d9"},
{file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:87ecc7c9a1a9f912e306997ffee020297ccb5ea388421fe62a2a02747e4d5539"},
{file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:387065e420aed3c71b61af7e82c7b6bc1c592f7e3c7a66e9f78dd178699da4fe"},
{file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ea3f5bc91d7d457da7d48c7a732beaf79d0c8131df3ab278e6bba6297e23c6c4"},
{file = "coverage-7.2.3-cp39-cp39-win32.whl", hash = "sha256:ae7863a1d8db6a014b6f2ff9c1582ab1aad55a6d25bac19710a8df68921b6e30"},
{file = "coverage-7.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:3f04becd4fcda03c0160d0da9c8f0c246bc78f2f7af0feea1ec0930e7c93fa4a"},
{file = "coverage-7.2.3-pp37.pp38.pp39-none-any.whl", hash = "sha256:965ee3e782c7892befc25575fa171b521d33798132692df428a09efacaffe8d0"},
{file = "coverage-7.2.3.tar.gz", hash = "sha256:d298c2815fa4891edd9abe5ad6e6cb4207104c7dd9fd13aea3fdebf6f9b91259"},
]
[package.dependencies]
@@ -823,7 +816,6 @@ files = [
]
[package.dependencies]
typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
zipp = ">=0.5"
[package.extras]
@@ -845,18 +837,18 @@ files = [
[[package]]
name = "isort"
version = "5.11.5"
version = "5.12.0"
description = "A Python utility / library to sort Python imports."
category = "dev"
optional = false
python-versions = ">=3.7.0"
python-versions = ">=3.8.0"
files = [
{file = "isort-5.11.5-py3-none-any.whl", hash = "sha256:ba1d72fb2595a01c7895a5128f9585a5cc4b6d395f1c8d514989b9a7eb2a8746"},
{file = "isort-5.11.5.tar.gz", hash = "sha256:6be1f76a507cb2ecf16c7cf14a37e41609ca082330be4e3436a18ef74add55db"},
{file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"},
{file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"},
]
[package.extras]
colors = ["colorama (>=0.4.3,<0.5.0)"]
colors = ["colorama (>=0.4.3)"]
pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"]
plugins = ["setuptools"]
requirements-deprecated-finder = ["pip-api", "pipreqs"]
@@ -940,7 +932,6 @@ files = [
[package.dependencies]
bech32 = "*"
pydantic = "*"
typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
[[package]]
name = "loguru"
@@ -1107,7 +1098,6 @@ files = [
[package.dependencies]
mypy-extensions = ">=0.4.3"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""}
typing-extensions = ">=3.10"
[package.extras]
@@ -1171,34 +1161,31 @@ six = "*"
[[package]]
name = "pathspec"
version = "0.11.0"
version = "0.11.1"
description = "Utility library for gitignore style pattern matching of file paths."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "pathspec-0.11.0-py3-none-any.whl", hash = "sha256:3a66eb970cbac598f9e5ccb5b2cf58930cd8e3ed86d393d541eaf2d8b1705229"},
{file = "pathspec-0.11.0.tar.gz", hash = "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc"},
{file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"},
{file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"},
]
[[package]]
name = "platformdirs"
version = "2.6.2"
version = "3.2.0"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "platformdirs-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"},
{file = "platformdirs-2.6.2.tar.gz", hash = "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"},
{file = "platformdirs-3.2.0-py3-none-any.whl", hash = "sha256:ebe11c0d7a805086e99506aa331612429a72ca7cd52a1f0d277dc4adc20cb10e"},
{file = "platformdirs-3.2.0.tar.gz", hash = "sha256:d5b638ca397f25f979350ff789db335903d7ea010ab28903f57b27e1b16c2b08"},
]
[package.dependencies]
typing-extensions = {version = ">=4.4", markers = "python_version < \"3.8\""}
[package.extras]
docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
[[package]]
name = "pluggy"
@@ -1212,9 +1199,6 @@ files = [
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
]
[package.dependencies]
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
@@ -1416,18 +1400,18 @@ files = [
[[package]]
name = "pylint"
version = "2.16.1"
version = "2.17.2"
description = "python code static checker"
category = "dev"
optional = false
python-versions = ">=3.7.2"
files = [
{file = "pylint-2.16.1-py3-none-any.whl", hash = "sha256:bad9d7c36037f6043a1e848a43004dfd5ea5ceb05815d713ba56ca4503a9fe37"},
{file = "pylint-2.16.1.tar.gz", hash = "sha256:ffe7fa536bb38ba35006a7c8a6d2efbfdd3d95bbf21199cad31f76b1c50aaf30"},
{file = "pylint-2.17.2-py3-none-any.whl", hash = "sha256:001cc91366a7df2970941d7e6bbefcbf98694e00102c1f121c531a814ddc2ea8"},
{file = "pylint-2.17.2.tar.gz", hash = "sha256:1b647da5249e7c279118f657ca28b6aaebb299f86bf92affc632acf199f7adbb"},
]
[package.dependencies]
astroid = ">=2.14.1,<=2.16.0-dev0"
astroid = ">=2.15.2,<=2.17.0-dev0"
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
dill = [
{version = ">=0.2", markers = "python_version < \"3.11\""},
@@ -1551,7 +1535,6 @@ files = [
attrs = ">=19.2.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<2.0"
@@ -1574,7 +1557,6 @@ files = [
[package.dependencies]
pytest = ">=6.1.0"
typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""}
[package.extras]
testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"]
@@ -1894,48 +1876,14 @@ files = [
[[package]]
name = "tomlkit"
version = "0.11.6"
version = "0.11.7"
description = "Style preserving TOML library"
category = "dev"
optional = false
python-versions = ">=3.6"
python-versions = ">=3.7"
files = [
{file = "tomlkit-0.11.6-py3-none-any.whl", hash = "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b"},
{file = "tomlkit-0.11.6.tar.gz", hash = "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"},
]
[[package]]
name = "typed-ast"
version = "1.5.4"
description = "a fork of Python 2 and 3 ast modules with type comment support"
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
{file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"},
{file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"},
{file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"},
{file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"},
{file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"},
{file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"},
{file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"},
{file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"},
{file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"},
{file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"},
{file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"},
{file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"},
{file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"},
{file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"},
{file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"},
{file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"},
{file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"},
{file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"},
{file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"},
{file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"},
{file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"},
{file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"},
{file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"},
{file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"},
{file = "tomlkit-0.11.7-py3-none-any.whl", hash = "sha256:5325463a7da2ef0c6bbfefb62a3dc883aebe679984709aee32a317907d0a8d3c"},
{file = "tomlkit-0.11.7.tar.gz", hash = "sha256:f392ef70ad87a672f02519f99967d28a4d3047133e2d1df936511465fbb3791d"},
]
[[package]]
@@ -1994,7 +1942,6 @@ files = [
[package.dependencies]
click = ">=7.0"
h11 = ">=0.8"
typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
[package.extras]
standard = ["colorama (>=0.4)", "httptools (>=0.4.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.0)"]
@@ -2114,76 +2061,87 @@ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"]
[[package]]
name = "wrapt"
version = "1.14.1"
version = "1.15.0"
description = "Module for decorators, wrappers and monkey patching."
category = "dev"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
files = [
{file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"},
{file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"},
{file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"},
{file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"},
{file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"},
{file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"},
{file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"},
{file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"},
{file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"},
{file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"},
{file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"},
{file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"},
{file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"},
{file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"},
{file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"},
{file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"},
{file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"},
{file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"},
{file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"},
{file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"},
{file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"},
{file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"},
{file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"},
{file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"},
{file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"},
{file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"},
{file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"},
{file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"},
{file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"},
{file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"},
{file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"},
{file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"},
{file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"},
{file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"},
{file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"},
{file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"},
{file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"},
{file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"},
{file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"},
{file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"},
{file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"},
{file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"},
{file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"},
{file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"},
{file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"},
{file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"},
{file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"},
{file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"},
{file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"},
{file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"},
{file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"},
{file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"},
{file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"},
{file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"},
{file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"},
{file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"},
{file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"},
{file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"},
{file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"},
{file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"},
{file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"},
{file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"},
{file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"},
{file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"},
{file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"},
{file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"},
{file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"},
{file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46"},
{file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c"},
{file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09"},
{file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079"},
{file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e"},
{file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a"},
{file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"},
{file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"},
{file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"},
{file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"},
{file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"},
{file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"},
{file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"},
{file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"},
{file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"},
{file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"},
{file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"},
{file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"},
{file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"},
{file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"},
{file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"},
{file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"},
{file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"},
{file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"},
{file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"},
{file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"},
{file = "wrapt-1.15.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29"},
{file = "wrapt-1.15.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd"},
{file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb"},
{file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248"},
{file = "wrapt-1.15.0-cp35-cp35m-win32.whl", hash = "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559"},
{file = "wrapt-1.15.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"},
{file = "wrapt-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba"},
{file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752"},
{file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364"},
{file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475"},
{file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8"},
{file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418"},
{file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2"},
{file = "wrapt-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1"},
{file = "wrapt-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420"},
{file = "wrapt-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317"},
{file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e"},
{file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e"},
{file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0"},
{file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019"},
{file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034"},
{file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653"},
{file = "wrapt-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0"},
{file = "wrapt-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e"},
{file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"},
{file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"},
{file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"},
{file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"},
{file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f"},
{file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"},
{file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"},
{file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"},
{file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"},
{file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"},
{file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"},
{file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"},
{file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"},
{file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"},
{file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"},
{file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"},
{file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"},
{file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"},
{file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"},
{file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"},
{file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"},
{file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"},
]
[[package]]
@@ -2204,5 +2162,5 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
[metadata]
lock-version = "2.0"
python-versions = "^3.10 | ^3.9 | ^3.8 | ^3.7"
content-hash = "3ede9f1c735533b865009b69fefba2faefc3e4a836b4453af34c81dec29e0682"
python-versions = "^3.10 | ^3.9 | ^3.8.1"
content-hash = "d8cae4bcda9fcc8db58c08e1027d1e735dfe87e77584d7763348a6183c0fbad8"
+9 -12
View File
@@ -9,7 +9,7 @@ generate-setup-file = false
script = "tools/build.py"
[tool.poetry.dependencies]
python = "^3.10 | ^3.9 | ^3.8 | ^3.7"
python = "^3.10 | ^3.9 | ^3.8.1"
bech32 = "1.2.0"
bitstring = "3.1.9"
click = "8.0.4"
@@ -40,8 +40,8 @@ pyln-client = "0.11.1"
boltz-client = "0.1.3"
cashu = "0.9.0"
[tool.poetry.dev-dependencies]
[tool.poetry.group.dev.dependencies]
flake8 = "^6.0.0"
isort = "^5.10.1"
pytest = "^7.1.2"
mock = "^4.0.3"
@@ -50,8 +50,7 @@ pytest-asyncio = "^0.19.0"
pytest-cov = "^3.0.0"
mypy = "^0.971"
types-protobuf = "^3.19.22"
flake8 = { version = "^6.0.0", python = ">=3.8.1" }
pylint = { version = "^2.15.10", python = ">=3.7.2" }
pylint = "^2.17.2"
[build-system]
requires = ["poetry-core>=1.0.0", "pyscss"]
@@ -69,18 +68,15 @@ include = [
]
exclude = [
"lnbits/wallets/lnd_grpc_files",
"lnbits/wallets",
"lnbits/core",
"lnbits/*.py",
"lnbits/extensions",
]
[tool.mypy]
files = "lnbits"
exclude = """(?x)(
^lnbits/wallets/lnd_grpc_files.
| ^lnbits/extensions.
)"""
exclude = [
"^lnbits/wallets/lnd_grpc_files",
"^lnbits/extensions",
]
[[tool.mypy.overrides]]
module = [
@@ -134,6 +130,7 @@ enable = [
[tool.pylint.MASTER]
ignore-paths = [
"^lnbits/extensions/.*$",
"^lnbits/wallets/lnd_grpc_files/.*$",
]
fail-under = 10.0
-76
View File
@@ -1,76 +0,0 @@
anyio==3.6.2 ; python_version >= "3.7" and python_version < "4.0"
asn1crypto==1.5.1 ; python_version >= "3.7" and python_version < "4.0"
async-timeout==4.0.2 ; python_version >= "3.7" and python_version < "4.0"
attrs==22.2.0 ; python_version >= "3.7" and python_version < "4.0"
base58==2.1.1 ; python_version >= "3.7" and python_version < "4.0"
bech32==1.2.0 ; python_version >= "3.7" and python_version < "4.0"
bitstring==3.1.9 ; python_version >= "3.7" and python_version < "4.0"
boltz-client==0.1.3 ; python_version >= "3.7" and python_version < "4.0"
cashu==0.9.0 ; python_version >= "3.7" and python_version < "4.0"
cerberus==1.3.4 ; python_version >= "3.7" and python_version < "4.0"
certifi==2022.12.7 ; python_version >= "3.7" and python_version < "4.0"
cffi==1.15.1 ; python_version >= "3.7" and python_version < "4.0"
charset-normalizer==2.0.12 ; python_version >= "3.7" and python_version < "4.0"
click==8.0.4 ; python_version >= "3.7" and python_version < "4.0"
coincurve==17.0.0 ; python_version >= "3.7" and python_version < "4.0"
colorama==0.4.6 ; python_version >= "3.7" and python_version < "4.0" and platform_system == "Windows" or python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32"
cryptography==36.0.2 ; python_version >= "3.7" and python_version < "4.0"
ecdsa==0.18.0 ; python_version >= "3.7" and python_version < "4.0"
embit==0.4.9 ; python_version >= "3.7" and python_version < "4.0"
enum34==1.1.10 ; python_version >= "3.7" and python_version < "4.0"
environs==9.5.0 ; python_version >= "3.7" and python_version < "4.0"
exceptiongroup==1.1.0 ; python_version >= "3.7" and python_version < "3.11"
fastapi==0.83.0 ; python_version >= "3.7" and python_version < "4.0"
grpcio==1.51.1 ; python_version >= "3.7" and python_version < "4.0"
h11==0.12.0 ; python_version >= "3.7" and python_version < "4.0"
httpcore==0.15.0 ; python_version >= "3.7" and python_version < "4.0"
httpx==0.23.0 ; python_version >= "3.7" and python_version < "4.0"
idna==3.4 ; python_version >= "3.7" and python_version < "4.0"
importlib-metadata==5.2.0 ; python_version >= "3.7" and python_version < "4.0"
iniconfig==2.0.0 ; python_version >= "3.7" and python_version < "4.0"
jinja2==3.0.1 ; python_version >= "3.7" and python_version < "4.0"
lnurl==0.3.6 ; python_version >= "3.7" and python_version < "4.0"
loguru==0.6.0 ; python_version >= "3.7" and python_version < "4.0"
markupsafe==2.1.2 ; python_version >= "3.7" and python_version < "4.0"
marshmallow==3.19.0 ; python_version >= "3.7" and python_version < "4.0"
outcome==1.2.0 ; python_version >= "3.7" and python_version < "4.0"
packaging==23.0 ; python_version >= "3.7" and python_version < "4.0"
pathlib2==2.3.7.post1 ; python_version >= "3.7" and python_version < "4.0"
pluggy==1.0.0 ; python_version >= "3.7" and python_version < "4.0"
protobuf==4.21.12 ; python_version >= "3.7" and python_version < "4.0"
psycopg2-binary==2.9.1 ; python_version >= "3.7" and python_version < "4.0"
pycparser==2.21 ; python_version >= "3.7" and python_version < "4.0"
pycryptodomex==3.16.0 ; python_version >= "3.7" and python_version < "4.0"
pydantic==1.10.4 ; python_version >= "3.7" and python_version < "4.0"
pyln-bolt7==1.0.246 ; python_version >= "3.7" and python_version < "4.0"
pyln-client==0.11.1 ; python_version >= "3.7" and python_version < "4.0"
pyln-proto==0.11.1 ; python_version >= "3.7" and python_version < "4.0"
pyqrcode==1.2.1 ; python_version >= "3.7" and python_version < "4.0"
pyscss==1.4.0 ; python_version >= "3.7" and python_version < "4.0"
pysocks==1.7.1 ; python_version >= "3.7" and python_version < "4.0"
pytest-asyncio==0.19.0 ; python_version >= "3.7" and python_version < "4.0"
pytest==7.2.1 ; python_version >= "3.7" and python_version < "4.0"
python-bitcoinlib==0.11.2 ; python_version >= "3.7" and python_version < "4.0"
python-dotenv==0.21.0 ; python_version >= "3.7" and python_version < "4.0"
represent==1.6.0.post0 ; python_version >= "3.7" and python_version < "4.0"
requests==2.27.1 ; python_version >= "3.7" and python_version < "4.0"
rfc3986[idna2008]==1.5.0 ; python_version >= "3.7" and python_version < "4.0"
secp256k1==0.14.0 ; python_version >= "3.7" and python_version < "4.0"
setuptools==65.7.0 ; python_version >= "3.7" and python_version < "4.0"
shortuuid==1.0.1 ; python_version >= "3.7" and python_version < "4.0"
six==1.16.0 ; python_version >= "3.7" and python_version < "4.0"
sniffio==1.3.0 ; python_version >= "3.7" and python_version < "4.0"
sqlalchemy-aio==0.17.0 ; python_version >= "3.7" and python_version < "4.0"
sqlalchemy==1.3.24 ; python_version >= "3.7" and python_version < "4.0"
sse-starlette==0.6.2 ; python_version >= "3.7" and python_version < "4.0"
starlette==0.19.1 ; python_version >= "3.7" and python_version < "4.0"
tomli==2.0.1 ; python_version >= "3.7" and python_version < "3.11"
typing-extensions==4.4.0 ; python_version >= "3.7" and python_version < "4.0"
urllib3==1.26.14 ; python_version >= "3.7" and python_version < "4.0"
uvicorn==0.18.3 ; python_version >= "3.7" and python_version < "4.0"
uvloop==0.16.0 ; python_version >= "3.7" and python_version < "4.0"
websocket-client==1.3.3 ; python_version >= "3.7" and python_version < "4.0"
websockets==10.0 ; python_version >= "3.7" and python_version < "4.0"
wheel==0.38.4 ; python_version >= "3.7" and python_version < "4.0"
win32-setctime==1.1.0 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32"
zipp==3.11.0 ; python_version >= "3.7" and python_version < "4.0"
-28
View File
@@ -1,28 +0,0 @@
[flake8]
max-line-length = 300
exclude = lnbits/wallets/lnd_grpc_files/
ignore =
# E203 whitespace before ':'
E203,
# E221: multiple spaces before operator
E221,
# E241: multiple spaces after ':'
E241,
# E402: module level import not at top of file
E402,
# E501: line too long
E501,
# E741 ambiguous variable name
E741,
# W503: line break before binary operator
W503,
# F821: undefined name - should be addressed in future PR
F821,
# E265 block comment should start with '# ' - should be addressed in future PR
E265,
# E266 too many leading '#' for block comment - should be addressed in future PR
E266,
# E722 do not use bare 'except' - should be addressed in future PR
E722,
# flake8-requirements import checks
I
+3 -4
View File
@@ -6,7 +6,7 @@ from lnbits import bolt11
from lnbits.core.views.api import api_payment
from lnbits.settings import get_wallet_class
from ...helpers import get_random_invoice_data, is_fake, is_regtest
from ...helpers import get_random_invoice_data, is_fake
WALLET = get_wallet_class()
@@ -170,16 +170,15 @@ async def test_pay_invoice_invoicekey(client, invoice, inkey_headers_from):
assert response.status_code >= 300 # should fail
# check POST /api/v1/payments: payment with admin key [should pass]
# check POST /api/v1/payments: payment with admin key, trying to pay twice [should fail]
@pytest.mark.asyncio
@pytest.mark.skipif(is_regtest, reason="this only works in fakewallet")
async def test_pay_invoice_adminkey(client, invoice, adminkey_headers_from):
data = {"out": True, "bolt11": invoice["payment_request"]}
# try payment with admin key
response = await client.post(
"/api/v1/payments", json=data, headers=adminkey_headers_from
)
assert response.status_code < 300 # should pass
assert response.status_code > 300 # should fail
# check POST /api/v1/payments/decode
+23 -51
View File
@@ -4,27 +4,22 @@ import pytest
@pytest.mark.asyncio
async def test_core_views_generic(client):
response = await client.get("/")
assert response.status_code == 200, (
str(response.url) + " " + str(response.status_code)
)
assert response.status_code == 200, f"{response.url} {response.status_code}"
# check GET /wallet: wallet info
@pytest.mark.asyncio
async def test_get_wallet(client):
response = await client.get("wallet")
assert response.status_code == 307, ( # redirect not modified
str(response.url) + " " + str(response.status_code)
)
# redirect not modified
assert response.status_code == 307, f"{response.url} {response.status_code}"
# check GET /wallet: do not allow redirects, expect code 307
@pytest.mark.asyncio
async def test_get_wallet_no_redirect(client):
response = await client.get("wallet", follow_redirects=False)
assert response.status_code == 307, (
str(response.url) + " " + str(response.status_code)
)
assert response.status_code == 307, f"{response.url} {response.status_code}"
# determine the next redirect location
request = client.build_request("GET", "wallet")
@@ -33,13 +28,11 @@ async def test_get_wallet_no_redirect(client):
response = await client.send(request)
request = response.next_request
if i == 0:
assert response.status_code == 307, ( # first redirect
str(response.url) + " " + str(response.status_code)
)
# first redirect
assert response.status_code == 307, f"{response.url} {response.status_code}"
elif i == 1:
assert response.status_code == 200, ( # then get the actual page
str(response.url) + " " + str(response.status_code)
)
# then get the actual page
assert response.status_code == 200, f"{response.url} {response.status_code}"
i += 1
@@ -47,18 +40,14 @@ async def test_get_wallet_no_redirect(client):
@pytest.mark.asyncio
async def test_get_wallet_with_nonexistent_user(client):
response = await client.get("wallet", params={"usr": "1"})
assert response.status_code == 400, (
str(response.url) + " " + str(response.status_code)
)
assert response.status_code == 400, f"{response.url} {response.status_code}"
# check GET /wallet: with user
@pytest.mark.asyncio
async def test_get_wallet_with_user(client, to_user):
response = await client.get("wallet", params={"usr": to_user.id})
assert response.status_code == 307, (
str(response.url) + " " + str(response.status_code)
)
assert response.status_code == 307, f"{response.url} {response.status_code}"
# determine the next redirect location
request = client.build_request("GET", "wallet", params={"usr": to_user.id})
@@ -67,13 +56,11 @@ async def test_get_wallet_with_user(client, to_user):
response = await client.send(request)
request = response.next_request
if i == 0:
assert response.status_code == 307, ( # first redirect
str(response.url) + " " + str(response.status_code)
)
# first redirect
assert response.status_code == 307, f"{response.url} {response.status_code}"
elif i == 1:
assert response.status_code == 200, ( # then get the actual page
str(response.url) + " " + str(response.status_code)
)
# then get the actual page
assert response.status_code == 200, f"{response.url} {response.status_code}"
i += 1
@@ -83,45 +70,36 @@ async def test_get_wallet_with_user_and_wallet(client, to_user, to_wallet):
response = await client.get(
"wallet", params={"usr": to_user.id, "wal": to_wallet.id}
)
assert response.status_code == 200, (
str(response.url) + " " + str(response.status_code)
)
assert response.status_code == 200, f"{response.url} {response.status_code}"
# check GET /wallet: wrong wallet and user, expect 400
@pytest.mark.asyncio
async def test_get_wallet_with_user_and_wrong_wallet(client, to_user):
response = await client.get("wallet", params={"usr": to_user.id, "wal": "1"})
assert response.status_code == 400, (
str(response.url) + " " + str(response.status_code)
)
assert response.status_code == 400, f"{response.url} {response.status_code}"
# check GET /extensions: extensions list
@pytest.mark.asyncio
async def test_get_extensions(client, to_user):
response = await client.get("extensions", params={"usr": to_user.id})
assert response.status_code == 200, (
str(response.url) + " " + str(response.status_code)
)
assert response.status_code == 200, f"{response.url} {response.status_code}"
# check GET /extensions: extensions list wrong user, expect 400
@pytest.mark.asyncio
async def test_get_extensions_wrong_user(client, to_user):
response = await client.get("extensions", params={"usr": "1"})
assert response.status_code == 400, (
str(response.url) + " " + str(response.status_code)
)
assert response.status_code == 400, f"{response.url} {response.status_code}"
# check GET /extensions: no user given, expect code 400 bad request
@pytest.mark.asyncio
async def test_get_extensions_no_user(client):
response = await client.get("extensions")
assert response.status_code == 400, ( # bad request
str(response.url) + " " + str(response.status_code)
)
# bad request
assert response.status_code == 400, f"{response.url} {response.status_code}"
# check GET /extensions: enable extension
@@ -131,9 +109,7 @@ async def test_get_extensions_no_user(client):
# response = await client.get(
# "extensions", params={"usr": to_user.id, "enable": "lnurlp"}
# )
# assert response.status_code == 200, (
# str(response.url) + " " + str(response.status_code)
# )
# assert response.status_code == 200, f"{response.url} {response.status_code}"
# check GET /extensions: enable and disable extensions, expect code 400 bad request
@@ -143,9 +119,7 @@ async def test_get_extensions_no_user(client):
# "extensions",
# params={"usr": to_user.id, "enable": "lnurlp", "disable": "lnurlp"},
# )
# assert response.status_code == 400, (
# str(response.url) + " " + str(response.status_code)
# )
# assert response.status_code == 400, f"{response.url} {response.status_code}"
# check GET /extensions: enable nonexistent extension, expect code 400 bad request
@@ -154,6 +128,4 @@ async def test_get_extensions_enable_nonexistent_extension(client, to_user):
response = await client.get(
"extensions", params={"usr": to_user.id, "enable": "12341234"}
)
assert response.status_code == 400, (
str(response.url) + " " + str(response.status_code)
)
assert response.status_code == 400, f"{response.url} {response.status_code}"
+27 -67
View File
@@ -1,74 +1,34 @@
import glob
import os
import warnings
from pathlib import Path
from typing import List
LNBITS_PATH = Path("lnbits").absolute()
# from ..lnbits.helpers import vendored_js, vendored_css
vendored_js = [
"/static/vendor/moment.js",
"/static/vendor/underscore.js",
"/static/vendor/axios.js",
"/static/vendor/vue.js",
"/static/vendor/vue-router.js",
"/static/vendor/vue-qrcode-reader.browser.js",
"/static/vendor/vue-qrcode.js",
"/static/vendor/vue-i18n.js",
"/static/vendor/vuex.js",
"/static/vendor/quasar.ie.polyfills.umd.min.js",
"/static/vendor/quasar.umd.js",
"/static/vendor/Chart.bundle.js",
]
def get_js_vendored(prefer_minified: bool = False) -> List[str]:
paths = get_vendored(".js", prefer_minified)
def sorter(key: str):
if "moment@" in key:
return 1
if "vue@" in key:
return 2
if "vue-router@" in key:
return 3
if "polyfills" in key:
return 4
return 9
return sorted(paths, key=sorter)
def get_css_vendored(prefer_minified: bool = False) -> List[str]:
paths = get_vendored(".css", prefer_minified)
def sorter(key: str):
if "quasar@" in key:
return 1
if "vue@" in key:
return 2
if "chart.js@" in key:
return 100
return 9
return sorted(paths, key=sorter)
def get_vendored(ext: str, prefer_minified: bool = False) -> List[str]:
paths: List[str] = []
for path in glob.glob(
os.path.join(LNBITS_PATH, "static/vendor/**"), recursive=True
):
if path.endswith(".min" + ext):
# path is minified
unminified = path.replace(".min" + ext, ext)
if prefer_minified:
paths.append(path)
if unminified in paths:
paths.remove(unminified)
elif unminified not in paths:
paths.append(path)
elif path.endswith(ext):
# path is not minified
minified = path.replace(ext, ".min" + ext)
if not prefer_minified:
paths.append(path)
if minified in paths:
paths.remove(minified)
elif minified not in paths:
paths.append(path)
return sorted(paths)
vendored_css = [
"/static/vendor/quasar.css",
"/static/vendor/Chart.css",
"/static/vendor/vue-qrcode-reader.css",
]
def url_for_vendored(abspath: str) -> str:
return "/" + os.path.relpath(abspath, LNBITS_PATH)
return f"/{os.path.relpath(abspath, LNBITS_PATH)}"
def transpile_scss():
@@ -82,14 +42,14 @@ def transpile_scss():
def bundle_vendored():
for getfiles, outputpath in [
(get_js_vendored, os.path.join(LNBITS_PATH, "static/bundle.js")),
(get_css_vendored, os.path.join(LNBITS_PATH, "static/bundle.css")),
for files, outputpath in [
(vendored_js, os.path.join(LNBITS_PATH, "static/bundle.js")),
(vendored_css, os.path.join(LNBITS_PATH, "static/bundle.css")),
]:
output = ""
for path in getfiles():
with open(path) as f:
output += "/* " + url_for_vendored(path) + " */\n" + f.read() + ";\n"
for path in files:
with open(f"{LNBITS_PATH}{path}") as f:
output += f.read() + ";\n"
with open(outputpath, "w") as f:
f.write(output)