Merge branch 'main' into matthewcroughan/nixify

This commit is contained in:
ben 2022-07-23 20:09:24 +01:00
commit 244d6f23bf
114 changed files with 1720 additions and 988 deletions

View File

@ -1,6 +1,21 @@
.git .git
docs data
docker docker
docs
tests
venv
tools
# ignore all the markdown
*.md *.md
*.log
.env
.gitignore
.prettierrc
LICENSE
Makefile
mypy.ini
package-lock.json
package.json
pytest.ini

View File

@ -1,16 +0,0 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_size = 2
indent_style = space
[*.md]
trim_trailing_whitespace = false
[*.py]
indent_size = 4
indent_style = space

View File

@ -1,7 +1,7 @@
HOST=127.0.0.1 HOST=127.0.0.1
PORT=5000 PORT=5000
DEBUG=true DEBUG=false
LNBITS_ALLOWED_USERS="" LNBITS_ALLOWED_USERS=""
LNBITS_ADMIN_USERS="" LNBITS_ADMIN_USERS=""

2
.github/FUNDING.yml vendored
View File

@ -1 +1 @@
custom: https://lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK custom: https://legend.lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK

View File

@ -15,6 +15,16 @@ jobs:
- run: python3 -m venv venv - run: python3 -m venv venv
- run: ./venv/bin/pip install black - run: ./venv/bin/pip install black
- run: make checkblack - run: make checkblack
isort:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: sudo apt-get install python3-venv
- run: python3 -m venv venv
- run: ./venv/bin/pip install isort
- run: make checkisort
prettier: prettier:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:

49
.github/workflows/migrations.yml vendored Normal file
View File

@ -0,0 +1,49 @@
name: migrations
on: [pull_request]
jobs:
sqlite-to-postgres:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:latest
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports:
# maps tcp port 5432 on service container to the host
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
strategy:
matrix:
python-version: [3.8]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
env:
VIRTUAL_ENV: ./venv
PATH: ${{ env.VIRTUAL_ENV }}/bin:${{ env.PATH }}
run: |
python -m venv ${{ env.VIRTUAL_ENV }}
./venv/bin/python -m pip install --upgrade pip
./venv/bin/pip install -r requirements.txt
./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock
- name: Run migrations
run: |
rm -rf ./data
mkdir -p ./data
export LNBITS_DATA_FOLDER="./data"
timeout 5s ./venv/bin/uvicorn lnbits.__main__:app --host 0.0.0.0 --port 5001 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi
export LNBITS_DATABASE_URL="postgres://postgres:postgres@0.0.0.0:5432/postgres"
timeout 5s ./venv/bin/uvicorn lnbits.__main__:app --host 0.0.0.0 --port 5001 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi
./venv/bin/python tools/conv.py --dont-ignore-missing

97
.github/workflows/regtest.yml vendored Normal file
View File

@ -0,0 +1,97 @@
name: regtest
on: [push, pull_request]
jobs:
LndRestWallet:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.8]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Setup Regtest
run: |
docker build -t lnbits-legend .
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
cd docker
source docker-scripts.sh
lnbits-regtest-start
echo "sleeping 60 seconds"
sleep 60
echo "continue"
lnbits-regtest-init
bitcoin-cli-sim -generate 1
lncli-sim 1 listpeers
sudo chmod -R a+rwx .
- 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 pylightning
./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock
- name: Run tests
env:
PYTHONUNBUFFERED: 1
PORT: 5123
LNBITS_DATA_FOLDER: ./data
LNBITS_BACKEND_WALLET_CLASS: LndRestWallet
LND_REST_ENDPOINT: https://localhost:8081/
LND_REST_CERT: docker/data/lnd-1/tls.cert
LND_REST_MACAROON: docker/data/lnd-1/data/chain/bitcoin/regtest/admin.macaroon
run: |
sudo chmod -R a+rwx . && rm -rf ./data && mkdir -p ./data
make test-real-wallet
CLightningWallet:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.8]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Setup Regtest
run: |
docker build -t lnbits-legend .
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
cd docker
source docker-scripts.sh
lnbits-regtest-start
echo "sleeping 60 seconds"
sleep 60
echo "continue"
lnbits-regtest-init
bitcoin-cli-sim -generate 1
lncli-sim 1 listpeers
sudo chmod -R a+rwx .
- 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 pylightning
./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock
- name: Run tests
env:
PYTHONUNBUFFERED: 1
PORT: 5123
LNBITS_DATA_FOLDER: ./data
LNBITS_BACKEND_WALLET_CLASS: CLightningWallet
CLIGHTNING_RPC: docker/data/clightning-1/regtest/lightning-rpc
run: |
sudo chmod -R a+rwx . && rm -rf ./data && mkdir -p ./data
make test-real-wallet

View File

@ -3,19 +3,17 @@ name: tests
on: [push, pull_request] on: [push, pull_request]
jobs: jobs:
sqlite: venv-sqlite:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: [3.7, 3.8] python-version: [3.7, 3.8, 3.9]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: psycopg2 prerequisites
run: sudo apt-get install python-dev libpq-dev
- name: Install dependencies - name: Install dependencies
env: env:
VIRTUAL_ENV: ./venv VIRTUAL_ENV: ./venv
@ -27,7 +25,7 @@ jobs:
./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock ./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock
- name: Run tests - name: Run tests
run: make test run: make test
postgres: venv-postgres:
runs-on: ubuntu-latest runs-on: ubuntu-latest
services: services:
postgres: postgres:
@ -46,15 +44,13 @@ jobs:
--health-retries 5 --health-retries 5
strategy: strategy:
matrix: matrix:
python-version: [3.7, 3.8] python-version: [3.8]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: psycopg2 prerequisites
run: sudo apt-get install python-dev libpq-dev
- name: Install dependencies - name: Install dependencies
env: env:
VIRTUAL_ENV: ./venv VIRTUAL_ENV: ./venv
@ -72,34 +68,21 @@ jobs:
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v3
with: with:
file: ./coverage.xml file: ./coverage.xml
# build: pipenv-sqlite:
# runs-on: ubuntu-latest runs-on: ubuntu-latest
# strategy: strategy:
# matrix: matrix:
# python-version: [3.7, 3.8] python-version: [3.7, 3.8, 3.9]
# steps: steps:
# - uses: actions/checkout@v2 - uses: actions/checkout@v2
# - name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
# uses: actions/setup-python@v1 uses: actions/setup-python@v2
# with: with:
# python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
# - name: Install dependencies - name: Install dependencies
# run: | run: |
# python -m pip install --upgrade pip pip install pipenv
# pip install -r requirements.txt pipenv install --dev
# - name: Test with pytest pipenv install importlib-metadata
# env: - name: Run tests
# LNBITS_BACKEND_WALLET_CLASS: LNPayWallet run: make test-pipenv
# LNBITS_FORCE_HTTPS: 0
# LNPAY_API_ENDPOINT: https://api.lnpay.co/v1/
# LNPAY_API_KEY: sak_gG5pSFZhFgOLHm26a8hcWvXKt98yd
# LNPAY_ADMIN_KEY: waka_HqWfOoNE0TPqmQHSYErbF4n9
# LNPAY_INVOICE_KEY: waki_ZqFEbhrTyopuPlOZButZUw
# LNPAY_READ_KEY: wakr_6IyTaNrvSeu3jbojSWt4ou6h
# run: |
# pip install pytest pytest-cov
# pytest --cov=lnbits --cov-report=xml
# - name: Upload coverage to Codecov
# uses: codecov/codecov-action@v1
# with:
# file: ./coverage.xml

View File

@ -2,7 +2,7 @@
all: format check requirements.txt all: format check requirements.txt
format: prettier black format: prettier isort black
check: mypy checkprettier checkblack check: mypy checkprettier checkblack
@ -17,12 +17,18 @@ mypy: $(shell find lnbits -name "*.py")
./venv/bin/mypy lnbits/core ./venv/bin/mypy lnbits/core
./venv/bin/mypy lnbits/extensions/* ./venv/bin/mypy lnbits/extensions/*
isort: $(shell find lnbits -name "*.py")
./venv/bin/isort --profile black lnbits
checkprettier: $(shell find lnbits -name "*.js" -name ".html") checkprettier: $(shell find lnbits -name "*.js" -name ".html")
./node_modules/.bin/prettier --check lnbits/static/js/*.js lnbits/core/static/js/*.js lnbits/extensions/*/templates/*/*.html ./lnbits/core/templates/core/*.html lnbits/templates/*.html lnbits/extensions/*/static/js/*.js ./node_modules/.bin/prettier --check lnbits/static/js/*.js lnbits/core/static/js/*.js lnbits/extensions/*/templates/*/*.html ./lnbits/core/templates/core/*.html lnbits/templates/*.html lnbits/extensions/*/static/js/*.js
checkblack: $(shell find lnbits -name "*.py") checkblack: $(shell find lnbits -name "*.py")
./venv/bin/black --check lnbits ./venv/bin/black --check lnbits
checkisort: $(shell find lnbits -name "*.py")
./venv/bin/isort --profile black --check-only lnbits
Pipfile.lock: Pipfile Pipfile.lock: Pipfile
./venv/bin/pipenv lock ./venv/bin/pipenv lock
@ -32,10 +38,27 @@ requirements.txt: Pipfile.lock
test: test:
rm -rf ./tests/data rm -rf ./tests/data
mkdir -p ./tests/data mkdir -p ./tests/data
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
FAKE_WALLET_SECRET="ToTheMoon1" \ FAKE_WALLET_SECRET="ToTheMoon1" \
LNBITS_DATA_FOLDER="./tests/data" \ LNBITS_DATA_FOLDER="./tests/data" \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
./venv/bin/pytest --durations=1 -s --cov=lnbits --cov-report=xml ./venv/bin/pytest --durations=1 -s --cov=lnbits --cov-report=xml tests
test-real-wallet:
rm -rf ./tests/data
mkdir -p ./tests/data
LNBITS_DATA_FOLDER="./tests/data" \
PYTHONUNBUFFERED=1 \
./venv/bin/pytest --durations=1 -s --cov=lnbits --cov-report=xml tests
test-pipenv:
rm -rf ./tests/data
mkdir -p ./tests/data
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
FAKE_WALLET_SECRET="ToTheMoon1" \
LNBITS_DATA_FOLDER="./tests/data" \
PYTHONUNBUFFERED=1 \
pipenv run pytest --durations=1 -s --cov=lnbits --cov-report=xml tests
bak: bak:
# LNBITS_DATABASE_URL=postgres://postgres:postgres@0.0.0.0:5432/postgres # LNBITS_DATABASE_URL=postgres://postgres:postgres@0.0.0.0:5432/postgres

12
Pipfile
View File

@ -4,7 +4,7 @@ url = "https://pypi.org/simple"
verify_ssl = true verify_ssl = true
[requires] [requires]
python_version = "3.7" python_version = "3.8"
[packages] [packages]
bitstring = "*" bitstring = "*"
@ -28,13 +28,17 @@ asyncio = "*"
fastapi = "*" fastapi = "*"
uvicorn = {extras = ["standard"], version = "*"} uvicorn = {extras = ["standard"], version = "*"}
sse-starlette = "*" sse-starlette = "*"
jinja2 = "3.0.1" jinja2 = "==3.0.1"
pyngrok = "*" pyngrok = "*"
secp256k1 = "*" secp256k1 = "==0.14.0"
cffi = "==1.15.0"
pycryptodomex = "*" pycryptodomex = "*"
[dev-packages] [dev-packages]
black = "==20.8b1" black = "==20.8b1"
pytest = "*" pytest = "*"
pytest-cov = "*" pytest-cov = "*"
mypy = "latest" mypy = "*"
pytest-asyncio = "*"
requests = "*"
mock = "*"

1015
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1 +0,0 @@
web: hypercorn -k trio --bind 0.0.0.0:5000 'lnbits.app:create_app()'

View File

@ -13,7 +13,7 @@ LNbits
(LNbits is beta, for responsible disclosure of any concerns please contact lnbits@pm.me) (LNbits is beta, for responsible disclosure of any concerns please contact lnbits@pm.me)
Use [lnbits.com](https://lnbits.com), or run your own LNbits server! Use [legend.lnbits.com](https://legend.lnbits.com), or run your own LNbits server!
LNbits is a very simple Python server that sits on top of any funding source, and can be used as: LNbits is a very simple Python server that sits on top of any funding source, and can be used as:
@ -33,7 +33,7 @@ LNbits is inspired by all the great work of [opennode.com](https://www.opennode.
## Running LNbits ## Running LNbits
See the [install guide](docs/devs/installation.md) for details on installation and setup. See the [install guide](docs/guide/installation.md) for details on installation and setup.
## LNbits as an account system ## LNbits as an account system
@ -67,7 +67,7 @@ Wallets can be easily generated and given out to people at events (one click mul
## Tip us ## Tip us
If you like this project and might even use or extend it, why not [send some tip love](https://lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK)! If you like this project and might even use or extend it, why not [send some tip love](https://legend.lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK)!
[docs]: https://lnbits.org/ [docs]: https://lnbits.org/

View File

@ -1,7 +0,0 @@
{
"scripts": {
"dokku": {
"predeploy": "quart migrate && quart assets"
}
}
}

View File

@ -15,6 +15,7 @@ cp lnbits/extensions/example lnbits/extensions/mysuperplugin -r # Let's not use
cd lnbits/extensions/mysuperplugin cd lnbits/extensions/mysuperplugin
find . -type f -print0 | xargs -0 sed -i 's/example/mysuperplugin/g' # Change all occurrences of 'example' to your plugin name 'mysuperplugin'. find . -type f -print0 | xargs -0 sed -i 's/example/mysuperplugin/g' # Change all occurrences of 'example' to your plugin name 'mysuperplugin'.
``` ```
- if you are on macOS and having difficulty with 'sed', consider `brew install gnu-sed` and use 'gsed', without -0 option after xargs.
Going over the example extension's structure: Going over the example extension's structure:
* views_api.py: This is where your public API would go. It will be exposed at "$DOMAIN/$PLUGIN/$ROUTE". For example: https://lnbits.com/mysuperplugin/api/v1/tools. * views_api.py: This is where your public API would go. It will be exposed at "$DOMAIN/$PLUGIN/$ROUTE". For example: https://lnbits.com/mysuperplugin/api/v1/tools.

View File

@ -7,46 +7,10 @@ nav_order: 1
# Installation # Installation
LNbits uses [Pipenv][pipenv] to manage Python packages. This guide has been moved to the [installation guide](../guide/installation.md).
To install the developer packages, use `pipenv install --dev`.
```sh ## Notes:
git clone https://github.com/lnbits/lnbits-legend.git
cd lnbits-legend/
sudo apt-get install pipenv * We recommend using <a href="https://caddyserver.com/docs/install#debian-ubuntu-raspbian">Caddy</a> for a reverse-proxy if you want to serve your install through a domain, alternatively you can use [ngrok](https://ngrok.com/).
pipenv shell
# pipenv --python 3.9 shell (if you wish to use a version of Python higher than 3.7)
pipenv install --dev
# pipenv --python 3.9 install --dev (if you wish to use a version of Python higher than 3.7)
# If any of the modules fails to install, try checking and upgrading your setupTool module
# pip install -U setuptools
# install libffi/libpq in case "pipenv install" fails
# sudo apt-get install -y libffi-dev libpq-dev
```
## Running the server
Create the data folder and edit the .env file:
mkdir data
cp .env.example .env
sudo nano .env
To then run the server for development purposes (includes hot-reload), use:
pipenv run python -m uvicorn lnbits.__main__:app --host 0.0.0.0 --reload
For production, use:
pipenv run python -m uvicorn lnbits.__main__:app --host 0.0.0.0
You might also need to install additional packages, depending on the [backend wallet](../guide/wallets.md) you use.
E.g. when you want to use LND you have to `pipenv run pip install lndgrpc` and `pipenv run pip install purerpc`.
Take a look at [Polar][polar] for an excellent way of spinning up a Lightning Network dev environment.
**Notes**:
* We reccomend using <a href="https://caddyserver.com/docs/install#debian-ubuntu-raspbian">Caddy</a> for a reverse-proxy if you want to serve your install through a domain, alternatively you can use [ngrok](https://ngrok.com/).
* <a href="https://linuxize.com/post/how-to-use-linux-screen/#starting-linux-screen">Screen</a> works well if you want LNbits to continue running when you close your terminal session. * <a href="https://linuxize.com/post/how-to-use-linux-screen/#starting-linux-screen">Screen</a> works well if you want LNbits to continue running when you close your terminal session.

View File

@ -4,8 +4,88 @@ title: Basic installation
nav_order: 2 nav_order: 2
--- ---
# Basic installation # Basic installation
Install Postgres and setup a database for LNbits:
You can choose between two python package managers, `venv` and `pipenv`. Both are fine but if you don't know what you're doing, just go for the first option.
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).
## Option 1: pipenv
You can also use Pipenv to manage your python packages.
```sh
git clone https://github.com/lnbits/lnbits-legend.git
cd lnbits-legend/
sudo apt update && sudo apt install -y pipenv
pipenv install --dev
# pipenv --python 3.9 install --dev (if you wish to use a version of Python higher than 3.7)
pipenv shell
# pipenv --python 3.9 shell (if you wish to use a version of Python higher than 3.7)
# If any of the modules fails to install, try checking and upgrading your setupTool module
# pip install -U setuptools wheel
# install libffi/libpq in case "pipenv install" fails
# sudo apt-get install -y libffi-dev libpq-dev
mkdir data && cp .env.example .env
```
#### Running the server
```sh
pipenv run python -m uvicorn lnbits.__main__:app --port 5000 --host 0.0.0.0
```
Add the flag `--reload` for development (includes hot-reload).
## Option 2: venv
Download this repo and install the dependencies:
```sh
git clone https://github.com/lnbits/lnbits-legend.git
cd lnbits-legend/
# ensure you have virtualenv installed, on debian/ubuntu 'apt install python3-venv'
python3 -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
```
#### 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`.
### Troubleshooting
Problems installing? These commands have helped us install LNbits.
```sh
sudo apt install pkg-config libffi-dev libpq-dev
# if the secp256k1 build fails:
# if you used pipenv (option 1)
pipenv install setuptools wheel
# if you used venv (option 2)
./venv/bin/pip install setuptools wheel
# build essentials for debian/ubuntu
sudo apt install python3-dev gcc build-essential
```
### Optional: PostgreSQL database
If you want to use LNbits at scale, we recommend using PostgreSQL as the backend database. Install Postgres and setup a database for LNbits:
```sh ```sh
# on debian/ubuntu 'sudo apt-get -y install postgresql' # on debian/ubuntu 'sudo apt-get -y install postgresql'
@ -22,22 +102,17 @@ createdb lnbits
exit exit
``` ```
Download this repo and install the dependencies: You need to edit the `.env` file.
```sh ```sh
git clone https://github.com/lnbits/lnbits-legend.git
cd lnbits-legend/
# ensure you have virtualenv installed, on debian/ubuntu 'apt install python3-venv' should work
python3 -m venv venv
./venv/bin/pip install -r requirements.txt
cp .env.example .env
# add the database connection string to .env 'nano .env' LNBITS_DATABASE_URL= # add the database connection string to .env 'nano .env' LNBITS_DATABASE_URL=
# postgres://<user>:<myPassword>@<host>/<lnbits> - alter line bellow with your user, password and db name # postgres://<user>:<myPassword>@<host>/<lnbits> - alter line bellow with your user, password and db name
LNBITS_DATABASE_URL="postgres://postgres:postgres@localhost/lnbits" LNBITS_DATABASE_URL="postgres://postgres:postgres@localhost/lnbits"
# save and exit # save and exit
./venv/bin/uvicorn lnbits.__main__:app --port 5000
``` ```
# Using LNbits
Now you can visit your LNbits at http://localhost:5000/. Now you can visit your LNbits at http://localhost:5000/.
Now modify the `.env` file with any settings you prefer and add a proper [funding source](./wallets.md) by modifying the value of `LNBITS_BACKEND_WALLET_CLASS` and providing the extra information and credentials related to the chosen funding source. Now modify the `.env` file with any settings you prefer and add a proper [funding source](./wallets.md) by modifying the value of `LNBITS_BACKEND_WALLET_CLASS` and providing the extra information and credentials related to the chosen funding source.
@ -46,10 +121,16 @@ Then you can restart it and it will be using the new settings.
You might also need to install additional packages or perform additional setup steps, depending on the chosen backend. See [the short guide](./wallets.md) on each different funding source. You might also need to install additional packages or perform additional setup steps, depending on the chosen backend. See [the short guide](./wallets.md) on each different funding source.
## Important note Take a look at [Polar](https://lightningpolar.com/) for an excellent way of spinning up a Lightning Network dev environment.
If you already have LNbits installed and running, on an SQLite database, we **HIGHLY** recommend you migrate to postgres!
There's a script included that can do the migration easy. You should have Postgres already installed and there should be a password for the user, check the guide above. Additionally, your lnbits instance should run once on postgres to implement the database schema before the migration works:
# Additional guides
## SQLite to PostgreSQL migration
If you already have LNbits installed and running, on an SQLite database, we **highly** recommend you migrate to postgres if you are planning to run LNbits on scale.
There's a script included that can do the migration easy. You should have Postgres already installed and there should be a password for the user (see Postgres install guide above). Additionally, your LNbits instance should run once on postgres to implement the database schema before the migration works:
```sh ```sh
# STOP LNbits # STOP LNbits
@ -61,17 +142,14 @@ LNBITS_DATABASE_URL="postgres://postgres:postgres@localhost/lnbits"
# START LNbits # START LNbits
# STOP LNbits # STOP LNbits
# on the LNBits folder, locate and edit 'conv.py' with the relevant credentials # on the LNBits folder, locate and edit 'tools/conv.py' with the relevant credentials
python3 conv.py python3 tools/conv.py
``` ```
Hopefully, everything works and get migrated... Launch LNbits again and check if everything is working properly. Hopefully, everything works and get migrated... Launch LNbits again and check if everything is working properly.
## LNbits as a systemd service
# Additional guides
### LNbits as a systemd service
Systemd is great for taking care of your LNbits instance. It will start it on boot and restart it in case it crashes. If you want to run LNbits as a systemd service on your Debian/Ubuntu/Raspbian server, create a file at `/etc/systemd/system/lnbits.service` with the following content: Systemd is great for taking care of your LNbits instance. It will start it on boot and restart it in case it crashes. If you want to run LNbits as a systemd service on your Debian/Ubuntu/Raspbian server, create a file at `/etc/systemd/system/lnbits.service` with the following content:
@ -110,11 +188,40 @@ sudo systemctl enable lnbits.service
sudo systemctl start lnbits.service sudo systemctl start lnbits.service
``` ```
### LNbits running on Umbrel behind Tor ## 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.
We have to create a self-signed certificate using `mkcert`. Note that this certiciate is not "trusted" by most browsers but that's fine (since you know that you have created it) and encryption is always better than clear text.
#### Install mkcert
You can find the install instructions for `mkcert` [here](https://github.com/FiloSottile/mkcert).
Install mkcert on Ubuntu:
```sh
sudo apt install libnss3-tools
curl -JLO "https://dl.filippo.io/mkcert/latest?for=linux/amd64"
chmod +x mkcert-v*-linux-amd64
sudo cp mkcert-v*-linux-amd64 /usr/local/bin/mkcert
```
#### Create certificate
To create a certificate, first `cd` into your lnbits folder and execute the following command ([more info](https://kifarunix.com/how-to-create-self-signed-ssl-certificate-with-mkcert-on-ubuntu-18-04/))
```sh
# add your local IP (192.x.x.x) as well if you want to use it in your local network
mkcert localhost 127.0.0.1 ::1
```
This will create two new files (`localhost-key.pem` and `localhost.pem `) which you can then pass to uvicorn when you start LNbits:
```sh
./venv/bin/uvicorn lnbits.__main__:app --host 0.0.0.0 --port 5000 --ssl-keyfile ./localhost-key.pem --ssl-certfile ./localhost.pem
```
## LNbits running on Umbrel behind Tor
If you want to run LNbits on your Umbrel but want it to be reached through clearnet, _Uxellodunum_ made an extensive [guide](https://community.getumbrel.com/t/guide-lnbits-without-tor/604) on how to do it. If you want to run LNbits on your Umbrel but want it to be reached through clearnet, _Uxellodunum_ made an extensive [guide](https://community.getumbrel.com/t/guide-lnbits-without-tor/604) on how to do it.
### Docker installation ## Docker installation
To install using docker you first need to build the docker image as: To install using docker you first need to build the docker image as:
@ -146,9 +253,3 @@ docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/
``` ```
Finally you can access your lnbits on your machine at port 5000. Finally you can access your lnbits on your machine at port 5000.
# Additional guides
## LNbits running on Umbrel behind Tor
If you want to run LNbits on your Umbrel but want it to be reached through clearnet, _Uxellodunum_ made an extensive [guide](https://community.getumbrel.com/t/guide-lnbits-without-tor/604) on how to do it.

View File

@ -1,21 +1,19 @@
import asyncio import asyncio
import uvloop import uvloop
from starlette.requests import Request
from loguru import logger from loguru import logger
from starlette.requests import Request
from .commands import migrate_databases from .commands import migrate_databases
from .settings import ( from .settings import (
DEBUG, DEBUG,
HOST,
LNBITS_COMMIT, LNBITS_COMMIT,
LNBITS_DATA_FOLDER, LNBITS_DATA_FOLDER,
LNBITS_DATABASE_URL,
LNBITS_SITE_TITLE, LNBITS_SITE_TITLE,
HOST,
PORT, PORT,
WALLET, WALLET,
LNBITS_DATABASE_URL,
LNBITS_DATA_FOLDER,
) )
uvloop.install() uvloop.install()

View File

@ -1,11 +1,9 @@
import asyncio import asyncio
import importlib import importlib
import logging
import sys import sys
import traceback import traceback
import warnings import warnings
from loguru import logger
from http import HTTPStatus from http import HTTPStatus
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
@ -14,6 +12,7 @@ from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware from fastapi.middleware.gzip import GZipMiddleware
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from loguru import logger
import lnbits.settings import lnbits.settings
from lnbits.core.tasks import register_task_listeners from lnbits.core.tasks import register_task_listeners
@ -199,8 +198,33 @@ def register_exception_handlers(app: FastAPI):
def configure_logger() -> None: def configure_logger() -> None:
logger.remove() logger.remove()
log_level: str = "DEBUG" if lnbits.settings.DEBUG else "INFO" log_level: str = "DEBUG" if lnbits.settings.DEBUG else "INFO"
formatter = Formatter()
logger.add(sys.stderr, level=log_level, format=formatter.format)
logging.getLogger("uvicorn").handlers = [InterceptHandler()]
logging.getLogger("uvicorn.access").handlers = [InterceptHandler()]
class Formatter:
def __init__(self):
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 lnbits.settings.DEBUG: if lnbits.settings.DEBUG:
fmt: str = "<green>{time:YYYY-MM-DD HH:mm:ss.SS}</green> | <level>{level: <6}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> | <level>{message}</level>" 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: else:
fmt: str = "<green>{time:YYYY-MM-DD HH:mm:ss.SS}</green> | <level>{level}</level> | <level>{message}</level>" self.fmt: str = self.minimal_fmt
logger.add(sys.stderr, level=log_level, format=fmt)
def format(self, record):
function = "{function}".format(**record)
if function == "emit": # uvicorn logs
return self.minimal_fmt
return self.fmt
class InterceptHandler(logging.Handler):
def emit(self, record):
try:
level = logger.level(record.levelname).name
except ValueError:
level = record.levelno
logger.log(level, record.getMessage())

View File

@ -1,15 +1,16 @@
import bitstring # type: ignore
import re
import hashlib import hashlib
from typing import List, NamedTuple, Optional import re
from bech32 import bech32_encode, bech32_decode, CHARSET
from ecdsa import SECP256k1, VerifyingKey # type: ignore
from ecdsa.util import sigdecode_string # type: ignore
from binascii import unhexlify
import time import time
from binascii import unhexlify
from decimal import Decimal from decimal import Decimal
from typing import List, NamedTuple, Optional
import bitstring # type: ignore
import embit import embit
import secp256k1 import secp256k1
from bech32 import CHARSET, bech32_decode, bech32_encode
from ecdsa import SECP256k1, VerifyingKey # type: ignore
from ecdsa.util import sigdecode_string # type: ignore
class Route(NamedTuple): class Route(NamedTuple):

View File

@ -1,18 +1,19 @@
import asyncio import asyncio
import warnings
import click
import importlib import importlib
import re
import os import os
import re
import warnings
import click
from loguru import logger from loguru import logger
from .db import SQLITE, POSTGRES, COCKROACH from .core import db as core_db
from .core import db as core_db, migrations as core_migrations from .core import migrations as core_migrations
from .db import COCKROACH, POSTGRES, SQLITE
from .helpers import ( from .helpers import (
get_valid_extensions,
get_css_vendored, get_css_vendored,
get_js_vendored, get_js_vendored,
get_valid_extensions,
url_for_vendored, url_for_vendored,
) )
from .settings import LNBITS_PATH from .settings import LNBITS_PATH

View File

@ -1,15 +1,15 @@
import json
import datetime import datetime
from uuid import uuid4 import json
from typing import List, Optional, Dict, Any from typing import Any, Dict, List, Optional
from urllib.parse import urlparse from urllib.parse import urlparse
from uuid import uuid4
from lnbits import bolt11 from lnbits import bolt11
from lnbits.db import Connection, POSTGRES, COCKROACH from lnbits.db import COCKROACH, POSTGRES, Connection
from lnbits.settings import DEFAULT_WALLET_NAME, LNBITS_ADMIN_USERS from lnbits.settings import DEFAULT_WALLET_NAME, LNBITS_ADMIN_USERS
from . import db from . import db
from .models import User, Wallet, Payment, BalanceCheck from .models import BalanceCheck, Payment, User, Wallet
# accounts # accounts
# -------- # --------

View File

@ -1,15 +1,15 @@
import json
import hmac
import hashlib import hashlib
from lnbits.helpers import url_for import hmac
import json
from sqlite3 import Row
from typing import Dict, List, NamedTuple, Optional
from ecdsa import SECP256k1, SigningKey # type: ignore from ecdsa import SECP256k1, SigningKey # type: ignore
from lnurl import encode as lnurl_encode # type: ignore from lnurl import encode as lnurl_encode # type: ignore
from typing import List, NamedTuple, Optional, Dict from loguru import logger
from sqlite3 import Row
from pydantic import BaseModel from pydantic import BaseModel
from loguru import logger from lnbits.helpers import url_for
from lnbits.settings import WALLET from lnbits.settings import WALLET

View File

@ -3,20 +3,25 @@ import json
from binascii import unhexlify from binascii import unhexlify
from io import BytesIO from io import BytesIO
from typing import Dict, Optional, Tuple from typing import Dict, Optional, Tuple
from loguru import logger
from urllib.parse import parse_qs, urlparse from urllib.parse import parse_qs, urlparse
import httpx import httpx
from fastapi import Depends
from lnurl import LnurlErrorResponse from lnurl import LnurlErrorResponse
from lnurl import decode as decode_lnurl # type: ignore from lnurl import decode as decode_lnurl # type: ignore
from loguru import logger
from lnbits import bolt11 from lnbits import bolt11
from lnbits.db import Connection from lnbits.db import Connection
from lnbits.decorators import (
WalletTypeInfo,
get_key_type,
require_admin_key,
require_invoice_key,
)
from lnbits.helpers import url_for, urlsafe_short_hash from lnbits.helpers import url_for, urlsafe_short_hash
from lnbits.requestvars import g from lnbits.requestvars import g
from lnbits.settings import WALLET from lnbits.settings import FAKE_WALLET, WALLET
from lnbits.wallets.base import PaymentResponse, PaymentStatus from lnbits.wallets.base import PaymentResponse, PaymentStatus
from . import db from . import db
@ -51,15 +56,19 @@ async def create_invoice(
description_hash: Optional[bytes] = None, description_hash: Optional[bytes] = None,
extra: Optional[Dict] = None, extra: Optional[Dict] = None,
webhook: Optional[str] = None, webhook: Optional[str] = None,
internal: Optional[bool] = False,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> Tuple[str, str]: ) -> Tuple[str, str]:
invoice_memo = None if description_hash else memo invoice_memo = None if description_hash else memo
ok, checking_id, payment_request, error_message = await WALLET.create_invoice( # use the fake wallet if the invoice is for internal use only
wallet = FAKE_WALLET if internal else WALLET
ok, checking_id, payment_request, error_message = await wallet.create_invoice(
amount=amount, memo=invoice_memo, description_hash=description_hash amount=amount, memo=invoice_memo, description_hash=description_hash
) )
if not ok: if not ok:
raise InvoiceFailure(error_message or "Unexpected backend error.") raise InvoiceFailure(error_message or "unexpected backend error.")
invoice = bolt11.decode(payment_request) invoice = bolt11.decode(payment_request)
@ -229,7 +238,7 @@ async def redeem_lnurl_withdraw(
conn=conn, conn=conn,
) )
except: except:
logger.warn( logger.warning(
f"failed to create invoice on redeem_lnurl_withdraw from {lnurl}. params: {res}" f"failed to create invoice on redeem_lnurl_withdraw from {lnurl}. params: {res}"
) )
return None return None
@ -256,12 +265,14 @@ async def redeem_lnurl_withdraw(
async def perform_lnurlauth( async def perform_lnurlauth(
callback: str, conn: Optional[Connection] = None callback: str,
wallet: WalletTypeInfo = Depends(require_admin_key),
conn: Optional[Connection] = None,
) -> Optional[LnurlErrorResponse]: ) -> Optional[LnurlErrorResponse]:
cb = urlparse(callback) cb = urlparse(callback)
k1 = unhexlify(parse_qs(cb.query)["k1"][0]) k1 = unhexlify(parse_qs(cb.query)["k1"][0])
key = g().wallet.lnurlauth_key(cb.netloc) key = wallet.wallet.lnurlauth_key(cb.netloc)
def int_to_bytes_suitable_der(x: int) -> bytes: def int_to_bytes_suitable_der(x: int) -> bytes:
"""for strict DER we need to encode the integer with some quirks""" """for strict DER we need to encode the integer with some quirks"""

View File

@ -1,4 +1,36 @@
new Vue({ new Vue({
el: '#vue', el: '#vue',
data: function () {
return {
searchTerm: '',
filteredExtensions: null
}
},
mounted() {
this.filteredExtensions = this.g.extensions
},
watch: {
searchTerm(term) {
// Reset the filter
this.filteredExtensions = this.g.extensions
if (term !== '') {
// Filter the extensions list
function extensionNameContains(searchTerm) {
return function (extension) {
return (
extension.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
extension.shortDescription
.toLowerCase()
.includes(searchTerm.toLowerCase())
)
}
}
this.filteredExtensions = this.filteredExtensions.filter(
extensionNameContains(term)
)
}
}
},
mixins: [windowMixin] mixins: [windowMixin]
}) })

View File

@ -1,7 +1,7 @@
import asyncio import asyncio
import httpx
from typing import List from typing import List
import httpx
from loguru import logger from loguru import logger
from lnbits.tasks import register_invoice_listener from lnbits.tasks import register_invoice_listener

View File

@ -49,7 +49,8 @@
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5> <h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code <code
>{"out": false, "amount": &lt;int&gt;, "memo": &lt;string&gt;, "unit": >{"out": false, "amount": &lt;int&gt;, "memo": &lt;string&gt;, "unit":
&lt;string&gt;, "webhook": &lt;url:string&gt;}</code &lt;string&gt;, "webhook": &lt;url:string&gt;, "internal":
&lt;bool&gt;}</code
> >
<h5 class="text-caption q-mt-sm q-mb-none"> <h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json) Returns 201 CREATED (application/json)

View File

@ -2,10 +2,23 @@
%} {% block scripts %} {{ window_vars(user) }} %} {% block scripts %} {{ window_vars(user) }}
<script src="/core/static/js/extensions.js"></script> <script src="/core/static/js/extensions.js"></script>
{% endblock %} {% block page %} {% endblock %} {% block page %}
<div class="row q-col-gutter-md q-mb-md">
<div class="col-sm-3 col-xs-8 q-ml-auto">
<q-input v-model="searchTerm" label="Search extensions">
<q-icon
v-if="searchTerm !== ''"
name="close"
@click="searchTerm = ''"
class="cursor-pointer q-mt-lg"
/>
</q-input>
</div>
</div>
<div class="row q-col-gutter-md"> <div class="row q-col-gutter-md">
<div <div
class="col-6 col-md-4 col-lg-3" class="col-6 col-md-4 col-lg-3"
v-for="extension in g.extensions" v-for="extension in filteredExtensions"
:key="extension.code" :key="extension.code"
> >
<q-card> <q-card>

View File

@ -7,30 +7,23 @@ from typing import Dict, List, Optional, Union
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
import httpx import httpx
from fastapi import Depends, Header, Query, Request
from loguru import logger
from fastapi import Header, Query, Request
from fastapi.exceptions import HTTPException from fastapi.exceptions import HTTPException
from fastapi.param_functions import Depends
from fastapi.params import Body from fastapi.params import Body
from loguru import logger
from pydantic import BaseModel from pydantic import BaseModel
from pydantic.fields import Field from pydantic.fields import Field
from sse_starlette.sse import EventSourceResponse from sse_starlette.sse import EventSourceResponse
from lnbits import bolt11, lnurl from lnbits import bolt11, lnurl
from lnbits.bolt11 import Invoice
from lnbits.core.models import Payment, Wallet from lnbits.core.models import Payment, Wallet
from lnbits.decorators import ( from lnbits.decorators import (
WalletAdminKeyChecker,
WalletInvoiceKeyChecker,
WalletTypeInfo, WalletTypeInfo,
get_key_type, get_key_type,
require_admin_key, require_admin_key,
require_invoice_key, require_invoice_key,
) )
from lnbits.helpers import url_for, urlsafe_short_hash from lnbits.helpers import url_for, urlsafe_short_hash
from lnbits.requestvars import g
from lnbits.settings import LNBITS_ADMIN_USERS, LNBITS_SITE_TITLE from lnbits.settings import LNBITS_ADMIN_USERS, LNBITS_SITE_TITLE
from lnbits.utils.exchange_rates import ( from lnbits.utils.exchange_rates import (
currencies, currencies,
@ -149,6 +142,7 @@ class CreateInvoiceData(BaseModel):
lnurl_balance_check: Optional[str] = None lnurl_balance_check: Optional[str] = None
extra: Optional[dict] = None extra: Optional[dict] = None
webhook: Optional[str] = None webhook: Optional[str] = None
internal: Optional[bool] = False
bolt11: Optional[str] = None bolt11: Optional[str] = None
@ -175,6 +169,7 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
description_hash=description_hash, description_hash=description_hash,
extra=data.extra, extra=data.extra,
webhook=data.webhook, webhook=data.webhook,
internal=data.internal,
conn=conn, conn=conn,
) )
except InvoiceFailure as e: except InvoiceFailure as e:
@ -395,7 +390,7 @@ async def api_payment(payment_hash, X_Api_Key: Optional[str] = Header(None)):
wallet = None wallet = None
try: try:
if X_Api_Key.extra: if X_Api_Key.extra:
logger.warn("No key") logger.warning("No key")
except: except:
wallet = await get_wallet_for_key(X_Api_Key) wallet = await get_wallet_for_key(X_Api_Key)
payment = await get_standalone_payment( payment = await get_standalone_payment(
@ -435,10 +430,8 @@ async def api_payment(payment_hash, X_Api_Key: Optional[str] = Header(None)):
return {"paid": not payment.pending, "preimage": payment.preimage} return {"paid": not payment.pending, "preimage": payment.preimage}
@core_app.get( @core_app.get("/api/v1/lnurlscan/{code}")
"/api/v1/lnurlscan/{code}", dependencies=[Depends(WalletInvoiceKeyChecker())] async def api_lnurlscan(code: str, wallet: WalletTypeInfo = Depends(get_key_type)):
)
async def api_lnurlscan(code: str):
try: try:
url = lnurl.decode(code) url = lnurl.decode(code)
domain = urlparse(url).netloc domain = urlparse(url).netloc
@ -466,7 +459,7 @@ async def api_lnurlscan(code: str):
params.update(kind="auth") params.update(kind="auth")
params.update(callback=url) # with k1 already in it params.update(callback=url) # with k1 already in it
lnurlauth_key = g().wallet.lnurlauth_key(domain) lnurlauth_key = wallet.wallet.lnurlauth_key(domain)
params.update(pubkey=lnurlauth_key.verifying_key.to_string("compressed").hex()) params.update(pubkey=lnurlauth_key.verifying_key.to_string("compressed").hex())
else: else:
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
@ -582,14 +575,19 @@ async def api_payments_decode(data: DecodePayment):
return {"message": "Failed to decode"} return {"message": "Failed to decode"}
@core_app.post("/api/v1/lnurlauth", dependencies=[Depends(WalletAdminKeyChecker())]) class Callback(BaseModel):
async def api_perform_lnurlauth(callback: str): callback: str = Query(...)
err = await perform_lnurlauth(callback)
@core_app.post("/api/v1/lnurlauth")
async def api_perform_lnurlauth(
callback: Callback, wallet: WalletTypeInfo = Depends(require_admin_key)
):
err = await perform_lnurlauth(callback.callback, wallet=wallet)
if err: if err:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.SERVICE_UNAVAILABLE, detail=err.reason status_code=HTTPStatus.SERVICE_UNAVAILABLE, detail=err.reason
) )
return "" return ""

View File

@ -7,11 +7,10 @@ from fastapi.exceptions import HTTPException
from fastapi.params import Depends, Query from fastapi.params import Depends, Query
from fastapi.responses import FileResponse, RedirectResponse from fastapi.responses import FileResponse, RedirectResponse
from fastapi.routing import APIRouter from fastapi.routing import APIRouter
from loguru import logger
from pydantic.types import UUID4 from pydantic.types import UUID4
from starlette.responses import HTMLResponse, JSONResponse from starlette.responses import HTMLResponse, JSONResponse
from loguru import logger
from lnbits.core import db from lnbits.core import db
from lnbits.core.models import User from lnbits.core.models import User
from lnbits.decorators import check_user_exists from lnbits.decorators import check_user_exists
@ -113,7 +112,7 @@ async def wallet(
if not user_id: if not user_id:
user = await get_user((await create_account()).id) user = await get_user((await create_account()).id)
logger.info(f"Created new account for user {user.id}") logger.info(f"Create user {user.id}")
else: else:
user = await get_user(user_id) user = await get_user(user_id)
if not user: if not user:
@ -140,7 +139,7 @@ async def wallet(
status_code=status.HTTP_307_TEMPORARY_REDIRECT, status_code=status.HTTP_307_TEMPORARY_REDIRECT,
) )
logger.info(f"Access wallet {wallet_name} of user {user.id}") logger.debug(f"Access wallet {wallet_name}{'of user '+ user.id if user else ''}")
wallet = user.get_wallet(wallet_id) wallet = user.get_wallet(wallet_id)
if not wallet: if not wallet:
return template_renderer().TemplateResponse( return template_renderer().TemplateResponse(

View File

@ -4,11 +4,10 @@ from http import HTTPStatus
from urllib.parse import urlparse from urllib.parse import urlparse
from fastapi import HTTPException from fastapi import HTTPException
from loguru import logger
from starlette.requests import Request from starlette.requests import Request
from starlette.responses import HTMLResponse from starlette.responses import HTMLResponse
from loguru import logger
from lnbits import bolt11 from lnbits import bolt11
from .. import core_app from .. import core_app

View File

@ -6,7 +6,6 @@ from contextlib import asynccontextmanager
from typing import Optional from typing import Optional
from loguru import logger from loguru import logger
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy_aio.base import AsyncConnection from sqlalchemy_aio.base import AsyncConnection
from sqlalchemy_aio.strategy import ASYNCIO_STRATEGY # type: ignore from sqlalchemy_aio.strategy import ASYNCIO_STRATEGY # type: ignore

View File

@ -14,9 +14,9 @@ from lnbits.core.crud import get_user, get_wallet_for_key
from lnbits.core.models import User, Wallet from lnbits.core.models import User, Wallet
from lnbits.requestvars import g from lnbits.requestvars import g
from lnbits.settings import ( from lnbits.settings import (
LNBITS_ALLOWED_USERS,
LNBITS_ADMIN_USERS,
LNBITS_ADMIN_EXTENSIONS, LNBITS_ADMIN_EXTENSIONS,
LNBITS_ADMIN_USERS,
LNBITS_ALLOWED_USERS,
) )

View File

@ -1,7 +1,8 @@
import httpx
import json import json
import os import os
import httpx
fiat_currencies = json.load( fiat_currencies = json.load(
open( open(
os.path.join( os.path.join(

View File

@ -3,9 +3,8 @@ import math
import traceback import traceback
from http import HTTPStatus from http import HTTPStatus
from starlette.requests import Request
from loguru import logger from loguru import logger
from starlette.requests import Request
from . import bleskomat_ext from . import bleskomat_ext
from .crud import ( from .crud import (

View File

@ -3,13 +3,12 @@ import time
from typing import Dict from typing import Dict
from fastapi.params import Query from fastapi.params import Query
from loguru import logger
from pydantic import BaseModel, validator from pydantic import BaseModel, validator
from starlette.requests import Request from starlette.requests import Request
from loguru import logger
from lnbits import bolt11 from lnbits import bolt11
from lnbits.core.services import pay_invoice, PaymentFailure from lnbits.core.services import PaymentFailure, pay_invoice
from . import db from . import db
from .exchange_rates import exchange_rate_providers, fiat_currencies from .exchange_rates import exchange_rate_providers, fiat_currencies

View File

@ -1,9 +1,8 @@
from http import HTTPStatus from http import HTTPStatus
from fastapi import Depends, Query from fastapi import Depends, Query
from starlette.exceptions import HTTPException
from loguru import logger from loguru import logger
from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user from lnbits.core.crud import get_user
from lnbits.decorators import WalletTypeInfo, require_admin_key from lnbits.decorators import WalletTypeInfo, require_admin_key

View File

@ -1,12 +1,14 @@
from urllib.parse import urlparse, urlunparse, parse_qs, urlencode, ParseResult
from starlette.requests import Request
from fastapi.param_functions import Query
from typing import Optional, Dict
from lnbits.lnurl import encode as lnurl_encode # type: ignore
from lnurl.types import LnurlPayMetadata # type: ignore
from pydantic import BaseModel
import json import json
from sqlite3 import Row from sqlite3 import Row
from typing import Dict, Optional
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
from fastapi.param_functions import Query
from lnurl.types import LnurlPayMetadata # type: ignore
from pydantic import BaseModel
from starlette.requests import Request
from lnbits.lnurl import encode as lnurl_encode # type: ignore
class CreateCopilotData(BaseModel): class CreateCopilotData(BaseModel):

View File

@ -25,7 +25,7 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None: async def on_invoice_paid(payment: Payment) -> None:
webhook = None webhook = None
data = None data = None
if "copilot" != payment.extra.get("tag"): if payment.extra.get("tag") != "copilot":
# not an copilot invoice # not an copilot invoice
return return

View File

@ -1,8 +1,8 @@
from sqlite3 import Row from sqlite3 import Row
from typing import Optional
from fastapi.param_functions import Query from fastapi.param_functions import Query
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional
class CreateUserData(BaseModel): class CreateUserData(BaseModel):

View File

@ -1,9 +1,9 @@
from typing import NamedTuple
from sqlite3 import Row from sqlite3 import Row
from typing import NamedTuple, Optional
from fastapi.param_functions import Query from fastapi.param_functions import Query
from pydantic.main import BaseModel
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional from pydantic.main import BaseModel
class CreateJukeLinkData(BaseModel): class CreateJukeLinkData(BaseModel):

View File

@ -16,7 +16,7 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None: async def on_invoice_paid(payment: Payment) -> None:
if "jukebox" != payment.extra.get("tag"): if payment.extra.get("tag") != "jukebox":
# not a jukebox invoice # not a jukebox invoice
return return
await update_jukebox_payment(payment.payment_hash, paid=True) await update_jukebox_payment(payment.payment_hash, paid=True)

View File

@ -117,7 +117,7 @@
> >
<q-step <q-step
:name="1" :name="1"
title="Pick wallet, price" title="1. Pick Wallet and Price"
icon="account_balance_wallet" icon="account_balance_wallet"
:done="step > 1" :done="step > 1"
> >
@ -170,16 +170,25 @@
<br /> <br />
</q-step> </q-step>
<q-step :name="2" title="Add api keys" icon="vpn_key" :done="step > 2"> <q-step
:name="2"
title="2. Add API keys"
icon="vpn_key"
:done="step > 2"
>
<img src="/jukebox/static/spotapi.gif" /> <img src="/jukebox/static/spotapi.gif" />
To use this extension you need a Spotify client ID and client secret. To use this extension you need a Spotify client ID and client secret.
You get these by creating an app in the Spotify developers dashboard You get these by creating an app in the Spotify Developer Dashboard
<a <br />
<br />
<q-btn
type="a"
target="_blank" target="_blank"
style="color: #43a047" color="primary"
href="https://developer.spotify.com/dashboard/applications" href="https://developer.spotify.com/dashboard/applications"
>here</a >Open the Spotify Developer Dashboard</q-btn
>. >
<q-input <q-input
filled filled
class="q-pb-md q-pt-md" class="q-pb-md q-pt-md"
@ -231,28 +240,39 @@
<br /> <br />
</q-step> </q-step>
<q-step :name="3" title="Add Redirect URI" icon="link" :done="step > 3"> <q-step
:name="3"
title="3. Add Redirect URI"
icon="link"
:done="step > 3"
>
<img src="/jukebox/static/spotapi1.gif" /> <img src="/jukebox/static/spotapi1.gif" />
<p>
In the app go to edit-settings, set the redirect URI to this link In the app go to edit-settings, set the redirect URI to this link
</p>
<q-card
class="cursor-pointer word-break"
@click="copyText(locationcb + jukeboxDialog.data.sp_id, 'Link copied to clipboard!')"
>
<q-card-section style="word-break: break-all">
{% raw %}{{ locationcb }}{{ jukeboxDialog.data.sp_id }}{% endraw
%}
</q-card-section>
<q-tooltip> Click to copy URL </q-tooltip>
</q-card>
<br /> <br />
<q-btn <q-btn
dense type="a"
outline
unelevated
color="primary"
size="xs"
@click="copyText(locationcb + jukeboxDialog.data.sp_id, 'Link copied to clipboard!')"
>{% raw %}{{ locationcb }}{{ jukeboxDialog.data.sp_id }}{% endraw
%}<q-tooltip> Click to copy URL </q-tooltip>
</q-btn>
<br />
Settings can be found
<a
target="_blank" target="_blank"
style="color: #43a047" color="primary"
href="https://developer.spotify.com/dashboard/applications" href="https://developer.spotify.com/dashboard/applications"
>here</a >Open the Spotify Application Settings</q-btn
>. >
<br /><br />
<p>
After adding the redirect URI, click the "Authorise access" button
below.
</p>
<div class="row q-mt-md"> <div class="row q-mt-md">
<div class="col-4"> <div class="col-4">
@ -281,7 +301,7 @@
<q-step <q-step
:name="4" :name="4"
title="Select playlists" title="4. Select Device and Playlists"
icon="queue_music" icon="queue_music"
active-color="primary" active-color="primary"
:done="step > 4" :done="step > 4"

View File

@ -22,7 +22,7 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None: async def on_invoice_paid(payment: Payment) -> None:
if "livestream" != payment.extra.get("tag"): if payment.extra.get("tag") != "livestream":
# not a livestream invoice # not a livestream invoice
return return

View File

@ -1,7 +1,5 @@
from http import HTTPStatus from http import HTTPStatus
# from mmap import MAP_DENYWRITE
from fastapi.param_functions import Depends from fastapi.param_functions import Depends
from fastapi.params import Query from fastapi.params import Query
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
@ -15,6 +13,8 @@ from lnbits.decorators import check_user_exists
from . import livestream_ext, livestream_renderer from . import livestream_ext, livestream_renderer
from .crud import get_livestream_by_track, get_track from .crud import get_livestream_by_track, get_track
# from mmap import MAP_DENYWRITE
@livestream_ext.get("/", response_class=HTMLResponse) @livestream_ext.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)): async def index(request: Request, user: User = Depends(check_user_exists)):

View File

@ -3,15 +3,13 @@ import json
from datetime import datetime, timedelta from datetime import datetime, timedelta
import httpx import httpx
from loguru import logger
from fastapi.params import Query from fastapi.params import Query
from lnurl import ( # type: ignore from lnurl import ( # type: ignore
LnurlErrorResponse, LnurlErrorResponse,
LnurlPayActionResponse, LnurlPayActionResponse,
LnurlPayResponse, LnurlPayResponse,
) )
from loguru import logger
from starlette.requests import Request from starlette.requests import Request
from starlette.responses import HTMLResponse from starlette.responses import HTMLResponse

View File

@ -43,13 +43,13 @@ async def call_webhook_on_paid(payment_hash):
async def on_invoice_paid(payment: Payment) -> None: async def on_invoice_paid(payment: Payment) -> None:
if "lnaddress" == payment.extra.get("tag"): if payment.extra.get("tag") == "lnaddress":
await payment.set_pending(False) await payment.set_pending(False)
await set_address_paid(payment_hash=payment.payment_hash) await set_address_paid(payment_hash=payment.payment_hash)
await call_webhook_on_paid(payment_hash=payment.payment_hash) await call_webhook_on_paid(payment_hash=payment.payment_hash)
elif "renew lnaddress" == payment.extra.get("tag"): elif payment.extra.get("tag") == "renew lnaddress":
await payment.set_pending(False) await payment.set_pending(False)
await set_address_renewed( await set_address_renewed(

View File

@ -1,14 +1,12 @@
from base64 import b64decode from base64 import b64decode
from fastapi.param_functions import Security
from fastapi.security.api_key import APIKeyHeader
from fastapi import Request, status from fastapi import Request, status
from fastapi.param_functions import Security
from fastapi.security.api_key import APIKeyHeader
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
from lnbits.decorators import WalletTypeInfo, get_key_type # type: ignore from lnbits.decorators import WalletTypeInfo, get_key_type # type: ignore
api_key_header_auth = APIKeyHeader( api_key_header_auth = APIKeyHeader(
name="AUTHORIZATION", name="AUTHORIZATION",
auto_error=False, auto_error=False,

View File

@ -1,8 +1,10 @@
from lnbits.decorators import check_user_exists
from . import lndhub_ext, lndhub_renderer
from fastapi import Request from fastapi import Request
from fastapi.params import Depends from fastapi.params import Depends
from lnbits.core.models import User from lnbits.core.models import User
from lnbits.decorators import check_user_exists
from . import lndhub_ext, lndhub_renderer
@lndhub_ext.get("/") @lndhub_ext.get("/")

View File

@ -1,6 +1,5 @@
import time
import asyncio import asyncio
import time
from base64 import urlsafe_b64encode from base64 import urlsafe_b64encode
from http import HTTPStatus from http import HTTPStatus
@ -13,7 +12,7 @@ from lnbits import bolt11
from lnbits.core.crud import delete_expired_invoices, get_payments from lnbits.core.crud import delete_expired_invoices, get_payments
from lnbits.core.services import create_invoice, pay_invoice from lnbits.core.services import create_invoice, pay_invoice
from lnbits.decorators import WalletTypeInfo from lnbits.decorators import WalletTypeInfo
from lnbits.settings import WALLET, LNBITS_SITE_TITLE from lnbits.settings import LNBITS_SITE_TITLE, WALLET
from . import lndhub_ext from . import lndhub_ext
from .decorators import check_wallet, require_admin_key from .decorators import check_wallet, require_admin_key

View File

@ -1,11 +1,12 @@
from lnbits.core.models import Wallet
from typing import List, Optional, Union from typing import List, Optional, Union
import httpx
from lnbits.core.models import Wallet
from lnbits.helpers import urlsafe_short_hash from lnbits.helpers import urlsafe_short_hash
from . import db from . import db
from .models import CreateFormData, CreateTicketData, Tickets, Forms from .models import CreateFormData, CreateTicketData, Forms, Tickets
import httpx
async def create_ticket( async def create_ticket(

View File

@ -1,4 +1,5 @@
from typing import Optional from typing import Optional
from fastapi.param_functions import Query from fastapi.param_functions import Query
from pydantic import BaseModel from pydantic import BaseModel

View File

@ -18,7 +18,7 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None: async def on_invoice_paid(payment: Payment) -> None:
if "lnticket" != payment.extra.get("tag"): if payment.extra.get("tag") != "lnticket":
# not a lnticket invoice # not a lnticket invoice
return return

View File

@ -1,30 +1,26 @@
import base64 import base64
import hashlib import hashlib
import hmac
from http import HTTPStatus from http import HTTPStatus
from io import BytesIO
from typing import Optional from typing import Optional
from embit import bech32 from embit import bech32, compact
from embit import compact
import base64
from io import BytesIO
import hmac
from fastapi import Request from fastapi import Request
from fastapi.param_functions import Query from fastapi.param_functions import Query
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
from lnbits.core.services import create_invoice from lnbits.core.services import create_invoice
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
from lnbits.core.views.api import pay_invoice from lnbits.core.views.api import pay_invoice
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
from . import lnurldevice_ext from . import lnurldevice_ext
from .crud import ( from .crud import (
create_lnurldevicepayment, create_lnurldevicepayment,
get_lnurldevice, get_lnurldevice,
get_lnurldevicepayment, get_lnurldevicepayment,
update_lnurldevicepayment,
get_lnurlpayload, get_lnurlpayload,
update_lnurldevicepayment,
) )

View File

@ -38,7 +38,7 @@ async def m001_initial(db):
async def m002_redux(db): async def m002_redux(db):
""" """
Moves everything from lnurlpos to lnurldevices Moves everything from lnurlpos to lnurldevice
""" """
try: try:
for row in [ for row in [

View File

@ -120,7 +120,7 @@
<q-card-section> <q-card-section>
<code <code
><span class="text-blue">GET</span> ><span class="text-blue">GET</span>
/lnurldevice/api/v1/lnurlposs</code /lnurldevice/api/v1/lnurlpos</code
> >
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5> <h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br /> <code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />

View File

@ -1,8 +1,9 @@
from typing import List, Optional, Union from typing import List, Optional, Union
from lnbits.db import SQLITE from lnbits.db import SQLITE
from . import db from . import db
from .models import PayLink, CreatePayLinkData from .models import CreatePayLinkData, PayLink
async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink: async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink:

View File

@ -1,12 +1,14 @@
import json import json
from urllib.parse import urlparse, urlunparse, parse_qs, urlencode, ParseResult
from starlette.requests import Request
from fastapi.param_functions import Query
from typing import Optional, Dict
from lnbits.lnurl import encode as lnurl_encode # type: ignore
from lnurl.types import LnurlPayMetadata # type: ignore
from sqlite3 import Row from sqlite3 import Row
from typing import Dict, Optional
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
from fastapi.param_functions import Query
from lnurl.types import LnurlPayMetadata # type: ignore
from pydantic import BaseModel from pydantic import BaseModel
from starlette.requests import Request
from lnbits.lnurl import encode as lnurl_encode # type: ignore
class CreatePayLinkData(BaseModel): class CreatePayLinkData(BaseModel):

View File

@ -35,6 +35,7 @@ new Vue({
rowsPerPage: 10 rowsPerPage: 10
} }
}, },
nfcTagWriting: false,
formDialog: { formDialog: {
show: false, show: false,
fixedAmount: true, fixedAmount: true,
@ -205,6 +206,42 @@ new Vue({
.catch(err => { .catch(err => {
LNbits.utils.notifyApiError(err) LNbits.utils.notifyApiError(err)
}) })
},
writeNfcTag: async function (lnurl) {
try {
if (typeof NDEFReader == 'undefined') {
throw {
toString: function () {
return 'NFC not supported on this device or browser.'
}
}
}
const ndef = new NDEFReader()
this.nfcTagWriting = true
this.$q.notify({
message: 'Tap your NFC tag to write the LNURL-pay link to it.'
})
await ndef.write({
records: [{recordType: 'url', data: 'lightning:' + lnurl, lang: 'en'}]
})
this.nfcTagWriting = false
this.$q.notify({
type: 'positive',
message: 'NFC tag written successfully.'
})
} catch (error) {
this.nfcTagWriting = false
this.$q.notify({
type: 'negative',
message: error
? error.toString()
: 'An unexpected error has occurred.'
})
}
} }
}, },
created() { created() {

View File

@ -1,5 +1,6 @@
import asyncio import asyncio
import json import json
import httpx import httpx
from lnbits.core import db as core_db from lnbits.core import db as core_db
@ -19,7 +20,7 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None: async def on_invoice_paid(payment: Payment) -> None:
if "lnurlp" != payment.extra.get("tag"): if payment.extra.get("tag") != "lnurlp":
# not an lnurlp invoice # not an lnurlp invoice
return return

View File

@ -14,10 +14,17 @@
</q-responsive> </q-responsive>
</a> </a>
</div> </div>
<div class="row q-mt-lg"> <div class="row q-mt-lg q-gutter-sm">
<q-btn outline color="grey" @click="copyText('{{ lnurl }}')" <q-btn outline color="grey" @click="copyText('{{ lnurl }}')"
>Copy LNURL</q-btn >Copy LNURL</q-btn
> >
<q-btn
outline
color="grey"
icon="nfc"
@click="writeNfcTag(' {{ lnurl }} ')"
:disable="nfcTagWriting"
></q-btn>
</div> </div>
</q-card-section> </q-card-section>
</q-card> </q-card>

View File

@ -99,7 +99,8 @@
@click="openUpdateDialog(props.row.id)" @click="openUpdateDialog(props.row.id)"
icon="edit" icon="edit"
color="light-blue" color="light-blue"
></q-btn> >
</q-btn>
<q-btn <q-btn
flat flat
dense dense
@ -153,7 +154,8 @@
v-model.trim="formDialog.data.description" v-model.trim="formDialog.data.description"
type="text" type="text"
label="Item description *" label="Item description *"
></q-input> >
</q-input>
<div class="row q-col-gutter-sm"> <div class="row q-col-gutter-sm">
<q-input <q-input
filled filled
@ -171,7 +173,8 @@
type="number" type="number"
:step="formDialog.data.currency && formDialog.data.currency !== 'satoshis' ? '0.01' : '1'" :step="formDialog.data.currency && formDialog.data.currency !== 'satoshis' ? '0.01' : '1'"
label="Max *" label="Max *"
></q-input> >
</q-input>
</div> </div>
<div class="row q-col-gutter-sm"> <div class="row q-col-gutter-sm">
<div class="col"> <div class="col">
@ -200,7 +203,8 @@
type="number" type="number"
label="Comment maximum characters" label="Comment maximum characters"
hint="Tell wallets to prompt users for a comment that will be sent along with the payment. LNURLp will store the comment and send it in the webhook." hint="Tell wallets to prompt users for a comment that will be sent along with the payment. LNURLp will store the comment and send it in the webhook."
></q-input> >
</q-input>
<q-input <q-input
filled filled
dense dense
@ -224,7 +228,8 @@
type="text" type="text"
label="Success URL (optional)" label="Success URL (optional)"
hint="Will be shown as a clickable link to the user in his wallet after a successful payment, appended by the payment_hash as a query string." hint="Will be shown as a clickable link to the user in his wallet after a successful payment, appended by the payment_hash as a query string."
></q-input> >
</q-input>
<div class="row q-mt-lg"> <div class="row q-mt-lg">
<q-btn <q-btn
v-if="formDialog.data.id" v-if="formDialog.data.id"
@ -294,6 +299,14 @@
@click="copyText(qrCodeDialog.data.pay_url, 'Link copied to clipboard!')" @click="copyText(qrCodeDialog.data.pay_url, 'Link copied to clipboard!')"
>Shareable link</q-btn >Shareable link</q-btn
> >
<q-btn
outline
color="grey"
icon="nfc"
@click="writeNfcTag(qrCodeDialog.data.lnurl)"
:disable="nfcTagWriting"
>
</q-btn>
<q-btn <q-btn
outline outline
color="grey" color="grey"

View File

@ -73,6 +73,7 @@ async def api_link_retrieve(
@lnurlp_ext.put("/api/v1/links/{link_id}", status_code=HTTPStatus.OK) @lnurlp_ext.put("/api/v1/links/{link_id}", status_code=HTTPStatus.OK)
async def api_link_create_or_update( async def api_link_create_or_update(
data: CreatePayLinkData, data: CreatePayLinkData,
request: Request,
link_id=None, link_id=None,
wallet: WalletTypeInfo = Depends(get_key_type), wallet: WalletTypeInfo = Depends(get_key_type),
): ):
@ -117,7 +118,7 @@ async def api_link_create_or_update(
link = await update_pay_link(**data.dict(), link_id=link_id) link = await update_pay_link(**data.dict(), link_id=link_id)
else: else:
link = await create_pay_link(data, wallet_id=wallet.wallet.id) link = await create_pay_link(data, wallet_id=wallet.wallet.id)
return {**link.dict(), "lnurl": link.lnurl} return {**link.dict(), "lnurl": link.lnurl(request)}
@lnurlp_ext.delete("/api/v1/links/{link_id}") @lnurlp_ext.delete("/api/v1/links/{link_id}")

View File

@ -1,4 +1,5 @@
import asyncio import asyncio
from fastapi import APIRouter from fastapi import APIRouter
from lnbits.db import Database from lnbits.db import Database

View File

@ -3,7 +3,7 @@ from typing import List, Optional, Union
from lnbits.helpers import urlsafe_short_hash from lnbits.helpers import urlsafe_short_hash
from . import db from . import db
from .models import lnurlpayout, CreateLnurlPayoutData from .models import CreateLnurlPayoutData, lnurlpayout
async def create_lnurlpayout( async def create_lnurlpayout(

View File

@ -2,9 +2,7 @@ import asyncio
from http import HTTPStatus from http import HTTPStatus
import httpx import httpx
from loguru import logger from loguru import logger
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
from lnbits.core import db as core_db from lnbits.core import db as core_db

View File

@ -1,9 +1,10 @@
from typing import List, Optional from typing import List, Optional
from lnbits.db import SQLITE from lnbits.db import SQLITE
from . import db from . import db
from .models import Item, Shop
from .wordlists import animals from .wordlists import animals
from .models import Shop, Item
async def create_shop(*, wallet_id: str) -> int: async def create_shop(*, wallet_id: str) -> int:

View File

@ -1,6 +1,6 @@
import base64 import base64
import struct
import hmac import hmac
import struct
import time import time

View File

@ -1,14 +1,15 @@
import json
import base64 import base64
import hashlib import hashlib
import json
from collections import OrderedDict from collections import OrderedDict
from typing import Dict, List, Optional
from typing import Optional, List, Dict
from lnurl import encode as lnurl_encode # type: ignore from lnurl import encode as lnurl_encode # type: ignore
from lnurl.types import LnurlPayMetadata # type: ignore
from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore
from lnurl.types import LnurlPayMetadata # type: ignore
from pydantic import BaseModel from pydantic import BaseModel
from starlette.requests import Request from starlette.requests import Request
from .helpers import totp from .helpers import totp
shop_counters: Dict = {} shop_counters: Dict = {}

View File

@ -3,18 +3,18 @@ from datetime import datetime
from http import HTTPStatus from http import HTTPStatus
from typing import List from typing import List
from fastapi import HTTPException, Request
from fastapi.params import Depends, Query from fastapi.params import Depends, Query
from starlette.responses import HTMLResponse from starlette.responses import HTMLResponse
from lnbits.decorators import check_user_exists
from lnbits.core.models import Payment, User
from lnbits.core.crud import get_standalone_payment from lnbits.core.crud import get_standalone_payment
from lnbits.core.models import Payment, User
from lnbits.core.views.api import api_payment from lnbits.core.views.api import api_payment
from lnbits.decorators import check_user_exists
from . import offlineshop_ext, offlineshop_renderer from . import offlineshop_ext, offlineshop_renderer
from .models import Item
from .crud import get_item, get_shop from .crud import get_item, get_shop
from fastapi import Request, HTTPException from .models import Item
@offlineshop_ext.get("/", response_class=HTMLResponse) @offlineshop_ext.get("/", response_class=HTMLResponse)

View File

@ -19,7 +19,7 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None: async def on_invoice_paid(payment: Payment) -> None:
if "charge" != payment.extra.get("tag"): if payment.extra.get("tag") != "charge":
# not a charge invoice # not a charge invoice
return return

View File

@ -14,3 +14,41 @@ async def m001_initial(db):
); );
""" """
) )
async def m002_float_percent(db):
"""
Add float percent and migrates the existing data.
"""
await db.execute("ALTER TABLE splitpayments.targets RENAME TO splitpayments_old")
await db.execute(
"""
CREATE TABLE splitpayments.targets (
wallet TEXT NOT NULL,
source TEXT NOT NULL,
percent REAL NOT NULL CHECK (percent >= 0 AND percent <= 100),
alias TEXT,
UNIQUE (source, wallet)
);
"""
)
for row in [
list(row)
for row in await db.fetchall("SELECT * FROM splitpayments.splitpayments_old")
]:
await db.execute(
"""
INSERT INTO splitpayments.targets (
wallet,
source,
percent,
alias
)
VALUES (?, ?, ?, ?)
""",
(row[0], row[1], row[2], row[3]),
)
await db.execute("DROP TABLE splitpayments.splitpayments_old")

View File

@ -7,14 +7,14 @@ from pydantic import BaseModel
class Target(BaseModel): class Target(BaseModel):
wallet: str wallet: str
source: str source: str
percent: int percent: float
alias: Optional[str] alias: Optional[str]
class TargetPutList(BaseModel): class TargetPutList(BaseModel):
wallet: str = Query(...) wallet: str = Query(...)
alias: str = Query("") alias: str = Query("")
percent: int = Query(..., ge=1) percent: float = Query(..., ge=0.01)
class TargetPut(BaseModel): class TargetPut(BaseModel):

View File

@ -105,7 +105,7 @@ new Vue({
if (currentTotal > 100 && isPercent) { if (currentTotal > 100 && isPercent) {
let diff = (currentTotal - 100) / (100 - this.targets[index].percent) let diff = (currentTotal - 100) / (100 - this.targets[index].percent)
this.targets.forEach((target, t) => { this.targets.forEach((target, t) => {
if (t !== index) target.percent -= Math.round(diff * target.percent) if (t !== index) target.percent -= +(diff * target.percent).toFixed(2)
}) })
} }

View File

@ -22,7 +22,7 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None: async def on_invoice_paid(payment: Payment) -> None:
if "splitpayments" == payment.extra.get("tag") or payment.extra.get("splitted"): if payment.extra.get("tag") == "splitpayments" or payment.extra.get("splitted"):
# already splitted, ignore # already splitted, ignore
return return

View File

@ -58,14 +58,14 @@
></q-input> ></q-input>
</div> </div>
<q-row class="row justify-evenly q-pa-lg"> <div class="row justify-evenly q-pa-lg">
<q-col> <div>
<q-btn unelevated outline color="secondary" @click="clearTargets"> <q-btn unelevated outline color="secondary" @click="clearTargets">
Clear Clear
</q-btn> </q-btn>
</q-col> </div>
<q-col> <div>
<q-btn <q-btn
unelevated unelevated
color="primary" color="primary"
@ -74,8 +74,8 @@
> >
Save Targets Save Targets
</q-btn> </q-btn>
</q-col> </div>
</q-row> </div>
</q-form> </q-form>
</q-card-section> </q-card-section>
</q-card> </q-card>

View File

@ -1,5 +1,8 @@
import json
import httpx
from lnbits.extensions.subdomains.models import Domains from lnbits.extensions.subdomains.models import Domains
import httpx, json
async def cloudflare_create_subdomain( async def cloudflare_create_subdomain(

View File

@ -19,7 +19,7 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None: async def on_invoice_paid(payment: Payment) -> None:
if "lnsubdomain" != payment.extra.get("tag"): if payment.extra.get("tag") != "lnsubdomain":
# not an lnurlp invoice # not an lnurlp invoice
return return

View File

@ -16,8 +16,8 @@ def tpos_renderer():
from .tasks import wait_for_paid_invoices from .tasks import wait_for_paid_invoices
from .views_api import * # noqa
from .views import * # noqa from .views import * # noqa
from .views_api import * # noqa
def tpos_start(): def tpos_start():

View File

@ -20,7 +20,7 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None: async def on_invoice_paid(payment: Payment) -> None:
if "tpos" == payment.extra.get("tag") and payment.extra.get("tipSplitted"): if payment.extra.get("tag") == "tpos" and payment.extra.get("tipSplitted"):
# already splitted, ignore # already splitted, ignore
return return

View File

@ -8,10 +8,7 @@ from starlette.responses import HTMLResponse
from lnbits.core.models import User from lnbits.core.models import User
from lnbits.decorators import check_user_exists from lnbits.decorators import check_user_exists
from lnbits.settings import ( from lnbits.settings import LNBITS_CUSTOM_LOGO, LNBITS_SITE_TITLE
LNBITS_CUSTOM_LOGO,
LNBITS_SITE_TITLE,
)
from . import tpos_ext, tpos_renderer from . import tpos_ext, tpos_renderer
from .crud import get_tpos from .crud import get_tpos

View File

@ -2,9 +2,8 @@ from http import HTTPStatus
from fastapi import Query from fastapi import Query
from fastapi.params import Depends from fastapi.params import Depends
from starlette.exceptions import HTTPException
from loguru import logger from loguru import logger
from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user from lnbits.core.crud import get_user
from lnbits.core.services import create_invoice from lnbits.core.services import create_invoice

View File

@ -1,8 +1,8 @@
from sqlite3 import Row from sqlite3 import Row
from typing import Optional
from fastapi.param_functions import Query from fastapi.param_functions import Query
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional
class CreateUserData(BaseModel): class CreateUserData(BaseModel):

View File

@ -75,7 +75,7 @@ async def api_usermanager_activate_extension(
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="User does not exist." status_code=HTTPStatus.NOT_FOUND, detail="User does not exist."
) )
update_user_extension(user_id=userid, extension=extension, active=active) await update_user_extension(user_id=userid, extension=extension, active=active)
return {"extension": "updated"} return {"extension": "updated"}

View File

@ -1,15 +1,13 @@
import json import json
import traceback import traceback
import httpx
from datetime import datetime from datetime import datetime
from http import HTTPStatus from http import HTTPStatus
from loguru import logger import httpx
import shortuuid # type: ignore import shortuuid # type: ignore
from fastapi import HTTPException from fastapi import HTTPException
from fastapi.param_functions import Query from fastapi.param_functions import Query
from loguru import logger
from starlette.requests import Request from starlette.requests import Request
from starlette.responses import HTMLResponse # type: ignore from starlette.responses import HTMLResponse # type: ignore

View File

@ -53,6 +53,7 @@ new Vue({
rowsPerPage: 10 rowsPerPage: 10
} }
}, },
nfcTagWriting: false,
formDialog: { formDialog: {
show: false, show: false,
secondMultiplier: 'seconds', secondMultiplier: 'seconds',
@ -231,6 +232,42 @@ new Vue({
}) })
}) })
}, },
writeNfcTag: async function (lnurl) {
try {
if (typeof NDEFReader == 'undefined') {
throw {
toString: function () {
return 'NFC not supported on this device or browser.'
}
}
}
const ndef = new NDEFReader()
this.nfcTagWriting = true
this.$q.notify({
message: 'Tap your NFC tag to write the LNURL-withdraw link to it.'
})
await ndef.write({
records: [{recordType: 'url', data: 'lightning:' + lnurl, lang: 'en'}]
})
this.nfcTagWriting = false
this.$q.notify({
type: 'positive',
message: 'NFC tag written successfully.'
})
} catch (error) {
this.nfcTagWriting = false
this.$q.notify({
type: 'negative',
message: error
? error.toString()
: 'An unexpected error has occurred.'
})
}
},
exportCSV: function () { exportCSV: function () {
LNbits.utils.exportCSV(this.paywallsTable.columns, this.paywalls) LNbits.utils.exportCSV(this.paywallsTable.columns, this.paywalls)
} }

View File

@ -13,14 +13,22 @@
:value="this.here + '/?lightning={{lnurl }}'" :value="this.here + '/?lightning={{lnurl }}'"
:options="{width: 800}" :options="{width: 800}"
class="rounded-borders" class="rounded-borders"
></qrcode> >
</qrcode>
</q-responsive> </q-responsive>
</a> </a>
</div> </div>
<div class="row q-mt-lg"> <div class="row q-mt-lg q-gutter-sm">
<q-btn outline color="grey" @click="copyText('{{ lnurl }}')" <q-btn outline color="grey" @click="copyText('{{ lnurl }}')"
>Copy LNURL</q-btn >Copy LNURL</q-btn
> >
<q-btn
outline
color="grey"
icon="nfc"
@click="writeNfcTag(' {{ lnurl }} ')"
:disable="nfcTagWriting"
></q-btn>
</div> </div>
</q-card-section> </q-card-section>
</q-card> </q-card>
@ -51,7 +59,8 @@
mixins: [windowMixin], mixins: [windowMixin],
data: function () { data: function () {
return { return {
here: location.protocol + '//' + location.host here: location.protocol + '//' + location.host,
nfcTagWriting: false
} }
} }
}) })

View File

@ -369,6 +369,13 @@
@click="copyText(qrCodeDialog.data.withdraw_url, 'Link copied to clipboard!')" @click="copyText(qrCodeDialog.data.withdraw_url, 'Link copied to clipboard!')"
>Shareable link</q-btn >Shareable link</q-btn
> >
<q-btn
outline
color="grey"
icon="nfc"
@click="writeNfcTag(qrCodeDialog.data.lnurl)"
:disable="nfcTagWriting"
></q-btn>
<q-btn <q-btn
outline outline
color="grey" color="grey"

View File

@ -1,11 +1,11 @@
from typing import Optional, List, Callable
from functools import partial from functools import partial
from urllib.request import parse_http_list as _parse_list_header from typing import Callable, List, Optional
from urllib.parse import urlparse from urllib.parse import urlparse
from werkzeug.datastructures import Headers from urllib.request import parse_http_list as _parse_list_header
from quart import Request from quart import Request
from quart_trio.asgi import TrioASGIHTTPConnection from quart_trio.asgi import TrioASGIHTTPConnection
from werkzeug.datastructures import Headers
class ASGIProxyFix(TrioASGIHTTPConnection): class ASGIProxyFix(TrioASGIHTTPConnection):

View File

@ -51,6 +51,7 @@ LNBITS_THEME_OPTIONS: List[str] = env.list(
LNBITS_CUSTOM_LOGO = env.str("LNBITS_CUSTOM_LOGO", default="") LNBITS_CUSTOM_LOGO = env.str("LNBITS_CUSTOM_LOGO", default="")
WALLET = wallet_class() WALLET = wallet_class()
FAKE_WALLET = getattr(wallets_module, "FakeWallet")()
DEFAULT_WALLET_NAME = env.str("LNBITS_DEFAULT_WALLET_NAME", default="LNbits wallet") DEFAULT_WALLET_NAME = env.str("LNBITS_DEFAULT_WALLET_NAME", default="LNbits wallet")
PREFER_SECURE_URLS = env.bool("LNBITS_FORCE_HTTPS", default=True) PREFER_SECURE_URLS = env.bool("LNBITS_FORCE_HTTPS", default=True)

View File

@ -392,7 +392,7 @@ window.windowMixin = {
} }
if (window.extensions) { if (window.extensions) {
var user = this.g.user var user = this.g.user
this.g.extensions = Object.freeze( const extensions = Object.freeze(
window.extensions window.extensions
.map(function (data) { .map(function (data) {
return window.LNbits.map.extension(data) return window.LNbits.map.extension(data)
@ -413,9 +413,13 @@ window.windowMixin = {
return obj return obj
}) })
.sort(function (a, b) { .sort(function (a, b) {
return a.name > b.name const nameA = a.name.toUpperCase()
const nameB = b.name.toUpperCase()
return nameA < nameB ? -1 : nameA > nameB ? 1 : 0
}) })
) )
this.g.extensions = extensions
} }
} }
} }

View File

@ -1,22 +1,20 @@
import time
import asyncio import asyncio
import time
import traceback import traceback
from http import HTTPStatus from http import HTTPStatus
from typing import List, Callable from typing import Callable, List
from loguru import logger
from fastapi.exceptions import HTTPException from fastapi.exceptions import HTTPException
from loguru import logger
from lnbits.settings import WALLET
from lnbits.core.crud import ( from lnbits.core.crud import (
get_payments,
get_standalone_payment,
delete_expired_invoices, delete_expired_invoices,
get_balance_checks, get_balance_checks,
get_payments,
get_standalone_payment,
) )
from lnbits.core.services import redeem_lnurl_withdraw from lnbits.core.services import redeem_lnurl_withdraw
from lnbits.settings import WALLET
deferred_async: List[Callable] = [] deferred_async: List[Callable] = []

View File

@ -228,7 +228,6 @@
<script type="text/javascript"> <script type="text/javascript">
const themes = {{ LNBITS_THEME_OPTIONS | tojson }} const themes = {{ LNBITS_THEME_OPTIONS | tojson }}
const LNBITS_DENOMINATION = {{ LNBITS_DENOMINATION | tojson}} const LNBITS_DENOMINATION = {{ LNBITS_DENOMINATION | tojson}}
console.log(LNBITS_DENOMINATION)
if(themes && themes.length) { if(themes && themes.length) {
window.allowedThemes = themes.map(str => str.trim()) window.allowedThemes = themes.map(str => str.trim())
} }

View File

@ -1,9 +1,8 @@
import asyncio import asyncio
from typing import Callable, NamedTuple from typing import Callable, NamedTuple
from loguru import logger
import httpx import httpx
from loguru import logger
currencies = { currencies = {
"AED": "United Arab Emirates Dirham", "AED": "United Arab Emirates Dirham",
@ -282,7 +281,7 @@ async def btc_price(currency: str) -> float:
if not rates: if not rates:
return 9999999999 return 9999999999
elif len(rates) == 1: elif len(rates) == 1:
logger.warn("Could only fetch one Bitcoin price.") logger.warning("Could only fetch one Bitcoin price.")
return sum([rate for rate in rates]) / len(rates) return sum([rate for rate in rates]) / len(rates)

View File

@ -1,5 +1,5 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import NamedTuple, Optional, AsyncGenerator, Coroutine from typing import AsyncGenerator, Coroutine, NamedTuple, Optional
class StatusResponse(NamedTuple): class StatusResponse(NamedTuple):

View File

@ -5,10 +5,12 @@ except ImportError: # pragma: nocover
import asyncio import asyncio
import random import random
import time
from functools import partial, wraps from functools import partial, wraps
from os import getenv from os import getenv
from typing import AsyncGenerator, Optional from typing import AsyncGenerator, Optional
import time
from lnbits import bolt11 as lnbits_bolt11
from .base import ( from .base import (
InvoiceResponse, InvoiceResponse,
@ -18,7 +20,6 @@ from .base import (
Unsupported, Unsupported,
Wallet, Wallet,
) )
from lnbits import bolt11 as lnbits_bolt11
def async_wrap(func): def async_wrap(func):

View File

@ -5,9 +5,8 @@ import urllib.parse
from os import getenv from os import getenv
from typing import AsyncGenerator, Dict, Optional from typing import AsyncGenerator, Dict, Optional
from loguru import logger
import httpx import httpx
from loguru import logger
from websockets import connect from websockets import connect
from websockets.exceptions import ( from websockets.exceptions import (
ConnectionClosed, ConnectionClosed,

View File

@ -1,23 +1,27 @@
import asyncio import asyncio
import hashlib
from os import getenv
from datetime import datetime
from typing import Optional, Dict, AsyncGenerator
import random import random
from datetime import datetime
from os import getenv
from typing import AsyncGenerator, Dict, Optional
from environs import Env # type: ignore
from loguru import logger from loguru import logger
from lnbits.helpers import urlsafe_short_hash from lnbits.helpers import urlsafe_short_hash
import hashlib
from ..bolt11 import encode, decode from ..bolt11 import decode, encode
from .base import ( from .base import (
StatusResponse,
InvoiceResponse, InvoiceResponse,
PaymentResponse, PaymentResponse,
PaymentStatus, PaymentStatus,
StatusResponse,
Wallet, Wallet,
) )
env = Env()
env.read_env()
class FakeWallet(Wallet): class FakeWallet(Wallet):
async def status(self) -> StatusResponse: async def status(self) -> StatusResponse:
@ -32,7 +36,9 @@ class FakeWallet(Wallet):
memo: Optional[str] = None, memo: Optional[str] = None,
description_hash: Optional[bytes] = None, description_hash: Optional[bytes] = None,
) -> InvoiceResponse: ) -> InvoiceResponse:
secret = getenv("FAKE_WALLET_SECRET") # we set a default secret since FakeWallet is used for internal=True invoices
# and the user might not have configured a secret yet
secret = env.str("FAKE_WALLET_SECTRET", default="ToTheMoon1")
data: Dict = { data: Dict = {
"out": False, "out": False,
"amount": amount, "amount": amount,
@ -85,10 +91,10 @@ class FakeWallet(Wallet):
) )
async def get_invoice_status(self, checking_id: str) -> PaymentStatus: async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
return PaymentStatus(False) return PaymentStatus(None)
async def get_payment_status(self, checking_id: str) -> PaymentStatus: async def get_payment_status(self, checking_id: str) -> PaymentStatus:
return PaymentStatus(False) return PaymentStatus(None)
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
self.queue = asyncio.Queue(0) self.queue = asyncio.Queue(0)

View File

@ -1,16 +1,16 @@
import asyncio import asyncio
import json import json
import httpx
from os import getenv from os import getenv
from typing import Optional, Dict, AsyncGenerator from typing import AsyncGenerator, Dict, Optional
import httpx
from loguru import logger from loguru import logger
from .base import ( from .base import (
StatusResponse,
InvoiceResponse, InvoiceResponse,
PaymentResponse, PaymentResponse,
PaymentStatus, PaymentStatus,
StatusResponse,
Wallet, Wallet,
) )

View File

@ -2,11 +2,11 @@
# Generated by the protocol buffer compiler. DO NOT EDIT! # Generated by the protocol buffer compiler. DO NOT EDIT!
# source: lightning.proto # source: lightning.proto
"""Generated protocol buffer code.""" """Generated protocol buffer code."""
from google.protobuf.internal import enum_type_wrapper
from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message from google.protobuf import message as _message
from google.protobuf import reflection as _reflection from google.protobuf import reflection as _reflection
from google.protobuf import symbol_database as _symbol_database from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import enum_type_wrapper
# @@protoc_insertion_point(imports) # @@protoc_insertion_point(imports)

Some files were not shown because too many files have changed in this diff Show More