Merge branch 'main' into SCRUB

This commit is contained in:
ben 2022-07-27 20:21:37 +01:00
commit b60ad6974c
221 changed files with 5843 additions and 1649 deletions

21
.dockerignore Normal file
View file

@ -0,0 +1,21 @@
.git
data
docker
docs
tests
venv
tools
*.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,10 +1,8 @@
QUART_APP=lnbits.app:create_app()
QUART_ENV=development
QUART_DEBUG=true
HOST=127.0.0.1 HOST=127.0.0.1
PORT=5000 PORT=5000
DEBUG=false
LNBITS_ALLOWED_USERS="" LNBITS_ALLOWED_USERS=""
LNBITS_ADMIN_USERS="" LNBITS_ADMIN_USERS=""
# Extensions only admin can access # Extensions only admin can access
@ -34,6 +32,7 @@ LNBITS_SITE_TAGLINE="free and open-source lightning wallet"
LNBITS_SITE_DESCRIPTION="Some description about your service, will display if title is not 'LNbits'" LNBITS_SITE_DESCRIPTION="Some description about your service, will display if title is not 'LNbits'"
# Choose from mint, flamingo, freedom, salvador, autumn, monochrome, classic # Choose from mint, flamingo, freedom, salvador, autumn, monochrome, classic
LNBITS_THEME_OPTIONS="classic, bitcoin, freedom, mint, autumn, monochrome, salvador" LNBITS_THEME_OPTIONS="classic, bitcoin, freedom, mint, autumn, monochrome, salvador"
# LNBITS_CUSTOM_LOGO="https://lnbits.com/assets/images/logo/logo.svg"
# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, # Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet,
# LndRestWallet, CLightningWallet, LNbitsWallet, SparkWallet, FakeWallet, EclairWallet # LndRestWallet, CLightningWallet, LNbitsWallet, SparkWallet, FakeWallet, EclairWallet

2
.github/FUNDING.yml vendored
View file

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

View file

@ -2,9 +2,9 @@ name: codeql
on: on:
push: push:
branches: [master, ] branches: [main, ]
pull_request: pull_request:
branches: [master] branches: [main]
schedule: schedule:
- cron: '0 12 * * 5' - cron: '0 12 * * 5'
@ -19,10 +19,10 @@ jobs:
- run: git checkout HEAD^2 - run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }} if: ${{ github.event_name == 'pull_request' }}
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v1 uses: github/codeql-action/init@v2
with: with:
languages: javascript, python languages: javascript, python
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v1 uses: github/codeql-action/autobuild@v2
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1 uses: github/codeql-action/analyze@v2

View file

@ -2,9 +2,9 @@ name: formatting
on: on:
push: push:
branches: [ master ] branches: [ main ]
pull_request: pull_request:
branches: [ master ] branches: [ main ]
jobs: jobs:
black: black:
@ -15,9 +15,22 @@ 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:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- run: npm install - uses: actions/setup-node@v3
- run: make checkprettier - run: sudo apt-get install python3-venv
- run: python3 -m venv venv
- run: npm install prettier
- run: ./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

51
.github/workflows/migrations.yml vendored Normal file
View file

@ -0,0 +1,51 @@
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
sudo apt install unzip
- name: Run migrations
run: |
rm -rf ./data
mkdir -p ./data
export LNBITS_DATA_FOLDER="./data"
unzip tests/data/mock_data.zip -d ./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

View file

@ -5,6 +5,7 @@ on: [push, pull_request]
jobs: jobs:
check: check:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ 'false' == 'true' }} # skip mypy for now
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- uses: jpetrucciani/mypy-check@master - uses: jpetrucciani/mypy-check@master

85
.github/workflows/regtest.yml vendored Normal file
View file

@ -0,0 +1,85 @@
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
chmod +x ./tests
./tests
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
chmod +x ./tests
./tests
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,15 +3,15 @@ name: tests
on: [push, pull_request] on: [push, pull_request]
jobs: jobs:
unit: venv-sqlite:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: [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
@ -22,37 +22,67 @@ jobs:
python -m venv ${{ env.VIRTUAL_ENV }} python -m venv ${{ env.VIRTUAL_ENV }}
./venv/bin/python -m pip install --upgrade pip ./venv/bin/python -m pip install --upgrade pip
./venv/bin/pip install -r requirements.txt ./venv/bin/pip install -r requirements.txt
./venv/bin/pip install pytest pytest-asyncio requests trio mock ./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock
- name: Run tests - name: Run tests
run: make test run: make test
# build: venv-postgres:
# runs-on: ubuntu-latest runs-on: ubuntu-latest
# strategy: services:
# matrix: postgres:
# python-version: [3.7, 3.8] image: postgres:latest
# steps: env:
# - uses: actions/checkout@v2 POSTGRES_USER: postgres
# - name: Set up Python ${{ matrix.python-version }} POSTGRES_PASSWORD: postgres
# uses: actions/setup-python@v1 POSTGRES_DB: postgres
# with: ports:
# python-version: ${{ matrix.python-version }} # maps tcp port 5432 on service container to the host
# - name: Install dependencies - 5432:5432
# run: | options: >-
# python -m pip install --upgrade pip --health-cmd pg_isready
# pip install -r requirements.txt --health-interval 10s
# - name: Test with pytest --health-timeout 5s
# env: --health-retries 5
# LNBITS_BACKEND_WALLET_CLASS: LNPayWallet strategy:
# LNBITS_FORCE_HTTPS: 0 matrix:
# LNPAY_API_ENDPOINT: https://api.lnpay.co/v1/ python-version: [3.8]
# LNPAY_API_KEY: sak_gG5pSFZhFgOLHm26a8hcWvXKt98yd steps:
# LNPAY_ADMIN_KEY: waka_HqWfOoNE0TPqmQHSYErbF4n9 - uses: actions/checkout@v2
# LNPAY_INVOICE_KEY: waki_ZqFEbhrTyopuPlOZButZUw - name: Set up Python ${{ matrix.python-version }}
# LNPAY_READ_KEY: wakr_6IyTaNrvSeu3jbojSWt4ou6h uses: actions/setup-python@v2
# run: | with:
# pip install pytest pytest-cov python-version: ${{ matrix.python-version }}
# pytest --cov=lnbits --cov-report=xml - name: Install dependencies
# - name: Upload coverage to Codecov env:
# uses: codecov/codecov-action@v1 VIRTUAL_ENV: ./venv
# with: PATH: ${{ env.VIRTUAL_ENV }}/bin:${{ env.PATH }}
# file: ./coverage.xml 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
env:
LNBITS_DATABASE_URL: postgres://postgres:postgres@0.0.0.0:5432/postgres
run: make test
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
pipenv-sqlite:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.7, 3.8, 3.9]
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
run: |
pip install pipenv
pipenv install --dev
pipenv install importlib-metadata
- name: Run tests
run: make test-pipenv

7
.gitignore vendored
View file

@ -15,7 +15,7 @@ __pycache__
.webassets-cache .webassets-cache
htmlcov htmlcov
test-reports test-reports
tests/data tests/data/*.sqlite3
*.swo *.swo
*.swp *.swp
@ -31,5 +31,10 @@ venv
__bundle__ __bundle__
coverage.xml
node_modules node_modules
lnbits/static/bundle.* lnbits/static/bundle.*
docker
# Nix
*result*

View file

@ -8,7 +8,7 @@ ENV PATH="$VIRTUAL_ENV/bin:$PATH"
# Install build deps # Install build deps
RUN apt-get update RUN apt-get update
RUN apt-get install -y --no-install-recommends build-essential pkg-config RUN apt-get install -y --no-install-recommends build-essential pkg-config libpq-dev
RUN python -m pip install --upgrade pip RUN python -m pip install --upgrade pip
RUN pip install wheel RUN pip install wheel

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
@ -30,8 +36,26 @@ requirements.txt: Pipfile.lock
cat Pipfile.lock | jq -r '.default | map_values(.version) | to_entries | map("\(.key)\(.value)") | join("\n")' > requirements.txt cat Pipfile.lock | jq -r '.default | map_values(.version) | to_entries | map("\(.key)\(.value)") | join("\n")' > requirements.txt
test: test:
rm -rf ./tests/data mkdir -p ./tests/data
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
FAKE_WALLET_SECRET="ToTheMoon1" \
LNBITS_DATA_FOLDER="./tests/data" \
PYTHONUNBUFFERED=1 \
./venv/bin/pytest --durations=1 -s --cov=lnbits --cov-report=xml tests
test-real-wallet:
mkdir -p ./tests/data mkdir -p ./tests/data
LNBITS_DATA_FOLDER="./tests/data" \ LNBITS_DATA_FOLDER="./tests/data" \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
./venv/bin/pytest -s ./venv/bin/pytest --durations=1 -s --cov=lnbits --cov-report=xml tests
test-pipenv:
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:
# LNBITS_DATABASE_URL=postgres://postgres:postgres@0.0.0.0:5432/postgres

13
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 = "*"
@ -12,6 +12,7 @@ cerberus = "*"
ecdsa = "*" ecdsa = "*"
environs = "*" environs = "*"
lnurl = "==0.3.6" lnurl = "==0.3.6"
loguru = "*"
pyscss = "*" pyscss = "*"
shortuuid = "*" shortuuid = "*"
typing-extensions = "*" typing-extensions = "*"
@ -27,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"
}
}
}

110
build.py Normal file
View file

@ -0,0 +1,110 @@
import warnings
import subprocess
import glob
import os
from os import path
from typing import Any, List, NamedTuple, Optional
from pathlib import Path
LNBITS_PATH = path.dirname(path.realpath(__file__)) + "/lnbits"
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)
def url_for_vendored(abspath: str) -> str:
return "/" + os.path.relpath(abspath, LNBITS_PATH)
def transpile_scss():
with warnings.catch_warnings():
warnings.simplefilter("ignore")
from scss.compiler import compile_string # type: ignore
with open(os.path.join(LNBITS_PATH, "static/scss/base.scss")) as scss:
with open(os.path.join(LNBITS_PATH, "static/css/base.css"), "w") as css:
css.write(compile_string(scss.read()))
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")),
]:
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)
def build():
transpile_scss()
bundle_vendored()
# root = Path("lnbits/static/foo")
# root.mkdir(parents=True)
# root.joinpath("example.css").write_text("")
if __name__ == "__main__":
build()
#def build(setup_kwargs):
# """Build """
# transpile_scss()
# bundle_vendored()
# subprocess.run(["ls", "-la", "./lnbits/static"])

View file

@ -17,7 +17,7 @@ Tests
This project has unit tests that help prevent regressions. Before you can run the tests, you must install a few dependencies: This project has unit tests that help prevent regressions. Before you can run the tests, you must install a few dependencies:
```bash ```bash
./venv/bin/pip install pytest pytest-asyncio requests trio mock ./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock
``` ```
Then to run the tests: Then to run the tests:

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,125 @@ 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 four package managers, `poetry`, `pipenv`, `venv` 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).
## Option 1: poetry
```sh
git clone https://github.com/lnbits/lnbits-legend.git
cd lnbits-legend/
curl -sSL https://install.python-poetry.org | python3 -
poetry install
# You may need to install python 3.9, update your python following this guide https://linuxize.com/post/how-to-install-python-3-9-on-ubuntu-20-04/
mkdir data && cp .env.example .env
```
#### Running the server
```sh
poetry run lnbits
# To change port/host pass 'poetry run lnbits --port 9000 --host 0.0.0.0'
```
## Option 2: pipenv
```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 3: venv
```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`.
## Option 4: Nix
```sh
git clone https://github.com/lnbits/lnbits-legend.git
cd lnbits-legend/
# Install nix, modern debian distros usually already include
sh <(curl -L https://nixos.org/nix/install) --daemon
nix build .#lnbits
mkdir data
```
#### Running the server
```sh
# .env variables are currently passed when running
LNBITS_DATA_FOLDER=data LNBITS_BACKEND_WALLET_CLASS=LNbitsWallet LNBITS_ENDPOINT=https://legend.lnbits.com LNBITS_KEY=7b1a78d6c78f48b09a202f2dcb2d22eb ./result/bin/lnbits --port 9000
```
### 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 +139,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,29 +158,35 @@ 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.
```sh
# STOP LNbits
# on the LNBits folder, locate and edit 'conv.py' with the relevant credentials
python3 conv.py
# add the database connection string to .env 'nano .env' LNBITS_DATABASE_URL=
# postgres://<user>:<password>@<host>/<database> - alter line bellow with your user, password and db name
LNBITS_DATABASE_URL="postgres://postgres:postgres@localhost/lnbits"
# save and exit
```
Hopefully, everything works and get migrated... Launch LNbits again and check if everything is working properly.
# Additional guides # Additional guides
### LNbits as a systemd service ## 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
# STOP LNbits
# add the database connection string to .env 'nano .env' LNBITS_DATABASE_URL=
# postgres://<user>:<password>@<host>/<database> - alter line bellow with your user, password and db name
LNBITS_DATABASE_URL="postgres://postgres:postgres@localhost/lnbits"
# save and exit
# START LNbits
# STOP LNbits
# on the LNBits folder, locate and edit 'tools/conv.py' with the relevant credentials
python3 tools/conv.py
```
Hopefully, everything works and get migrated... Launch LNbits again and check if everything is working properly.
## 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:
@ -78,17 +196,23 @@ Systemd is great for taking care of your LNbits instance. It will start it on bo
[Unit] [Unit]
Description=LNbits Description=LNbits
#Wants=lnd.service # you can uncomment these lines if you know what you're doing # you can uncomment these lines if you know what you're doing
#After=lnd.service # it will make sure that lnbits starts after lnd (replace with your own backend service) # it will make sure that lnbits starts after lnd (replace with your own backend service)
#Wants=lnd.service
#After=lnd.service
[Service] [Service]
WorkingDirectory=/home/bitcoin/lnbits # replace with the absolute path of your lnbits installation # replace with the absolute path of your lnbits installation
ExecStart=/home/bitcoin/lnbits/venv/bin/uvicorn lnbits.__main__:app --port 5000 # same here WorkingDirectory=/home/bitcoin/lnbits
User=bitcoin # replace with the user that you're running lnbits on # same here
ExecStart=/home/bitcoin/lnbits/venv/bin/uvicorn lnbits.__main__:app --port 5000
# replace with the user that you're running lnbits on
User=bitcoin
Restart=always Restart=always
TimeoutSec=120 TimeoutSec=120
RestartSec=30 RestartSec=30
Environment=PYTHONUNBUFFERED=1 # this makes sure that you receive logs in real time # this makes sure that you receive logs in real time
Environment=PYTHONUNBUFFERED=1
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target
@ -101,11 +225,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:
@ -137,9 +290,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

@ -17,7 +17,6 @@ A backend wallet can be configured using the following LNbits environment variab
### CLightning ### CLightning
Using this wallet requires the installation of the `pylightning` Python package. Using this wallet requires the installation of the `pylightning` Python package.
If you want to use LNURLp you should use SparkWallet because of an issue with description_hash and CLightning.
- `LNBITS_BACKEND_WALLET_CLASS`: **CLightningWallet** - `LNBITS_BACKEND_WALLET_CLASS`: **CLightningWallet**
- `CLIGHTNING_RPC`: /file/path/lightning-rpc - `CLIGHTNING_RPC`: /file/path/lightning-rpc

77
flake.lock generated Normal file
View file

@ -0,0 +1,77 @@
{
"nodes": {
"flake-utils": {
"locked": {
"lastModified": 1656928814,
"narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1657114324,
"narHash": "sha256-fWuaUNXrHcz/ciHRHlcSO92dvV3EVS0GJQUSBO5JIB4=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "a5c867d9fe9e4380452628e8f171c26b69fa9d3d",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"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"
},
"locked": {
"lastModified": 1657149754,
"narHash": "sha256-iSnZoqwNDDVoO175whSuvl4sS9lAb/2zZ3Sa4ywo970=",
"owner": "nix-community",
"repo": "poetry2nix",
"rev": "fc1930e011dea149db81863aac22fe701f36f1b5",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "poetry2nix",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"poetry2nix": "poetry2nix"
}
}
},
"root": "root",
"version": 7
}

55
flake.nix Normal file
View file

@ -0,0 +1,55 @@
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
poetry2nix.url = "github:nix-community/poetry2nix";
};
outputs = { self, nixpkgs, poetry2nix }@inputs:
let
supportedSystems = [ "x86_64-linux" "aarch64-linux" ];
forSystems = systems: f:
nixpkgs.lib.genAttrs systems
(system: f system (import nixpkgs { inherit system; overlays = [ poetry2nix.overlay self.overlays.default ]; }));
forAllSystems = forSystems supportedSystems;
projectName = "lnbits";
in
{
devShells = forAllSystems (system: pkgs: {
default = pkgs.mkShell {
buildInputs = with pkgs; [
nodePackages.prettier
];
};
});
overlays = {
default = final: prev: {
${projectName} = self.packages.${final.hostPlatform.system}.${projectName};
};
};
packages = forAllSystems (system: pkgs: {
default = self.packages.${system}.${projectName};
${projectName} = pkgs.poetry2nix.mkPoetryApplication {
projectDir = ./.;
python = pkgs.python39;
};
});
nixosModules = {
default = { pkgs, lib, config, ... }: {
imports = [ "${./nix/modules/${projectName}-service.nix}" ];
nixpkgs.overlays = [ self.overlays.default ];
};
};
checks = forAllSystems (system: pkgs:
let
vmTests = import ./nix/tests {
makeTest = (import (nixpkgs + "/nixos/lib/testing-python.nix") { inherit system; }).makeTest;
inherit inputs pkgs;
};
in
pkgs.lib.optionalAttrs pkgs.stdenv.isLinux vmTests # vmTests can only be ran on Linux, so append them only if on Linux.
//
{
# Other checks here...
}
);
};
}

View file

@ -1,36 +1,38 @@
import asyncio import asyncio
import uvloop import uvloop
from loguru import logger
from starlette.requests import Request from starlette.requests import Request
from .commands import bundle_vendored, migrate_databases, transpile_scss 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,
PORT, PORT,
SERVICE_FEE,
WALLET, WALLET,
) )
uvloop.install() uvloop.install()
asyncio.create_task(migrate_databases()) asyncio.create_task(migrate_databases())
transpile_scss()
bundle_vendored()
from .app import create_app from .app import create_app
app = create_app() app = create_app()
print( logger.info("Starting LNbits")
f"""Starting LNbits with logger.info(f"Host: {HOST}")
- git version: {LNBITS_COMMIT} logger.info(f"Port: {PORT}")
- site title: {LNBITS_SITE_TITLE} logger.info(f"Debug: {DEBUG}")
- debug: {DEBUG} logger.info(f"Site title: {LNBITS_SITE_TITLE}")
- data folder: {LNBITS_DATA_FOLDER} logger.info(f"Funding source: {WALLET.__class__.__name__}")
- funding source: {WALLET.__class__.__name__} logger.info(
- service fee: {SERVICE_FEE} f"Database: {'PostgreSQL' if LNBITS_DATABASE_URL and LNBITS_DATABASE_URL.startswith('postgres://') else 'CockroachDB' if LNBITS_DATABASE_URL and LNBITS_DATABASE_URL.startswith('cockroachdb://') else 'SQLite'}"
"""
) )
logger.info(f"Data folder: {LNBITS_DATA_FOLDER}")
logger.info(f"Git version: {LNBITS_COMMIT}")
# logger.info(f"Service fee: {SERVICE_FEE}")

View file

@ -1,14 +1,18 @@
import asyncio import asyncio
import importlib import importlib
import logging
import sys import sys
import traceback import traceback
import warnings import warnings
from http import HTTPStatus
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware 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.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
@ -39,10 +43,21 @@ def create_app(config_object="lnbits.settings") -> FastAPI:
"""Create application factory. """Create application factory.
:param config_object: The configuration object to use. :param config_object: The configuration object to use.
""" """
app = FastAPI() configure_logger()
app.mount("/static", StaticFiles(directory="lnbits/static"), name="static")
app = FastAPI(
title="LNbits API",
description="API for LNbits, the free and open source bitcoin wallet and accounts system with plugins.",
license_info={
"name": "MIT License",
"url": "https://raw.githubusercontent.com/lnbits/lnbits-legend/main/LICENSE",
},
)
app.mount("/static", StaticFiles(packages=[("lnbits", "static")]), name="static")
app.mount( app.mount(
"/core/static", StaticFiles(directory="lnbits/core/static"), name="core_static" "/core/static",
StaticFiles(packages=[("lnbits.core", "static")]),
name="core_static",
) )
origins = ["*"] origins = ["*"]
@ -58,15 +73,19 @@ def create_app(config_object="lnbits.settings") -> FastAPI:
async def validation_exception_handler( async def validation_exception_handler(
request: Request, exc: RequestValidationError request: Request, exc: RequestValidationError
): ):
# Only the browser sends "text/html" request
# not fail proof, but everything else get's a JSON response
if "text/html" in request.headers["accept"]:
return template_renderer().TemplateResponse( return template_renderer().TemplateResponse(
"error.html", "error.html",
{"request": request, "err": f"`{exc.errors()}` is not a valid UUID."}, {"request": request, "err": f"{exc.errors()} is not a valid UUID."},
) )
# return HTMLResponse( return JSONResponse(
# status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=HTTPStatus.NO_CONTENT,
# content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}), content={"detail": exc.errors()},
# ) )
app.add_middleware(GZipMiddleware, minimum_size=1000) app.add_middleware(GZipMiddleware, minimum_size=1000)
# app.add_middleware(ASGIProxyFix) # app.add_middleware(ASGIProxyFix)
@ -88,14 +107,14 @@ def check_funding_source(app: FastAPI) -> None:
error_message, balance = await WALLET.status() error_message, balance = await WALLET.status()
if not error_message: if not error_message:
break break
warnings.warn( logger.error(
f" × The backend for {WALLET.__class__.__name__} isn't working properly: '{error_message}'", f"The backend for {WALLET.__class__.__name__} isn't working properly: '{error_message}'",
RuntimeWarning, RuntimeWarning,
) )
print("Retrying connection to backend in 5 seconds...") logger.info("Retrying connection to backend in 5 seconds...")
await asyncio.sleep(5) await asyncio.sleep(5)
print( logger.info(
f" ✔️ {WALLET.__class__.__name__} seems to be connected and with a balance of {balance} msat." f"✔️ Backend {WALLET.__class__.__name__} connected and with a balance of {balance} msat."
) )
@ -118,9 +137,10 @@ def register_routes(app: FastAPI) -> None:
for s in ext_statics: for s in ext_statics:
app.mount(s["path"], s["app"], s["name"]) app.mount(s["path"], s["app"], s["name"])
logger.trace(f"adding route for extension {ext_module}")
app.include_router(ext_route) app.include_router(ext_route)
except Exception as e: except Exception as e:
print(str(e)) logger.error(str(e))
raise ImportError( raise ImportError(
f"Please make sure that the extension `{ext.code}` follows conventions." f"Please make sure that the extension `{ext.code}` follows conventions."
) )
@ -167,10 +187,53 @@ def register_async_tasks(app):
def register_exception_handlers(app: FastAPI): def register_exception_handlers(app: FastAPI):
@app.exception_handler(Exception) @app.exception_handler(Exception)
async def basic_error(request: Request, err): async def basic_error(request: Request, err):
print("handled error", traceback.format_exc()) logger.error("handled error", traceback.format_exc())
logger.error("ERROR:", err)
etype, _, tb = sys.exc_info() etype, _, tb = sys.exc_info()
traceback.print_exception(etype, err, tb) traceback.print_exception(etype, err, tb)
exc = traceback.format_exc() exc = traceback.format_exc()
if "text/html" in request.headers["accept"]:
return template_renderer().TemplateResponse( return template_renderer().TemplateResponse(
"error.html", {"request": request, "err": err} "error.html", {"request": request, "err": err}
) )
return JSONResponse(
status_code=HTTPStatus.NO_CONTENT,
content={"detail": err},
)
def configure_logger() -> None:
logger.remove()
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:
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
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):
@ -165,7 +166,7 @@ def lnencode(addr, privkey):
if addr.amount: if addr.amount:
amount = Decimal(str(addr.amount)) amount = Decimal(str(addr.amount))
# We can only send down to millisatoshi. # We can only send down to millisatoshi.
if amount * 10 ** 12 % 10: if amount * 10**12 % 10:
raise ValueError( raise ValueError(
"Cannot encode {}: too many decimal places".format(addr.amount) "Cannot encode {}: too many decimal places".format(addr.amount)
) )
@ -270,7 +271,7 @@ class LnAddr(object):
def shorten_amount(amount): def shorten_amount(amount):
"""Given an amount in bitcoin, shorten it""" """Given an amount in bitcoin, shorten it"""
# Convert to pico initially # Convert to pico initially
amount = int(amount * 10 ** 12) amount = int(amount * 10**12)
units = ["p", "n", "u", "m", ""] units = ["p", "n", "u", "m", ""]
for unit in units: for unit in units:
if amount % 1000 == 0: if amount % 1000 == 0:
@ -289,7 +290,7 @@ def _unshorten_amount(amount: str) -> int:
# * `u` (micro): multiply by 0.000001 # * `u` (micro): multiply by 0.000001
# * `n` (nano): multiply by 0.000000001 # * `n` (nano): multiply by 0.000000001
# * `p` (pico): multiply by 0.000000000001 # * `p` (pico): multiply by 0.000000000001
units = {"p": 10 ** 12, "n": 10 ** 9, "u": 10 ** 6, "m": 10 ** 3} units = {"p": 10**12, "n": 10**9, "u": 10**6, "m": 10**3}
unit = str(amount)[-1] unit = str(amount)[-1]
# BOLT #11: # BOLT #11:
@ -348,9 +349,9 @@ def _trim_to_bytes(barr):
def _readable_scid(short_channel_id: int) -> str: def _readable_scid(short_channel_id: int) -> str:
return "{blockheight}x{transactionindex}x{outputindex}".format( return "{blockheight}x{transactionindex}x{outputindex}".format(
blockheight=((short_channel_id >> 40) & 0xffffff), blockheight=((short_channel_id >> 40) & 0xFFFFFF),
transactionindex=((short_channel_id >> 16) & 0xffffff), transactionindex=((short_channel_id >> 16) & 0xFFFFFF),
outputindex=(short_channel_id & 0xffff), outputindex=(short_channel_id & 0xFFFF),
) )

View file

@ -1,16 +1,19 @@
import asyncio import asyncio
import warnings
import click
import importlib import importlib
import re
import os import os
import re
import warnings
from .db import SQLITE, POSTGRES, COCKROACH import click
from .core import db as core_db, migrations as core_migrations from loguru import logger
from .core import db as core_db
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
@ -69,7 +72,7 @@ async def migrate_databases():
if match: if match:
version = int(match.group(1)) version = int(match.group(1))
if version > current_versions.get(db_name, 0): if version > current_versions.get(db_name, 0):
print(f"running migration {db_name}.{version}") logger.debug(f"running migration {db_name}.{version}")
await migrate(db) await migrate(db)
if db.schema == None: if db.schema == None:
@ -110,4 +113,4 @@ async def migrate_databases():
async with ext_db.connect() as ext_conn: async with ext_db.connect() as ext_conn:
await run_migration(ext_conn, ext_migrations) await run_migration(ext_conn, ext_migrations)
print(" ✔️ All migrations done.") logger.info("✔️ All migrations done.")

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
# -------- # --------
@ -180,16 +180,28 @@ async def get_wallet_for_key(
async def get_standalone_payment( async def get_standalone_payment(
checking_id_or_hash: str, conn: Optional[Connection] = None checking_id_or_hash: str,
conn: Optional[Connection] = None,
incoming: Optional[bool] = False,
wallet_id: Optional[str] = None,
) -> Optional[Payment]: ) -> Optional[Payment]:
clause: str = "checking_id = ? OR hash = ?"
values = [checking_id_or_hash, checking_id_or_hash]
if incoming:
clause = f"({clause}) AND amount > 0"
if wallet_id:
clause = f"({clause}) AND wallet = ?"
values.append(wallet_id)
row = await (conn or db).fetchone( row = await (conn or db).fetchone(
""" f"""
SELECT * SELECT *
FROM apipayments FROM apipayments
WHERE checking_id = ? OR hash = ? WHERE {clause}
LIMIT 1 LIMIT 1
""", """,
(checking_id_or_hash, checking_id_or_hash), tuple(values),
) )
return Payment.from_row(row) if row else None return Payment.from_row(row) if row else None

View file

@ -1,12 +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 lnbits.helpers import url_for
from lnbits.settings import WALLET from lnbits.settings import WALLET
@ -142,10 +145,12 @@ class Payment(BaseModel):
status = await WALLET.get_invoice_status(self.checking_id) status = await WALLET.get_invoice_status(self.checking_id)
if self.is_out and status.failed: if self.is_out and status.failed:
print(f" - deleting outgoing failed payment {self.checking_id}: {status}") logger.info(
f" - deleting outgoing failed payment {self.checking_id}: {status}"
)
await self.delete() await self.delete()
elif not status.pending: elif not status.pending:
print( logger.info(
f" - marking '{'in' if self.is_in else 'out'}' {self.checking_id} as not pending anymore: {status}" f" - marking '{'in' if self.is_in else 'out'}' {self.checking_id} as not pending anymore: {status}"
) )
await self.set_pending(status.pending) await self.set_pending(status.pending)

View file

@ -6,14 +6,22 @@ from typing import Dict, Optional, Tuple
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
@ -48,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)
@ -120,6 +132,7 @@ async def pay_invoice(
# check_internal() returns the checking_id of the invoice we're waiting for # check_internal() returns the checking_id of the invoice we're waiting for
internal_checking_id = await check_internal(invoice.payment_hash, conn=conn) internal_checking_id = await check_internal(invoice.payment_hash, conn=conn)
if internal_checking_id: if internal_checking_id:
logger.debug(f"creating temporary internal payment with id {internal_id}")
# create a new payment from this wallet # create a new payment from this wallet
await create_payment( await create_payment(
checking_id=internal_id, checking_id=internal_id,
@ -129,6 +142,7 @@ async def pay_invoice(
**payment_kwargs, **payment_kwargs,
) )
else: else:
logger.debug(f"creating temporary payment with id {temp_id}")
# create a temporary payment here so we can check if # create a temporary payment here so we can check if
# the balance is enough in the next step # the balance is enough in the next step
await create_payment( await create_payment(
@ -142,6 +156,7 @@ async def pay_invoice(
wallet = await get_wallet(wallet_id, conn=conn) wallet = await get_wallet(wallet_id, conn=conn)
assert wallet assert wallet
if wallet.balance_msat < 0: 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: if not internal_checking_id and wallet.balance_msat > -fee_reserve_msat:
raise PaymentFailure( raise PaymentFailure(
f"You must reserve at least 1% ({round(fee_reserve_msat/1000)} sat) to cover potential routing fees." f"You must reserve at least 1% ({round(fee_reserve_msat/1000)} sat) to cover potential routing fees."
@ -149,6 +164,7 @@ async def pay_invoice(
raise PermissionError("Insufficient balance.") raise PermissionError("Insufficient balance.")
if internal_checking_id: if internal_checking_id:
logger.debug(f"marking temporary payment as not pending {internal_checking_id}")
# mark the invoice from the other side as not pending anymore # mark the invoice from the other side as not pending anymore
# so the other side only has access to his new money when we are sure # so the other side only has access to his new money when we are sure
# the payer has enough to deduct from # the payer has enough to deduct from
@ -163,11 +179,14 @@ async def pay_invoice(
await internal_invoice_queue.put(internal_checking_id) await internal_invoice_queue.put(internal_checking_id)
else: else:
logger.debug(f"backend: sending payment {temp_id}")
# actually pay the external invoice # actually pay the external invoice
payment: PaymentResponse = await WALLET.pay_invoice( payment: PaymentResponse = await WALLET.pay_invoice(
payment_request, fee_reserve_msat payment_request, fee_reserve_msat
) )
logger.debug(f"backend: pay_invoice finished {temp_id}")
if payment.checking_id: if payment.checking_id:
logger.debug(f"creating final payment {payment.checking_id}")
async with db.connect() as conn: async with db.connect() as conn:
await create_payment( await create_payment(
checking_id=payment.checking_id, checking_id=payment.checking_id,
@ -177,15 +196,18 @@ async def pay_invoice(
conn=conn, conn=conn,
**payment_kwargs, **payment_kwargs,
) )
logger.debug(f"deleting temporary payment {temp_id}")
await delete_payment(temp_id, conn=conn) await delete_payment(temp_id, conn=conn)
else: else:
logger.debug(f"backend payment failed, no checking_id {temp_id}")
async with db.connect() as conn: async with db.connect() as conn:
logger.debug(f"deleting temporary payment {temp_id}")
await delete_payment(temp_id, conn=conn) await delete_payment(temp_id, conn=conn)
raise PaymentFailure( raise PaymentFailure(
payment.error_message payment.error_message
or "Payment failed, but backend didn't give us an error message." or "Payment failed, but backend didn't give us an error message."
) )
logger.debug(f"payment successful {payment.checking_id}")
return invoice.payment_hash return invoice.payment_hash
@ -216,7 +238,7 @@ async def redeem_lnurl_withdraw(
conn=conn, conn=conn,
) )
except: except:
print( 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
@ -243,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"""
@ -325,11 +349,11 @@ async def check_invoice_status(
if not payment.pending: if not payment.pending:
return status return status
if payment.is_out and status.failed: if payment.is_out and status.failed:
print(f" - deleting outgoing failed payment {payment.checking_id}: {status}") logger.info(f"deleting outgoing failed payment {payment.checking_id}: {status}")
await payment.delete() await payment.delete()
elif not status.pending: elif not status.pending:
print( logger.info(
f" - marking '{'in' if payment.is_in else 'out'}' {payment.checking_id} as not pending anymore: {status}" f"marking '{'in' if payment.is_in else 'out'}' {payment.checking_id} as not pending anymore: {status}"
) )
await payment.set_pending(status.pending) await payment.set_pending(status.pending)
return status return status

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

@ -0,0 +1,51 @@
// the cache version gets updated every time there is a new deployment
const CACHE_VERSION = 1
const CURRENT_CACHE = `lnbits-${CACHE_VERSION}-`
const getApiKey = request => {
return request.headers.get('X-Api-Key') || 'none'
}
// on activation we clean up the previously registered service workers
self.addEventListener('activate', evt =>
evt.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
const currentCacheVersion = cacheName.split('-').slice(-2, 2)
if (currentCacheVersion !== CACHE_VERSION) {
return caches.delete(cacheName)
}
})
)
})
)
)
// The fetch handler serves responses for same-origin resources from a cache.
// If no response is found, it populates the runtime cache with the response
// from the network before returning it to the page.
self.addEventListener('fetch', event => {
// Skip cross-origin requests, like those for Google Analytics.
if (
event.request.url.startsWith(self.location.origin) &&
event.request.method == 'GET'
) {
// Open the cache
event.respondWith(
caches.open(CURRENT_CACHE + getApiKey(event.request)).then(cache => {
// Go to the network first
return fetch(event.request)
.then(fetchedResponse => {
cache.put(event.request, fetchedResponse.clone())
return fetchedResponse
})
.catch(() => {
// If the network is unavailable, get
return cache.match(event.request.url)
})
})
)
}
})

View file

@ -702,3 +702,11 @@ new Vue({
) )
} }
}) })
if (navigator.serviceWorker != null) {
navigator.serviceWorker
.register('/service-worker.js')
.then(function (registration) {
console.log('Registered events at scope: ', registration.scope)
})
}

View file

@ -1,7 +1,9 @@
import asyncio import asyncio
import httpx
from typing import List from typing import List
import httpx
from loguru import logger
from lnbits.tasks import register_invoice_listener from lnbits.tasks import register_invoice_listener
from . import db from . import db
@ -20,7 +22,7 @@ async def register_task_listeners():
async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue): async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
while True: while True:
payment = await invoice_paid_queue.get() payment = await invoice_paid_queue.get()
logger.debug("received invoice paid event")
# send information to sse channel # send information to sse channel
await dispatch_invoice_listener(payment) await dispatch_invoice_listener(payment)
@ -44,7 +46,7 @@ async def dispatch_invoice_listener(payment: Payment):
try: try:
send_channel.put_nowait(payment) send_channel.put_nowait(payment)
except asyncio.QueueFull: except asyncio.QueueFull:
print("removing sse listener", send_channel) logger.debug("removing sse listener", send_channel)
api_invoice_listeners.remove(send_channel) api_invoice_listeners.remove(send_channel)
@ -52,6 +54,7 @@ async def dispatch_webhook(payment: Payment):
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
data = payment.dict() data = payment.dict()
try: try:
logger.debug("sending webhook", payment.webhook)
r = await client.post(payment.webhook, json=data, timeout=40) r = await client.post(payment.webhook, json=data, timeout=40)
await mark_webhook_sent(payment, r.status_code) await mark_webhook_sent(payment, r.status_code)
except (httpx.ConnectError, httpx.RequestError): except (httpx.ConnectError, httpx.RequestError):

View file

@ -48,7 +48,9 @@
<code>{"X-Api-Key": "<i>{{ wallet.inkey }}</i>"}</code><br /> <code>{"X-Api-Key": "<i>{{ wallet.inkey }}</i>"}</code><br />
<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": &lt;string&gt;, "webhook": &lt;url:string&gt;}</code >{"out": false, "amount": &lt;int&gt;, "memo": &lt;string&gt;, "unit":
&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)
@ -61,8 +63,8 @@
<code <code
>curl -X POST {{ request.base_url }}api/v1/payments -d '{"out": false, >curl -X POST {{ request.base_url }}api/v1/payments -d '{"out": false,
"amount": &lt;int&gt;, "memo": &lt;string&gt;, "webhook": "amount": &lt;int&gt;, "memo": &lt;string&gt;, "webhook":
&lt;url:string&gt;, "unit": &lt;string&gt;}' -H "X-Api-Key: <i>{{ wallet.inkey }}</i>" -H &lt;url:string&gt;, "unit": &lt;string&gt;}' -H "X-Api-Key:
"Content-type: application/json"</code <i>{{ wallet.inkey }}</i>" -H "Content-type: application/json"</code
> >
</q-card-section> </q-card-section>
</q-card> </q-card>

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

@ -4,7 +4,6 @@
<!----> <!---->
{% block scripts %} {{ window_vars(user, wallet) }} {% block scripts %} {{ window_vars(user, wallet) }}
<script src="/core/static/js/wallet.js"></script> <script src="/core/static/js/wallet.js"></script>
<link rel="manifest" href="/manifest/{{ user.id }}.webmanifest" />
{% endblock %} {% endblock %}
<!----> <!---->
{% block title %} {{ wallet.name }} - {{ SITE_TITLE }} {% endblock %} {% block title %} {{ wallet.name }} - {{ SITE_TITLE }} {% endblock %}
@ -709,7 +708,7 @@
<q-dialog v-model="disclaimerDialog.show"> <q-dialog v-model="disclaimerDialog.show">
<q-card class="q-pa-lg"> <q-card class="q-pa-lg">
<h6 class="q-my-md text-deep-purple">Warning</h6> <h6 class="q-my-md text-primary">Warning</h6>
<p> <p>
Login functionality to be released in v0.2, for now, Login functionality to be released in v0.2, for now,
<strong <strong

View file

@ -7,26 +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 Header, Query, Request from fastapi import Depends, 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,
) )
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,
@ -110,16 +107,29 @@ async def api_update_wallet(
@core_app.get("/api/v1/payments") @core_app.get("/api/v1/payments")
async def api_payments(wallet: WalletTypeInfo = Depends(get_key_type)): async def api_payments(
await get_payments(wallet_id=wallet.wallet.id, pending=True, complete=True) limit: Optional[int] = None,
offset: Optional[int] = None,
wallet: WalletTypeInfo = Depends(get_key_type),
):
pendingPayments = await get_payments( pendingPayments = await get_payments(
wallet_id=wallet.wallet.id, pending=True, exclude_uncheckable=True wallet_id=wallet.wallet.id,
pending=True,
exclude_uncheckable=True,
limit=limit,
offset=offset,
) )
for payment in pendingPayments: for payment in pendingPayments:
await check_invoice_status( await check_invoice_status(
wallet_id=payment.wallet_id, payment_hash=payment.payment_hash wallet_id=payment.wallet_id, payment_hash=payment.payment_hash
) )
return await get_payments(wallet_id=wallet.wallet.id, pending=True, complete=True) return await get_payments(
wallet_id=wallet.wallet.id,
pending=True,
complete=True,
limit=limit,
offset=offset,
)
class CreateInvoiceData(BaseModel): class CreateInvoiceData(BaseModel):
@ -132,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
@ -145,6 +156,7 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
if data.unit == "sat": if data.unit == "sat":
amount = int(data.amount) amount = int(data.amount)
else: else:
assert data.unit is not None, "unit not set"
price_in_sats = await fiat_amount_as_satoshis(data.amount, data.unit) price_in_sats = await fiat_amount_as_satoshis(data.amount, data.unit)
amount = price_in_sats amount = price_in_sats
@ -157,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:
@ -169,6 +182,9 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
lnurl_response: Union[None, bool, str] = None lnurl_response: Union[None, bool, str] = None
if data.lnurl_callback: if data.lnurl_callback:
if "lnurl_balance_check" in data: if "lnurl_balance_check" in data:
assert (
data.lnurl_balance_check is not None
), "lnurl_balance_check is required"
save_balance_check(wallet.id, data.lnurl_balance_check) save_balance_check(wallet.id, data.lnurl_balance_check)
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
@ -231,12 +247,9 @@ async def api_payments_pay_invoice(bolt11: str, wallet: Wallet):
status_code=HTTPStatus.CREATED, status_code=HTTPStatus.CREATED,
) )
async def api_payments_create( async def api_payments_create(
wallet: WalletTypeInfo = Depends(get_key_type), wallet: WalletTypeInfo = Depends(require_invoice_key),
invoiceData: CreateInvoiceData = Body(...), invoiceData: CreateInvoiceData = Body(...),
): ):
if wallet.wallet_type < 0 or wallet.wallet_type > 2:
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="Key is invalid")
if invoiceData.out is True and wallet.wallet_type == 0: if invoiceData.out is True and wallet.wallet_type == 0:
if not invoiceData.bolt11: if not invoiceData.bolt11:
raise HTTPException( raise HTTPException(
@ -246,8 +259,14 @@ async def api_payments_create(
return await api_payments_pay_invoice( return await api_payments_pay_invoice(
invoiceData.bolt11, wallet.wallet invoiceData.bolt11, wallet.wallet
) # admin key ) # admin key
elif not invoiceData.out:
# invoice key # invoice key
return await api_payments_create_invoice(invoiceData, wallet.wallet) return await api_payments_create_invoice(invoiceData, wallet.wallet)
else:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="Invoice (or Admin) key required.",
)
class CreateLNURLData(BaseModel): class CreateLNURLData(BaseModel):
@ -260,7 +279,7 @@ class CreateLNURLData(BaseModel):
@core_app.post("/api/v1/payments/lnurl") @core_app.post("/api/v1/payments/lnurl")
async def api_payments_pay_lnurl( async def api_payments_pay_lnurl(
data: CreateLNURLData, wallet: WalletTypeInfo = Depends(get_key_type) data: CreateLNURLData, wallet: WalletTypeInfo = Depends(require_admin_key)
): ):
domain = urlparse(data.callback).netloc domain = urlparse(data.callback).netloc
@ -286,6 +305,12 @@ async def api_payments_pay_lnurl(
detail=f"{domain} said: '{params.get('reason', '')}'", detail=f"{domain} said: '{params.get('reason', '')}'",
) )
if not params.get("pr"):
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"{domain} did not return a payment request.",
)
invoice = bolt11.decode(params["pr"]) invoice = bolt11.decode(params["pr"])
if invoice.amount_msat != data.amount: if invoice.amount_msat != data.amount:
raise HTTPException( raise HTTPException(
@ -293,11 +318,11 @@ async def api_payments_pay_lnurl(
detail=f"{domain} returned an invalid invoice. Expected {data.amount} msat, got {invoice.amount_msat}.", detail=f"{domain} returned an invalid invoice. Expected {data.amount} msat, got {invoice.amount_msat}.",
) )
# if invoice.description_hash != data.description_hash: if invoice.description_hash != data.description_hash:
# raise HTTPException( raise HTTPException(
# status_code=HTTPStatus.BAD_REQUEST, status_code=HTTPStatus.BAD_REQUEST,
# detail=f"{domain} returned an invalid invoice. Expected description_hash == {data.description_hash}, got {invoice.description_hash}.", detail=f"{domain} returned an invalid invoice. Expected description_hash == {data.description_hash}, got {invoice.description_hash}.",
# ) )
extra = {} extra = {}
@ -305,7 +330,7 @@ async def api_payments_pay_lnurl(
extra["success_action"] = params["successAction"] extra["success_action"] = params["successAction"]
if data.comment: if data.comment:
extra["comment"] = data.comment extra["comment"] = data.comment
assert data.description is not None, "description is required"
payment_hash = await pay_invoice( payment_hash = await pay_invoice(
wallet_id=wallet.wallet.id, wallet_id=wallet.wallet.id,
payment_request=params["pr"], payment_request=params["pr"],
@ -322,19 +347,20 @@ async def api_payments_pay_lnurl(
async def subscribe(request: Request, wallet: Wallet): async def subscribe(request: Request, wallet: Wallet):
this_wallet_id = wallet.wallet.id this_wallet_id = wallet.id
payment_queue = asyncio.Queue(0) payment_queue: asyncio.Queue[Payment] = asyncio.Queue(0)
print("adding sse listener", payment_queue) logger.debug("adding sse listener", payment_queue)
api_invoice_listeners.append(payment_queue) api_invoice_listeners.append(payment_queue)
send_queue = asyncio.Queue(0) send_queue: asyncio.Queue[tuple[str, Payment]] = asyncio.Queue(0)
async def payment_received() -> None: async def payment_received() -> None:
while True: while True:
payment: Payment = await payment_queue.get() payment: Payment = await payment_queue.get()
if payment.wallet_id == this_wallet_id: if payment.wallet_id == this_wallet_id:
logger.debug("payment receieved", payment)
await send_queue.put(("payment-received", payment)) await send_queue.put(("payment-received", payment))
asyncio.create_task(payment_received()) asyncio.create_task(payment_received())
@ -359,21 +385,32 @@ async def api_payments_sse(
request: Request, wallet: WalletTypeInfo = Depends(get_key_type) request: Request, wallet: WalletTypeInfo = Depends(get_key_type)
): ):
return EventSourceResponse( return EventSourceResponse(
subscribe(request, wallet), ping=20, media_type="text/event-stream" subscribe(request, wallet.wallet), ping=20, media_type="text/event-stream"
) )
@core_app.get("/api/v1/payments/{payment_hash}") @core_app.get("/api/v1/payments/{payment_hash}")
async def api_payment(payment_hash, X_Api_Key: Optional[str] = Header(None)): async def api_payment(payment_hash, X_Api_Key: Optional[str] = Header(None)):
# 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 = None wallet = None
try: try:
if X_Api_Key.extra: if X_Api_Key.extra:
print("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_hash) payment = await get_standalone_payment(
payment_hash, wallet_id=wallet.id if wallet else None
) # 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
if payment is None:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Payment does not exist."
)
await check_invoice_status(payment.wallet_id, payment_hash) await check_invoice_status(payment.wallet_id, payment_hash)
payment = await get_standalone_payment(payment_hash) payment = await get_standalone_payment(
payment_hash, wallet_id=wallet.id if wallet else None
)
if not payment: if not payment:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Payment does not exist." status_code=HTTPStatus.NOT_FOUND, detail="Payment does not exist."
@ -391,14 +428,16 @@ async def api_payment(payment_hash, X_Api_Key: Optional[str] = Header(None)):
return {"paid": False} return {"paid": False}
if wallet and wallet.id == payment.wallet_id: if wallet and wallet.id == payment.wallet_id:
return {"paid": not payment.pending, "preimage": payment.preimage, "details": payment} return {
"paid": not payment.pending,
"preimage": payment.preimage,
"details": payment,
}
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
@ -426,7 +465,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:
@ -542,14 +581,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,6 +7,7 @@ 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
@ -17,10 +18,12 @@ from lnbits.helpers import template_renderer, url_for
from lnbits.settings import ( from lnbits.settings import (
LNBITS_ADMIN_USERS, LNBITS_ADMIN_USERS,
LNBITS_ALLOWED_USERS, LNBITS_ALLOWED_USERS,
LNBITS_CUSTOM_LOGO,
LNBITS_SITE_TITLE, LNBITS_SITE_TITLE,
SERVICE_FEE, SERVICE_FEE,
) )
from ...helpers import get_valid_extensions
from ..crud import ( from ..crud import (
create_account, create_account,
create_wallet, create_wallet,
@ -64,11 +67,21 @@ async def extensions(
HTTPStatus.BAD_REQUEST, "You can either `enable` or `disable` an extension." HTTPStatus.BAD_REQUEST, "You can either `enable` or `disable` an extension."
) )
# check if extension exists
if extension_to_enable or extension_to_disable:
ext = extension_to_enable or extension_to_disable
if ext not in [e.code for e in get_valid_extensions()]:
raise HTTPException(
HTTPStatus.BAD_REQUEST, f"Extension '{ext}' doesn't exist."
)
if extension_to_enable: if extension_to_enable:
logger.info(f"Enabling extension: {extension_to_enable} for user {user.id}")
await update_user_extension( await update_user_extension(
user_id=user.id, extension=extension_to_enable, active=True user_id=user.id, extension=extension_to_enable, active=True
) )
elif extension_to_disable: elif extension_to_disable:
logger.info(f"Disabling extension: {extension_to_disable} for user {user.id}")
await update_user_extension( await update_user_extension(
user_id=user.id, extension=extension_to_disable, active=False user_id=user.id, extension=extension_to_disable, active=False
) )
@ -108,6 +121,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"Create user {user.id}")
else: else:
user = await get_user(user_id) user = await get_user(user_id)
if not user: if not user:
@ -125,12 +139,16 @@ async def wallet(
wallet = user.wallets[0] wallet = user.wallets[0]
else: else:
wallet = await create_wallet(user_id=user.id, wallet_name=wallet_name) wallet = await create_wallet(user_id=user.id, wallet_name=wallet_name)
logger.info(
f"Created new wallet {wallet_name if wallet_name else '(no name)'} for user {user.id}"
)
return RedirectResponse( return RedirectResponse(
f"/wallet?usr={user.id}&wal={wallet.id}", f"/wallet?usr={user.id}&wal={wallet.id}",
status_code=status.HTTP_307_TEMPORARY_REDIRECT, status_code=status.HTTP_307_TEMPORARY_REDIRECT,
) )
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(
@ -144,6 +162,7 @@ async def wallet(
"user": user.dict(), "user": user.dict(),
"wallet": wallet.dict(), "wallet": wallet.dict(),
"service_fee": service_fee, "service_fee": service_fee,
"web_manifest": f"/manifest/{user.id}.webmanifest",
}, },
) )
@ -200,13 +219,13 @@ async def lnurl_full_withdraw_callback(request: Request):
async def deletewallet(request: Request, wal: str = Query(...), usr: str = Query(...)): async def deletewallet(request: Request, wal: str = Query(...), usr: str = Query(...)):
user = await get_user(usr) user = await get_user(usr)
user_wallet_ids = [u.id for u in user.wallets] user_wallet_ids = [u.id for u in user.wallets]
print("USR", user_wallet_ids)
if wal not in user_wallet_ids: if wal not in user_wallet_ids:
raise HTTPException(HTTPStatus.FORBIDDEN, "Not your wallet.") raise HTTPException(HTTPStatus.FORBIDDEN, "Not your wallet.")
else: else:
await delete_wallet(user_id=user.id, wallet_id=wal) await delete_wallet(user_id=user.id, wallet_id=wal)
user_wallet_ids.remove(wal) user_wallet_ids.remove(wal)
logger.debug("Deleted wallet {wal} of user {user.id}")
if user_wallet_ids: if user_wallet_ids:
return RedirectResponse( return RedirectResponse(
@ -226,7 +245,9 @@ async def lnurl_balance_notify(request: Request, service: str):
redeem_lnurl_withdraw(bc.wallet, bc.url) redeem_lnurl_withdraw(bc.wallet, bc.url)
@core_html_routes.get("/lnurlwallet", response_class=RedirectResponse, name="core.lnurlwallet") @core_html_routes.get(
"/lnurlwallet", response_class=RedirectResponse, name="core.lnurlwallet"
)
async def lnurlwallet(request: Request): async def lnurlwallet(request: Request):
async with db.connect() as conn: async with db.connect() as conn:
account = await create_account(conn=conn) account = await create_account(conn=conn)
@ -249,6 +270,11 @@ async def lnurlwallet(request: Request):
) )
@core_html_routes.get("/service-worker.js", response_class=FileResponse)
async def service_worker():
return FileResponse("lnbits/core/static/js/service-worker.js")
@core_html_routes.get("/manifest/{usr}.webmanifest") @core_html_routes.get("/manifest/{usr}.webmanifest")
async def manifest(usr: str): async def manifest(usr: str):
user = await get_user(usr) user = await get_user(usr)
@ -256,21 +282,23 @@ async def manifest(usr: str):
raise HTTPException(status_code=HTTPStatus.NOT_FOUND) raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
return { return {
"short_name": "LNbits", "short_name": LNBITS_SITE_TITLE,
"name": "LNbits Wallet", "name": LNBITS_SITE_TITLE + " Wallet",
"icons": [ "icons": [
{ {
"src": "https://cdn.jsdelivr.net/gh/lnbits/lnbits@0.3.0/docs/logos/lnbits.png", "src": LNBITS_CUSTOM_LOGO
if LNBITS_CUSTOM_LOGO
else "https://cdn.jsdelivr.net/gh/lnbits/lnbits@0.3.0/docs/logos/lnbits.png",
"type": "image/png", "type": "image/png",
"sizes": "900x900", "sizes": "900x900",
} }
], ],
"start_url": "/wallet?usr=" + usr, "start_url": "/wallet?usr=" + usr + "&wal=" + user.wallets[0].id,
"background_color": "#3367D6", "background_color": "#1F2234",
"description": "Weather forecast information", "description": "Bitcoin Lightning Wallet",
"display": "standalone", "display": "standalone",
"scope": "/", "scope": "/",
"theme_color": "#3367D6", "theme_color": "#1F2234",
"shortcuts": [ "shortcuts": [
{ {
"name": wallet.name, "name": wallet.name,

View file

@ -4,6 +4,7 @@ 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
@ -45,7 +46,7 @@ async def api_public_payment_longpolling(payment_hash):
payment_queue = asyncio.Queue(0) payment_queue = asyncio.Queue(0)
print("adding standalone invoice listener", payment_hash, payment_queue) logger.debug("adding standalone invoice listener", payment_hash, payment_queue)
api_invoice_listeners.append(payment_queue) api_invoice_listeners.append(payment_queue)
response = None response = None

View file

@ -5,6 +5,7 @@ import time
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from typing import Optional from typing import Optional
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
@ -139,7 +140,7 @@ class Database(Compat):
f"LNBITS_DATA_FOLDER named {LNBITS_DATA_FOLDER} was not created" f"LNBITS_DATA_FOLDER named {LNBITS_DATA_FOLDER} was not created"
f" - please 'mkdir {LNBITS_DATA_FOLDER}' and try again" f" - please 'mkdir {LNBITS_DATA_FOLDER}' and try again"
) )
logger.trace(f"database {self.type} added for {self.name}")
self.schema = self.name self.schema = self.name
if self.name.startswith("ext_"): if self.name.startswith("ext_"):
self.schema = self.name[4:] self.schema = self.name[4:]

View file

@ -13,7 +13,11 @@ from starlette.requests import Request
from lnbits.core.crud import get_user, get_wallet_for_key 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 LNBITS_ALLOWED_USERS, LNBITS_ADMIN_USERS, LNBITS_ADMIN_EXTENSIONS from lnbits.settings import (
LNBITS_ADMIN_EXTENSIONS,
LNBITS_ADMIN_USERS,
LNBITS_ALLOWED_USERS,
)
class KeyChecker(SecurityBase): class KeyChecker(SecurityBase):
@ -122,7 +126,7 @@ async def get_key_type(
# 0: admin # 0: admin
# 1: invoice # 1: invoice
# 2: invalid # 2: invalid
pathname = r['path'].split('/')[1] pathname = r["path"].split("/")[1]
if not api_key_header and not api_key_query: if not api_key_header and not api_key_query:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST) raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
@ -133,8 +137,12 @@ async def get_key_type(
checker = WalletAdminKeyChecker(api_key=token) checker = WalletAdminKeyChecker(api_key=token)
await checker.__call__(r) await checker.__call__(r)
wallet = WalletTypeInfo(0, checker.wallet) wallet = WalletTypeInfo(0, checker.wallet)
if (LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS) and (LNBITS_ADMIN_EXTENSIONS and pathname in LNBITS_ADMIN_EXTENSIONS): if (LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS) and (
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized.") LNBITS_ADMIN_EXTENSIONS and pathname in LNBITS_ADMIN_EXTENSIONS
):
raise HTTPException(
status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized."
)
return wallet return wallet
except HTTPException as e: except HTTPException as e:
if e.status_code == HTTPStatus.BAD_REQUEST: if e.status_code == HTTPStatus.BAD_REQUEST:
@ -148,8 +156,12 @@ async def get_key_type(
checker = WalletInvoiceKeyChecker(api_key=token) checker = WalletInvoiceKeyChecker(api_key=token)
await checker.__call__(r) await checker.__call__(r)
wallet = WalletTypeInfo(1, checker.wallet) wallet = WalletTypeInfo(1, checker.wallet)
if (LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS) and (LNBITS_ADMIN_EXTENSIONS and pathname in LNBITS_ADMIN_EXTENSIONS): if (LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS) and (
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized.") LNBITS_ADMIN_EXTENSIONS and pathname in LNBITS_ADMIN_EXTENSIONS
):
raise HTTPException(
status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized."
)
return wallet return wallet
except HTTPException as e: except HTTPException as e:
if e.status_code == HTTPStatus.BAD_REQUEST: if e.status_code == HTTPStatus.BAD_REQUEST:

View file

@ -9,7 +9,7 @@ db = Database("ext_bleskomat")
bleskomat_static_files = [ bleskomat_static_files = [
{ {
"path": "/bleskomat/static", "path": "/bleskomat/static",
"app": StaticFiles(directory="lnbits/extensions/bleskomat/static"), "app": StaticFiles(packages=[("lnbits", "extensions/bleskomat/static")]),
"name": "bleskomat_static", "name": "bleskomat_static",
} }
] ]

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,6 +3,7 @@ import math
import traceback import traceback
from http import HTTPStatus from http import HTTPStatus
from loguru import logger
from starlette.requests import Request from starlette.requests import Request
from . import bleskomat_ext from . import bleskomat_ext
@ -122,7 +123,7 @@ async def api_bleskomat_lnurl(req: Request):
except LnurlHttpError as e: except LnurlHttpError as e:
return {"status": "ERROR", "reason": str(e)} return {"status": "ERROR", "reason": str(e)}
except Exception as e: except Exception as e:
print(str(e)) logger.error(str(e))
return {"status": "ERROR", "reason": "Unexpected error"} return {"status": "ERROR", "reason": "Unexpected error"}
return {"status": "OK"} return {"status": "OK"}

View file

@ -3,11 +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 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
@ -125,7 +126,7 @@ class BleskomatLnurl(BaseModel):
except (ValueError, PermissionError, PaymentFailure) as e: except (ValueError, PermissionError, PaymentFailure) as e:
raise LnurlValidationError("Failed to pay invoice: " + str(e)) raise LnurlValidationError("Failed to pay invoice: " + str(e))
except Exception as e: except Exception as e:
print(str(e)) logger.error(str(e))
raise LnurlValidationError("Unexpected error") raise LnurlValidationError("Unexpected error")
async def use(self, conn) -> bool: async def use(self, conn) -> bool:

View file

@ -62,4 +62,5 @@
</p> </p>
</q-card-section> </q-card-section>
</q-card> </q-card>
<q-btn flat label="Swagger API" type="a" href="../docs#/Bleskomat"></q-btn>
</q-expansion-item> </q-expansion-item>

View file

@ -1,6 +1,7 @@
from http import HTTPStatus from http import HTTPStatus
from fastapi import Depends, Query from fastapi import Depends, Query
from loguru import logger
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user from lnbits.core.crud import get_user
@ -60,7 +61,7 @@ async def api_bleskomat_create_or_update(
currency=fiat_currency, provider=exchange_rate_provider currency=fiat_currency, provider=exchange_rate_provider
) )
except Exception as e: except Exception as e:
print(e) logger.error(e)
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail=f'Failed to fetch BTC/{fiat_currency} currency pair from "{exchange_rate_provider}"', detail=f'Failed to fetch BTC/{fiat_currency} currency pair from "{exchange_rate_provider}"',

View file

@ -12,7 +12,7 @@ db = Database("ext_copilot")
copilot_static_files = [ copilot_static_files = [
{ {
"path": "/copilot/static", "path": "/copilot/static",
"app": StaticFiles(directory="lnbits/extensions/copilot/static"), "app": StaticFiles(packages=[("lnbits", "extensions/copilot/static")]),
"name": "copilot_static", "name": "copilot_static",
} }
] ]

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

@ -14,6 +14,7 @@
label="API info" label="API info"
:content-inset-level="0.5" :content-inset-level="0.5"
> >
<q-btn flat label="Swagger API" type="a" href="../docs#/copilot"></q-btn>
<q-expansion-item group="api" dense expand-separator label="Create copilot"> <q-expansion-item group="api" dense expand-separator label="Create copilot">
<q-card> <q-card>
<q-card-section> <q-card-section>
@ -31,8 +32,8 @@
<code>[&lt;copilot_object&gt;, ...]</code> <code>[&lt;copilot_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X POST {{ request.base_url }}api/v1/copilot -d '{"title": >curl -X POST {{ request.base_url }}copilot/api/v1/copilot -d
&lt;string&gt;, "animation": &lt;string&gt;, '{"title": &lt;string&gt;, "animation": &lt;string&gt;,
"show_message":&lt;string&gt;, "amount": &lt;integer&gt;, "show_message":&lt;string&gt;, "amount": &lt;integer&gt;,
"lnurl_title": &lt;string&gt;}' -H "Content-type: application/json" "lnurl_title": &lt;string&gt;}' -H "Content-type: application/json"
-H "X-Api-Key: {{user.wallets[0].adminkey }}" -H "X-Api-Key: {{user.wallets[0].adminkey }}"
@ -59,11 +60,11 @@
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X POST {{ request.base_url >curl -X POST {{ request.base_url
}}api/v1/copilot/&lt;copilot_id&gt; -d '{"title": &lt;string&gt;, }}copilot/api/v1/copilot/&lt;copilot_id&gt; -d '{"title":
"animation": &lt;string&gt;, "show_message":&lt;string&gt;, &lt;string&gt;, "animation": &lt;string&gt;,
"amount": &lt;integer&gt;, "lnurl_title": &lt;string&gt;}' -H "show_message":&lt;string&gt;, "amount": &lt;integer&gt;,
"Content-type: application/json" -H "X-Api-Key: "lnurl_title": &lt;string&gt;}' -H "Content-type: application/json"
{{user.wallets[0].adminkey }}" -H "X-Api-Key: {{user.wallets[0].adminkey }}"
</code> </code>
</q-card-section> </q-card-section>
</q-card> </q-card>
@ -87,8 +88,9 @@
<code>[&lt;copilot_object&gt;, ...]</code> <code>[&lt;copilot_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X GET {{ request.base_url }}api/v1/copilot/&lt;copilot_id&gt; >curl -X GET {{ request.base_url
-H "X-Api-Key: {{ user.wallets[0].inkey }}" }}copilot/api/v1/copilot/&lt;copilot_id&gt; -H "X-Api-Key: {{
user.wallets[0].inkey }}"
</code> </code>
</q-card-section> </q-card-section>
</q-card> </q-card>
@ -110,8 +112,8 @@
<code>[&lt;copilot_object&gt;, ...]</code> <code>[&lt;copilot_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X GET {{ request.base_url }}api/v1/copilots -H "X-Api-Key: {{ >curl -X GET {{ request.base_url }}copilot/api/v1/copilots -H
user.wallets[0].inkey }}" "X-Api-Key: {{ user.wallets[0].inkey }}"
</code> </code>
</q-card-section> </q-card-section>
</q-card> </q-card>
@ -136,7 +138,7 @@
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X DELETE {{ request.base_url >curl -X DELETE {{ request.base_url
}}api/v1/copilot/&lt;copilot_id&gt; -H "X-Api-Key: {{ }}copilot/api/v1/copilot/&lt;copilot_id&gt; -H "X-Api-Key: {{
user.wallets[0].adminkey }}" user.wallets[0].adminkey }}"
</code> </code>
</q-card-section> </q-card-section>
@ -161,9 +163,10 @@
<code></code> <code></code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X GET {{ request.base_url }}/api/v1/copilot/ws/&lt;string, >curl -X GET {{ request.base_url
copilot_id&gt;/&lt;string, comment&gt;/&lt;string, gif name&gt; -H }}copilot/api/v1/copilot/ws/&lt;string, copilot_id&gt;/&lt;string,
"X-Api-Key: {{ user.wallets[0].adminkey }}" comment&gt;/&lt;string, gif name&gt; -H "X-Api-Key: {{
user.wallets[0].adminkey }}"
</code> </code>
</q-card-section> </q-card-section>
</q-card> </q-card>

View file

@ -9,7 +9,7 @@ db = Database("ext_discordbot")
discordbot_static_files = [ discordbot_static_files = [
{ {
"path": "/discordbot/static", "path": "/discordbot/static",
"app": StaticFiles(directory="lnbits/extensions/discordbot/static"), "app": StaticFiles(packages=[("lnbits", "extensions/discordbot/static")]),
"name": "discordbot_static", "name": "discordbot_static",
} }
] ]

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):
@ -11,6 +11,7 @@ class CreateUserData(BaseModel):
admin_id: str = Query(...) admin_id: str = Query(...)
discord_id: str = Query("") discord_id: str = Query("")
class CreateUserWallet(BaseModel): class CreateUserWallet(BaseModel):
user_id: str = Query(...) user_id: str = Query(...)
wallet_name: str = Query(...) wallet_name: str = Query(...)
@ -23,6 +24,7 @@ class Users(BaseModel):
admin: str admin: str
discord_id: str discord_id: str
class Wallets(BaseModel): class Wallets(BaseModel):
id: str id: str
admin: str admin: str

View file

@ -10,12 +10,19 @@
Discord Bot: Connect Discord users to LNbits. Discord Bot: Connect Discord users to LNbits.
</h5> </h5>
<p> <p>
Connect your LNbits instance to a <a href="https://github.com/chrislennon/lnbits-discord-bot">Discord Bot</a> leveraging LNbits as a community based lightning node.<br /> Connect your LNbits instance to a
<a href="https://github.com/chrislennon/lnbits-discord-bot"
>Discord Bot</a
>
leveraging LNbits as a community based lightning node.<br />
<small> <small>
Created by, <a href="https://github.com/chrislennon">Chris Lennon</a></small Created by,
> <br /> <a href="https://github.com/chrislennon">Chris Lennon</a></small
>
<br />
<small> <small>
Based on User Manager, by <a href="https://github.com/benarc">Ben Arc</a></small Based on User Manager, by
<a href="https://github.com/benarc">Ben Arc</a></small
> >
</p> </p>
</q-card-section> </q-card-section>
@ -27,6 +34,7 @@
label="API info" label="API info"
:content-inset-level="0.5" :content-inset-level="0.5"
> >
<q-btn flat label="Swagger API" type="a" href="../docs#/discordbot"></q-btn>
<q-expansion-item group="api" dense expand-separator label="GET users"> <q-expansion-item group="api" dense expand-separator label="GET users">
<q-card> <q-card>
<q-card-section> <q-card-section>
@ -149,8 +157,9 @@
<code <code
>curl -X POST {{ request.base_url }}discordbot/api/v1/users -d >curl -X POST {{ request.base_url }}discordbot/api/v1/users -d
'{"admin_id": "{{ user.id }}", "wallet_name": &lt;string&gt;, '{"admin_id": "{{ user.id }}", "wallet_name": &lt;string&gt;,
"user_name": &lt;string&gt;, "discord_id": &lt;string&gt;}' -H "X-Api-Key: {{ "user_name": &lt;string&gt;, "discord_id": &lt;string&gt;}' -H
user.wallets[0].inkey }}" -H "Content-type: application/json" "X-Api-Key: {{ user.wallets[0].inkey }}" -H "Content-type:
application/json"
</code> </code>
</q-card-section> </q-card-section>
</q-card> </q-card>

View file

@ -136,7 +136,8 @@
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md"> <div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
<q-card> <q-card>
<q-card-section> <q-card-section>
<h6 class="text-subtitle1 q-my-none">LNbits Discord Bot Extension <h6 class="text-subtitle1 q-my-none">
LNbits Discord Bot Extension
<!--{{SITE_TITLE}} Discord Bot Extension--> <!--{{SITE_TITLE}} Discord Bot Extension-->
</h6> </h6>
</q-card-section> </q-card-section>
@ -236,7 +237,12 @@
columns: [ columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'}, {name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'name', align: 'left', label: 'Username', field: 'name'}, {name: 'name', align: 'left', label: 'Username', field: 'name'},
{name: 'discord_id', align: 'left', label: 'discord_id', field: 'discord_id'} {
name: 'discord_id',
align: 'left',
label: 'discord_id',
field: 'discord_id'
}
], ],
pagination: { pagination: {
rowsPerPage: 10 rowsPerPage: 10

View file

@ -109,9 +109,7 @@ async def api_discordbot_wallet_transactions(
async def api_discordbot_users_wallets( async def api_discordbot_users_wallets(
user_id, wallet: WalletTypeInfo = Depends(get_key_type) user_id, wallet: WalletTypeInfo = Depends(get_key_type)
): ):
return [ return [s_wallet.dict() for s_wallet in await get_discordbot_users_wallets(user_id)]
s_wallet.dict() for s_wallet in await get_discordbot_users_wallets(user_id)
]
@discordbot_ext.delete("/api/v1/wallets/{wallet_id}") @discordbot_ext.delete("/api/v1/wallets/{wallet_id}")

View file

@ -16,7 +16,7 @@ async def create_ticket(
INSERT INTO events.ticket (id, wallet, event, name, email, registered, paid) INSERT INTO events.ticket (id, wallet, event, name, email, registered, paid)
VALUES (?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
""", """,
(payment_hash, wallet, event, name, email, False, False), (payment_hash, wallet, event, name, email, False, True),
) )
ticket = await get_ticket(payment_hash) ticket = await get_ticket(payment_hash)

View file

@ -20,4 +20,5 @@
</p> </p>
</q-card-section> </q-card-section>
</q-card> </q-card>
<q-btn flat label="Swagger API" type="a" href="../docs#/events"></q-btn>
</q-expansion-item> </q-expansion-item>

View file

@ -135,15 +135,7 @@
var self = this var self = this
axios axios
.post( .get('/events/api/v1/tickets/' + '{{ event_id }}')
'/events/api/v1/tickets/' + '{{ event_id }}/{{ event_price }}',
{
event: '{{ event_id }}',
event_name: '{{ event_name }}',
name: self.formDialog.data.name,
email: self.formDialog.data.email
}
)
.then(function (response) { .then(function (response) {
self.paymentReq = response.data.payment_request self.paymentReq = response.data.payment_request
self.paymentCheck = response.data.payment_hash self.paymentCheck = response.data.payment_hash
@ -161,7 +153,17 @@
paymentChecker = setInterval(function () { paymentChecker = setInterval(function () {
axios axios
.get('/events/api/v1/tickets/' + self.paymentCheck) .post(
'/events/api/v1/tickets/' +
'{{ event_id }}/' +
self.paymentCheck,
{
event: '{{ event_id }}',
event_name: '{{ event_name }}',
name: self.formDialog.data.name,
email: self.formDialog.data.email
}
)
.then(function (res) { .then(function (res) {
if (res.data.paid) { if (res.data.paid) {
clearInterval(paymentChecker) clearInterval(paymentChecker)

View file

@ -133,7 +133,10 @@
var self = this var self = this
LNbits.api LNbits.api
.request('GET', '/events/api/v1/register/ticket/' + res) .request(
'GET',
'/events/api/v1/register/ticket/' + res.split('//')[1]
)
.then(function (response) { .then(function (response) {
self.$q.notify({ self.$q.notify({
type: 'positive', type: 'positive',

View file

@ -13,9 +13,8 @@
<br /> <br />
<qrcode <qrcode
:value="'{{ ticket_id }}'" :value="'ticket://{{ ticket_id }}'"
:options="{width: 340}" :options="{width: 500}"
class="rounded-borders"
></qrcode> ></qrcode>
<br /> <br />
<q-btn @click="printWindow" color="grey" class="q-ml-auto"> <q-btn @click="printWindow" color="grey" class="q-ml-auto">

View file

@ -97,8 +97,8 @@ async def api_tickets(
return [ticket.dict() for ticket in await get_tickets(wallet_ids)] return [ticket.dict() for ticket in await get_tickets(wallet_ids)]
@events_ext.post("/api/v1/tickets/{event_id}/{sats}") @events_ext.get("/api/v1/tickets/{event_id}")
async def api_ticket_make_ticket(event_id, sats, data: CreateTicket): async def api_ticket_make_ticket(event_id):
event = await get_event(event_id) event = await get_event(event_id)
if not event: if not event:
raise HTTPException( raise HTTPException(
@ -107,13 +107,22 @@ async def api_ticket_make_ticket(event_id, sats, data: CreateTicket):
try: try:
payment_hash, payment_request = await create_invoice( payment_hash, payment_request = await create_invoice(
wallet_id=event.wallet, wallet_id=event.wallet,
amount=int(sats), amount=event.price_per_ticket,
memo=f"{event_id}", memo=f"{event_id}",
extra={"tag": "events"}, extra={"tag": "events"},
) )
except Exception as e: except Exception as e:
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e)) raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
return {"payment_hash": payment_hash, "payment_request": payment_request}
@events_ext.post("/api/v1/tickets/{event_id}/{payment_hash}")
async def api_ticket_send_ticket(event_id, payment_hash, data: CreateTicket):
event = await get_event(event_id)
try:
status = await api_payment(payment_hash)
if status["paid"]:
ticket = await create_ticket( ticket = await create_ticket(
payment_hash=payment_hash, payment_hash=payment_hash,
wallet=event.wallet, wallet=event.wallet,
@ -127,17 +136,6 @@ async def api_ticket_make_ticket(event_id, sats, data: CreateTicket):
status_code=HTTPStatus.NOT_FOUND, detail=f"Event could not be fetched." status_code=HTTPStatus.NOT_FOUND, detail=f"Event could not be fetched."
) )
return {"payment_hash": payment_hash, "payment_request": payment_request}
@events_ext.get("/api/v1/tickets/{payment_hash}")
async def api_ticket_send_ticket(payment_hash):
ticket = await get_ticket(payment_hash)
try:
status = await api_payment(payment_hash)
if status["paid"]:
await set_ticket_paid(payment_hash=payment_hash)
return {"paid": True, "ticket_id": ticket.id} return {"paid": True, "ticket_id": ticket.id}
except Exception: except Exception:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Not paid") raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Not paid")

View file

@ -0,0 +1,11 @@
<h1>Example Extension</h1>
<h2>*tagline*</h2>
This is an example extension to help you organise and build you own.
Try to include an image
<img src="https://i.imgur.com/9i4xcQB.png">
<h2>If your extension has API endpoints, include useful ones here</h2>
<code>curl -H "Content-type: application/json" -X POST https://YOUR-LNBITS/YOUR-EXTENSION/api/v1/EXAMPLE -d '{"amount":"100","memo":"example"}' -H "X-Api-Key: YOUR_WALLET-ADMIN/INVOICE-KEY"</code>

View file

@ -0,0 +1,16 @@
from fastapi import APIRouter
from lnbits.db import Database
from lnbits.helpers import template_renderer
db = Database("ext_example")
example_ext: APIRouter = APIRouter(prefix="/example", tags=["example"])
def example_renderer():
return template_renderer(["lnbits/extensions/example/templates"])
from .views import * # noqa
from .views_api import * # noqa

View file

@ -0,0 +1,6 @@
{
"name": "Build your own!!",
"short_description": "Join us, make an extension",
"icon": "info",
"contributors": ["github_username"]
}

View file

@ -0,0 +1,10 @@
# async def m001_initial(db):
# await db.execute(
# f"""
# CREATE TABLE example.example (
# id TEXT PRIMARY KEY,
# wallet TEXT NOT NULL,
# time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
# );
# """
# )

View file

@ -0,0 +1,5 @@
# from pydantic import BaseModel
# class Example(BaseModel):
# id: str
# wallet: str

View file

@ -0,0 +1,59 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<q-card>
<q-card-section>
<h5 class="text-subtitle1 q-mt-none q-mb-md">
Frameworks used by {{SITE_TITLE}}
</h5>
<q-list>
<q-item
v-for="tool in tools"
:key="tool.name"
tag="a"
:href="tool.url"
target="_blank"
>
{% raw %}
<!-- with raw Flask won't try to interpret the Vue moustaches -->
<q-item-section>
<q-item-label>{{ tool.name }}</q-item-label>
<q-item-label caption>{{ tool.language }}</q-item-label>
</q-item-section>
{% endraw %}
</q-item>
</q-list>
<q-separator class="q-my-lg"></q-separator>
<p>
A magical "g" is always available, with info about the user, wallets and
extensions:
</p>
<code class="text-caption">{% raw %}{{ g }}{% endraw %}</code>
</q-card-section>
</q-card>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
tools: []
}
},
created: function () {
var self = this
// axios is available for making requests
axios({
method: 'GET',
url: '/example/api/v1/tools',
headers: {
'X-example-header': 'not-used'
}
}).then(function (response) {
self.tools = response.data
})
}
})
</script>
{% endblock %}

View file

@ -0,0 +1,18 @@
from fastapi import FastAPI, Request
from fastapi.params import Depends
from fastapi.templating import Jinja2Templates
from starlette.responses import HTMLResponse
from lnbits.core.models import User
from lnbits.decorators import check_user_exists
from . import example_ext, example_renderer
templates = Jinja2Templates(directory="templates")
@example_ext.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)):
return example_renderer().TemplateResponse(
"example/index.html", {"request": request, "user": user.dict()}
)

View file

@ -0,0 +1,35 @@
# views_api.py is for you API endpoints that could be hit by another service
# add your dependencies here
# import httpx
# (use httpx just like requests, except instead of response.ok there's only the
# response.is_error that is its inverse)
from . import example_ext
# add your endpoints here
@example_ext.get("/api/v1/tools")
async def api_example():
"""Try to add descriptions for others."""
tools = [
{
"name": "fastAPI",
"url": "https://fastapi.tiangolo.com/",
"language": "Python",
},
{
"name": "Vue.js",
"url": "https://vuejs.org/",
"language": "JavaScript",
},
{
"name": "Quasar Framework",
"url": "https://quasar.dev/",
"language": "JavaScript",
},
]
return tools

View file

@ -12,7 +12,7 @@ db = Database("ext_jukebox")
jukebox_static_files = [ jukebox_static_files = [
{ {
"path": "/jukebox/static", "path": "/jukebox/static",
"app": StaticFiles(directory="lnbits/extensions/jukebox/static"), "app": StaticFiles(packages=[("lnbits", "extensions/jukebox/static")]),
"name": "jukebox_static", "name": "jukebox_static",
} }
] ]

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

@ -3,7 +3,7 @@
Vue.component(VueQrcode.name, VueQrcode) Vue.component(VueQrcode.name, VueQrcode)
var mapJukebox = obj => { var mapJukebox = obj => {
if(obj.sp_device){ if (obj.sp_device) {
obj._data = _.clone(obj) obj._data = _.clone(obj)
obj.sp_id = obj._data.id obj.sp_id = obj._data.id
@ -17,11 +17,9 @@ var mapJukebox = obj => {
obj.playlist = playlistsar.join() obj.playlist = playlistsar.join()
console.log(obj) console.log(obj)
return obj return obj
} } else {
else {
return return
} }
} }
new Vue({ new Vue({
@ -103,7 +101,6 @@ new Vue({
) )
.then(function (response) { .then(function (response) {
self.JukeboxLinks = response.data.map(function (obj) { self.JukeboxLinks = response.data.map(function (obj) {
return mapJukebox(obj) return mapJukebox(obj)
}) })
console.log(self.JukeboxLinks) console.log(self.JukeboxLinks)

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

@ -24,6 +24,8 @@
label="API info" label="API info"
:content-inset-level="0.5" :content-inset-level="0.5"
> >
<q-btn flat label="Swagger API" type="a" href="../docs#/jukebox"></q-btn>
<q-expansion-item group="api" dense expand-separator label="List jukeboxes"> <q-expansion-item group="api" dense expand-separator label="List jukeboxes">
<q-card> <q-card>
<q-card-section> <q-card-section>
@ -37,8 +39,8 @@
<code>[&lt;jukebox_object&gt;, ...]</code> <code>[&lt;jukebox_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X GET {{ request.base_url }}api/v1/jukebox -H "X-Api-Key: {{ >curl -X GET {{ request.base_url }}jukebox/api/v1/jukebox -H
user.wallets[0].adminkey }}" "X-Api-Key: {{ user.wallets[0].adminkey }}"
</code> </code>
</q-card-section> </q-card-section>
</q-card> </q-card>
@ -59,8 +61,9 @@
<code>&lt;jukebox_object&gt;</code> <code>&lt;jukebox_object&gt;</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X GET {{ request.base_url }}api/v1/jukebox/&lt;juke_id&gt; -H >curl -X GET {{ request.base_url
"X-Api-Key: {{ user.wallets[0].adminkey }}" }}jukebox/api/v1/jukebox/&lt;juke_id&gt; -H "X-Api-Key: {{
user.wallets[0].adminkey }}"
</code> </code>
</q-card-section> </q-card-section>
</q-card> </q-card>
@ -86,8 +89,8 @@
<code>&lt;jukbox_object&gt;</code> <code>&lt;jukbox_object&gt;</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X POST {{ request.base_url }}api/v1/jukebox/ -d '{"user": >curl -X POST {{ request.base_url }}jukebox/api/v1/jukebox/ -d
&lt;string, user_id&gt;, "title": &lt;string&gt;, '{"user": &lt;string, user_id&gt;, "title": &lt;string&gt;,
"wallet":&lt;string&gt;, "sp_user": &lt;string, "wallet":&lt;string&gt;, "sp_user": &lt;string,
spotify_user_account&gt;, "sp_secret": &lt;string, spotify_user_account&gt;, "sp_secret": &lt;string,
spotify_user_secret&gt;, "sp_access_token": &lt;string, spotify_user_secret&gt;, "sp_access_token": &lt;string,
@ -116,8 +119,9 @@
<code>&lt;jukebox_object&gt;</code> <code>&lt;jukebox_object&gt;</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X DELETE {{ request.base_url }}api/v1/jukebox/&lt;juke_id&gt; >curl -X DELETE {{ request.base_url
-H "X-Api-Key: {{ user.wallets[0].adminkey }}" }}jukebox/api/v1/jukebox/&lt;juke_id&gt; -H "X-Api-Key: {{
user.wallets[0].adminkey }}"
</code> </code>
</q-card-section> </q-card-section>
</q-card> </q-card>

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

@ -455,5 +455,6 @@ async def api_get_jukebox_currently(
) )
except: except:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Something went wrong, or no song is playing yet" status_code=HTTPStatus.NOT_FOUND,
detail="Something went wrong, or no song is playing yet",
) )

View file

@ -12,7 +12,7 @@ db = Database("ext_livestream")
livestream_static_files = [ livestream_static_files = [
{ {
"path": "/livestream/static", "path": "/livestream/static",
"app": StaticFiles(directory="lnbits/extensions/livestream/static"), "app": StaticFiles(packages=[("lnbits", "extensions/livestream/static")]),
"name": "livestream_static", "name": "livestream_static",
} }
] ]

View file

@ -1,6 +1,8 @@
import asyncio import asyncio
import json import json
from loguru import logger
from lnbits.core import db as core_db from lnbits.core import db as core_db
from lnbits.core.crud import create_payment from lnbits.core.crud import create_payment
from lnbits.core.models import Payment from lnbits.core.models import Payment
@ -20,17 +22,17 @@ 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
track = await get_track(payment.extra.get("track", -1)) track = await get_track(payment.extra.get("track", -1))
if not track: if not track:
print("this should never happen", payment) logger.error("this should never happen", payment)
return return
if payment.extra.get("shared_with"): if payment.extra.get("shared_with"):
print("payment was shared already", payment) logger.error("payment was shared already", payment)
return return
producer = await get_producer(track.producer) producer = await get_producer(track.producer)

View file

@ -17,6 +17,8 @@
label="API info" label="API info"
:content-inset-level="0.5" :content-inset-level="0.5"
> >
<q-btn flat label="Swagger API" type="a" href="../docs#/livestream"></q-btn>
<q-expansion-item <q-expansion-item
group="api" group="api"
dense dense
@ -38,8 +40,8 @@
<code>[&lt;livestream_object&gt;, ...]</code> <code>[&lt;livestream_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X GET {{ request.url_root }}api/v1/livestream -H "X-Api-Key: {{ >curl -X GET {{ request.base_url }}livestream/api/v1/livestream -H
user.wallets[0].inkey }}" "X-Api-Key: {{ user.wallets[0].inkey }}"
</code> </code>
</q-card-section> </q-card-section>
</q-card> </q-card>
@ -59,8 +61,8 @@
</h5> </h5>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X PUT {{ request.url_root >curl -X PUT {{ request.base_url }}
}}api/v1/livestream/track/&lt;track_id&gt; -H "X-Api-Key: {{ livestream/api/v1/livestream/track/&lt;track_id&gt; -H "X-Api-Key: {{
user.wallets[0].inkey }}" user.wallets[0].inkey }}"
</code> </code>
</q-card-section> </q-card-section>
@ -81,8 +83,8 @@
</h5> </h5>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X PUT {{ request.url_root >curl -X PUT {{ request.base_url }}
}}api/v1/livestream/fee/&lt;fee_pct&gt; -H "X-Api-Key: {{ livestream/api/v1/livestream/fee/&lt;fee_pct&gt; -H "X-Api-Key: {{
user.wallets[0].inkey }}" user.wallets[0].inkey }}"
</code> </code>
</q-card-section> </q-card-section>
@ -109,11 +111,12 @@
</h5> </h5>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X POST {{ request.url_root }}api/v1/livestream/tracks -d >curl -X POST {{ request.base_url }}
'{"name": &lt;string&gt;, "download_url": &lt;string&gt;, livestream/api/v1/livestream/tracks -d '{"name": &lt;string&gt;,
"price_msat": &lt;integer&gt;, "producer_id": &lt;integer&gt;, "download_url": &lt;string&gt;, "price_msat": &lt;integer&gt;,
"producer_name": &lt;string&gt;}' -H "Content-type: application/json" "producer_id": &lt;integer&gt;, "producer_name": &lt;string&gt;}' -H
-H "X-Api-Key: {{ user.wallets[0].adminkey }}" "Content-type: application/json" -H "X-Api-Key: {{
user.wallets[0].adminkey }}"
</code> </code>
</q-card-section> </q-card-section>
</q-card> </q-card>
@ -123,6 +126,7 @@
dense dense
expand-separator expand-separator
label="Delete a withdraw link" label="Delete a withdraw link"
class="q-pb-md"
> >
<q-card> <q-card>
<q-card-section> <q-card-section>
@ -136,8 +140,8 @@
<code></code> <code></code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X DELETE {{ request.url_root >curl -X DELETE {{ request.base_url }}
}}api/v1/livestream/tracks/&lt;track_id&gt; -H "X-Api-Key: {{ livestream/api/v1/livestream/tracks/&lt;track_id&gt; -H "X-Api-Key: {{
user.wallets[0].inkey }}" user.wallets[0].inkey }}"
</code> </code>
</q-card-section> </q-card-section>

View file

@ -1,5 +1,4 @@
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
@ -14,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

@ -1,6 +1,8 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import List, Optional, Union from typing import List, Optional, Union
from loguru import logger
from lnbits.helpers import urlsafe_short_hash from lnbits.helpers import urlsafe_short_hash
from . import db from . import db
@ -186,9 +188,9 @@ async def purge_addresses(domain_id: str):
) # give user 1 day to topup is address ) # give user 1 day to topup is address
if not paid and pay_expire: if not paid and pay_expire:
print("DELETE UNP_PAY_EXP", r["username"]) logger.debug("DELETE UNP_PAY_EXP", r["username"])
await delete_address(r["id"]) await delete_address(r["id"])
if paid and expired: if paid and expired:
print("DELETE PAID_EXP", r["username"]) logger.debug("DELETE PAID_EXP", r["username"])
await delete_address(r["id"]) await delete_address(r["id"])

View file

@ -9,6 +9,7 @@ from lnurl import ( # type: ignore
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
@ -38,13 +39,12 @@ async def lnurl_response(username: str, domain: str, request: Request):
"maxSendable": 1000000000, "maxSendable": 1000000000,
} }
print("RESP", resp) logger.debug("RESP", resp)
return resp return resp
@lnaddress_ext.get("/lnurl/cb/{address_id}", name="lnaddress.lnurl_callback") @lnaddress_ext.get("/lnurl/cb/{address_id}", name="lnaddress.lnurl_callback")
async def lnurl_callback(address_id, amount: int = Query(...)): async def lnurl_callback(address_id, amount: int = Query(...)):
print("PING")
address = await get_address(address_id) address = await get_address(address_id)
if not address: if not address:
return LnurlErrorResponse(reason=f"Address not found").dict() return LnurlErrorResponse(reason=f"Address not found").dict()

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

@ -31,6 +31,7 @@
label="API info" label="API info"
:content-inset-level="0.5" :content-inset-level="0.5"
> >
<q-btn flat label="Swagger API" type="a" href="../docs#/lnaddress"></q-btn>
<q-expansion-item group="api" dense expand-separator label="GET domains"> <q-expansion-item group="api" dense expand-separator label="GET domains">
<q-card> <q-card>
<q-card-section> <q-card-section>
@ -45,7 +46,7 @@
<code>JSON list of users</code> <code>JSON list of users</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X GET {{ request.url_root }}lnaddress/api/v1/domains -H >curl -X GET {{ request.base_url }}lnaddress/api/v1/domains -H
"X-Api-Key: {{ user.wallets[0].inkey }}" "X-Api-Key: {{ user.wallets[0].inkey }}"
</code> </code>
</q-card-section> </q-card-section>
@ -81,7 +82,7 @@
> >
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X POST {{ request.url_root }}lnaddress/api/v1/domains -d >curl -X POST {{ request.base_url }}lnaddress/api/v1/domains -d
'{"wallet": "{{ user.wallets[0].id }}", "domain": &lt;string&gt;, '{"wallet": "{{ user.wallets[0].id }}", "domain": &lt;string&gt;,
"cf_token": &lt;string&gt;,"cf_zone_id": &lt;string&gt;,"webhook": "cf_token": &lt;string&gt;,"cf_zone_id": &lt;string&gt;,"webhook":
&lt;Optional string&gt; ,"cost": &lt;integer&gt;}' -H "X-Api-Key: {{ &lt;Optional string&gt; ,"cost": &lt;integer&gt;}' -H "X-Api-Key: {{
@ -101,7 +102,7 @@
<code>{"X-Api-Key": &lt;string&gt;}</code> <code>{"X-Api-Key": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X DELETE {{ request.url_root >curl -X DELETE {{ request.base_url
}}lnaddress/api/v1/domains/&lt;domain_id&gt; -H "X-Api-Key: {{ }}lnaddress/api/v1/domains/&lt;domain_id&gt; -H "X-Api-Key: {{
user.wallets[0].inkey }}" user.wallets[0].inkey }}"
</code> </code>
@ -122,7 +123,7 @@
<code>JSON list of addresses</code> <code>JSON list of addresses</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X GET {{ request.url_root }}lnaddress/api/v1/addresses -H >curl -X GET {{ request.base_url }}lnaddress/api/v1/addresses -H
"X-Api-Key: {{ user.wallets[0].inkey }}" "X-Api-Key: {{ user.wallets[0].inkey }}"
</code> </code>
</q-card-section> </q-card-section>
@ -142,14 +143,20 @@
<code>JSON list of addresses</code> <code>JSON list of addresses</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X GET {{ request.url_root >curl -X GET {{ request.base_url
}}lnaddress/api/v1/address/&lt;domain&gt;/&lt;username&gt;/&lt;wallet_key&gt; }}lnaddress/api/v1/address/&lt;domain&gt;/&lt;username&gt;/&lt;wallet_key&gt;
-H "X-Api-Key: {{ user.wallets[0].inkey }}" -H "X-Api-Key: {{ user.wallets[0].inkey }}"
</code> </code>
</q-card-section> </q-card-section>
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="POST address"> <q-expansion-item
group="api"
dense
expand-separator
label="POST address"
class="q-pb-md"
>
<q-card> <q-card>
<q-card-section> <q-card-section>
<code <code
@ -160,7 +167,7 @@
<code>{"X-Api-Key": &lt;string&gt;}</code> <code>{"X-Api-Key": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X POST {{ request.url_root >curl -X POST {{ request.base_url
}}lnaddress/api/v1/address/&lt;domain_id&gt; -d '{"domain": }}lnaddress/api/v1/address/&lt;domain_id&gt; -d '{"domain":
&lt;string&gt;, "username": &lt;string&gt;,"email": &lt;Optional &lt;string&gt;, "username": &lt;string&gt;,"email": &lt;Optional
string&gt;, "wallet_endpoint": &lt;string&gt;, "wallet_key": string&gt;, "wallet_endpoint": &lt;string&gt;, "wallet_key":

View file

@ -192,7 +192,11 @@
label="Cloudflare API token" label="Cloudflare API token"
> >
<template v-slot:hint> <template v-slot:hint>
Check extension <a href="https://github.com/lnbits/lnbits-legend/tree/master/lnbits/extensions/lnaddress">documentation!</a> Check extension
<a
href="https://github.com/lnbits/lnbits-legend/blob/main/lnbits/extensions/lnaddress/README.md"
>documentation!</a
>
</template> </template>
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left" <q-tooltip class="bg-grey-8" anchor="bottom left" self="top left"
>Your API key in cloudflare</q-tooltip >Your API key in cloudflare</q-tooltip

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

@ -31,5 +31,6 @@
</li> </li>
</ul> </ul>
</q-card-section> </q-card-section>
<q-btn flat label="Swagger API" type="a" href="../docs#/lndhub"></q-btn>
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>

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

@ -1,5 +1,7 @@
import asyncio import asyncio
from loguru import logger
from lnbits.core.models import Payment from lnbits.core.models import Payment
from lnbits.tasks import register_invoice_listener from lnbits.tasks import register_invoice_listener
@ -16,13 +18,13 @@ 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
ticket = await get_ticket(payment.checking_id) ticket = await get_ticket(payment.checking_id)
if not ticket: if not ticket:
print("this should never happen", payment) logger.error("this should never happen", payment)
return return
await payment.set_pending(False) await payment.set_pending(False)

View file

@ -19,4 +19,5 @@
</p> </p>
</q-card-section> </q-card-section>
</q-card> </q-card>
<q-btn flat label="Swagger API" type="a" href="../docs#/lnticket"></q-btn>
</q-expansion-item> </q-expansion-item>

View file

@ -145,11 +145,13 @@
icon="launch" icon="launch"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="ticketCard(props)" @click="ticketCard(props)"
><q-tooltip> Click to show ticket </q-tooltip></q-btn> ><q-tooltip> Click to show ticket </q-tooltip></q-btn
>
</q-td> </q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props"> <q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label == "Ticket" ? col.value.length > 20 ? `${col.value.substring(0, 20)}...` : col.value : col.value }} {{ col.label == "Ticket" ? col.value.length > 20 ?
`${col.value.substring(0, 20)}...` : col.value : col.value }}
</q-td> </q-td>
<q-td auto-width> <q-td auto-width>
@ -410,7 +412,7 @@
}) })
}) })
}, },
ticketCard(ticket){ ticketCard(ticket) {
this.ticketDialog.show = true this.ticketDialog.show = true
let {date, email, ltext, name} = ticket.row let {date, email, ltext, name} = ticket.row
this.ticketDialog.data = { this.ticketDialog.data = {
@ -469,7 +471,7 @@
}, },
updateformDialog: function (formId) { updateformDialog: function (formId) {
var link = _.findWhere(this.forms, {id: formId}) var link = _.findWhere(this.forms, {id: formId})
console.log("LINK", link) console.log('LINK', link)
this.formDialog.data.id = link.id this.formDialog.data.id = link.id
this.formDialog.data.wallet = link.wallet this.formDialog.data.wallet = link.wallet

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,
) )
@ -150,7 +146,7 @@ async def lnurl_v1_params(
"defaultDescription": device.title, "defaultDescription": device.title,
} }
price_msat = int(price_msat * ((device.profit / 100) + 1) / 1000) price_msat = int(price_msat * ((device.profit / 100) + 1) / 1000)
print(price_msat)
lnurldevicepayment = await create_lnurldevicepayment( lnurldevicepayment = await create_lnurldevicepayment(
deviceid=device.id, deviceid=device.id,
payload=p, payload=p,
@ -204,7 +200,7 @@ async def lnurl_callback(
extra={"tag": "withdraw"}, extra={"tag": "withdraw"},
) )
return {"status": "OK"} return {"status": "OK"}
print(lnurldevicepayment.sats)
payment_hash, payment_request = await create_invoice( payment_hash, payment_request = await create_invoice(
wallet_id=device.wallet, wallet_id=device.wallet,
amount=lnurldevicepayment.sats / 1000, amount=lnurldevicepayment.sats / 1000,

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