diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..51cee13c --- /dev/null +++ b/.dockerignore @@ -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 diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index b6a0d721..00000000 --- a/.editorconfig +++ /dev/null @@ -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 diff --git a/.env.example b/.env.example index 14a87d02..6ef60bc1 100644 --- a/.env.example +++ b/.env.example @@ -1,10 +1,8 @@ -QUART_APP=lnbits.app:create_app() -QUART_ENV=development -QUART_DEBUG=true - HOST=127.0.0.1 PORT=5000 +DEBUG=false + LNBITS_ALLOWED_USERS="" LNBITS_ADMIN_USERS="" # 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'" # Choose from mint, flamingo, freedom, salvador, autumn, monochrome, classic 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, # LndRestWallet, CLightningWallet, LNbitsWallet, SparkWallet, FakeWallet, EclairWallet diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index ad72b9d5..696d1aa2 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -custom: https://lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK +custom: https://legend.lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index baa938c6..876c8b8a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -2,9 +2,9 @@ name: codeql on: push: - branches: [master, ] + branches: [main, ] pull_request: - branches: [master] + branches: [main] schedule: - cron: '0 12 * * 5' @@ -19,10 +19,10 @@ jobs: - run: git checkout HEAD^2 if: ${{ github.event_name == 'pull_request' }} - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: javascript, python - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v2 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml index 61f03a2b..23d7ae3e 100644 --- a/.github/workflows/formatting.yml +++ b/.github/workflows/formatting.yml @@ -2,9 +2,9 @@ name: formatting on: push: - branches: [ master ] + branches: [ main ] pull_request: - branches: [ master ] + branches: [ main ] jobs: black: @@ -15,9 +15,22 @@ jobs: - run: python3 -m venv venv - run: ./venv/bin/pip install black - 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: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - run: npm install - - run: make checkprettier + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + - 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 diff --git a/.github/workflows/migrations.yml b/.github/workflows/migrations.yml new file mode 100644 index 00000000..08557bc1 --- /dev/null +++ b/.github/workflows/migrations.yml @@ -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 diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index bf90a8e3..4d6c6d4d 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -5,6 +5,7 @@ on: [push, pull_request] jobs: check: runs-on: ubuntu-latest + if: ${{ 'false' == 'true' }} # skip mypy for now steps: - uses: actions/checkout@v1 - uses: jpetrucciani/mypy-check@master diff --git a/.github/workflows/regtest.yml b/.github/workflows/regtest.yml new file mode 100644 index 00000000..f26e6c38 --- /dev/null +++ b/.github/workflows/regtest.yml @@ -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 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1d2826c9..218a557b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,15 +3,15 @@ name: tests on: [push, pull_request] jobs: - unit: + venv-sqlite: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.8] + python-version: [3.7, 3.8, 3.9] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -22,37 +22,67 @@ jobs: 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 requests trio mock + ./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock - name: Run tests run: make test - # build: - # runs-on: ubuntu-latest - # strategy: - # matrix: - # python-version: [3.7, 3.8] - # steps: - # - uses: actions/checkout@v2 - # - name: Set up Python ${{ matrix.python-version }} - # uses: actions/setup-python@v1 - # with: - # python-version: ${{ matrix.python-version }} - # - name: Install dependencies - # run: | - # python -m pip install --upgrade pip - # pip install -r requirements.txt - # - name: Test with pytest - # env: - # LNBITS_BACKEND_WALLET_CLASS: LNPayWallet - # LNBITS_FORCE_HTTPS: 0 - # LNPAY_API_ENDPOINT: https://api.lnpay.co/v1/ - # LNPAY_API_KEY: sak_gG5pSFZhFgOLHm26a8hcWvXKt98yd - # LNPAY_ADMIN_KEY: waka_HqWfOoNE0TPqmQHSYErbF4n9 - # LNPAY_INVOICE_KEY: waki_ZqFEbhrTyopuPlOZButZUw - # LNPAY_READ_KEY: wakr_6IyTaNrvSeu3jbojSWt4ou6h - # run: | - # pip install pytest pytest-cov - # pytest --cov=lnbits --cov-report=xml - # - name: Upload coverage to Codecov - # uses: codecov/codecov-action@v1 - # with: - # file: ./coverage.xml + venv-postgres: + runs-on: ubuntu-latest + services: + postgres: + image: postgres:latest + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + ports: + # maps tcp port 5432 on service container to the host + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + strategy: + matrix: + python-version: [3.8] + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + env: + VIRTUAL_ENV: ./venv + PATH: ${{ env.VIRTUAL_ENV }}/bin:${{ env.PATH }} + run: | + python -m venv ${{ env.VIRTUAL_ENV }} + ./venv/bin/python -m pip install --upgrade pip + ./venv/bin/pip install -r requirements.txt + ./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock + - name: Run 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 \ No newline at end of file diff --git a/.gitignore b/.gitignore index c5f1498c..c2a305e7 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,7 @@ __pycache__ .webassets-cache htmlcov test-reports -tests/data +tests/data/*.sqlite3 *.swo *.swp @@ -31,5 +31,10 @@ venv __bundle__ +coverage.xml node_modules lnbits/static/bundle.* +docker + +# Nix +*result* diff --git a/Dockerfile b/Dockerfile index 7b8e523d..f9eb1dd1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ ENV PATH="$VIRTUAL_ENV/bin:$PATH" # Install build deps 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 pip install wheel diff --git a/Makefile b/Makefile index 300b81aa..2873ae77 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ all: format check requirements.txt -format: prettier black +format: prettier isort black check: mypy checkprettier checkblack @@ -17,12 +17,18 @@ mypy: $(shell find lnbits -name "*.py") ./venv/bin/mypy lnbits/core ./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") ./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") ./venv/bin/black --check lnbits +checkisort: $(shell find lnbits -name "*.py") + ./venv/bin/isort --profile black --check-only lnbits + Pipfile.lock: Pipfile ./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 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 LNBITS_DATA_FOLDER="./tests/data" \ 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 diff --git a/Pipfile b/Pipfile index 6e738367..f8c42a9d 100644 --- a/Pipfile +++ b/Pipfile @@ -4,7 +4,7 @@ url = "https://pypi.org/simple" verify_ssl = true [requires] -python_version = "3.7" +python_version = "3.8" [packages] bitstring = "*" @@ -12,6 +12,7 @@ cerberus = "*" ecdsa = "*" environs = "*" lnurl = "==0.3.6" +loguru = "*" pyscss = "*" shortuuid = "*" typing-extensions = "*" @@ -27,13 +28,17 @@ asyncio = "*" fastapi = "*" uvicorn = {extras = ["standard"], version = "*"} sse-starlette = "*" -jinja2 = "3.0.1" +jinja2 = "==3.0.1" pyngrok = "*" -secp256k1 = "*" +secp256k1 = "==0.14.0" +cffi = "==1.15.0" pycryptodomex = "*" [dev-packages] black = "==20.8b1" pytest = "*" pytest-cov = "*" -mypy = "latest" +mypy = "*" +pytest-asyncio = "*" +requests = "*" +mock = "*" diff --git a/Pipfile.lock b/Pipfile.lock index e77de500..42d471c6 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,11 +1,11 @@ { "_meta": { "hash": { - "sha256": "3e19364434fd2db3748162ccc1f3b6bddcf7a382473069d15cee6eda5e07eef1" + "sha256": "503e9942306106e40621c59f37a3ab866b483f8c5f27b879c1c6783dca30949f" }, "pipfile-spec": 6, "requires": { - "python_version": "3.7" + "python_version": "3.8" }, "sources": [ { @@ -26,19 +26,10 @@ }, "anyio": { "hashes": [ - "sha256:a0aeffe2fb1fdf374a8e4b471444f0f3ac4fb9f5a5b542b48824475e0042a5a6", - "sha256:b5fa16c5ff93fa1046f2eeb5bbff2dad4d3514d6cda61d02816dba34fa8c3c2e" + "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b", + "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be" ], - "markers": "python_full_version >= '3.6.2'", - "version": "==3.5.0" - }, - "asgiref": { - "hashes": [ - "sha256:2f8abc20f7248433085eda803936d98992f1343ddb022065779f37c5da0181d0", - "sha256:88d59c13d634dcffe0510be048210188edd79aeccb6a6c9028cdad6f31d730a9" - ], - "markers": "python_version >= '3.7'", - "version": "==3.5.0" + "version": "==3.6.1" }, "asyncio": { "hashes": [ @@ -55,7 +46,6 @@ "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==21.4.0" }, "bech32": { @@ -63,7 +53,6 @@ "sha256:7d6db8214603bd7871fcfa6c0826ef68b85b0abd90fa21c285a9c5e21d2bd899", "sha256:990dc8e5a5e4feabbdf55207b5315fdd9b73db40be294a19b3752cde9e79d981" ], - "markers": "python_version >= '3.5'", "version": "==1.2.0" }, "bitstring": { @@ -84,10 +73,10 @@ }, "certifi": { "hashes": [ - "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", - "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" + "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d", + "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412" ], - "version": "==2021.10.8" + "version": "==2022.6.15" }, "cffi": { "hashes": [ @@ -142,38 +131,30 @@ "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997", "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796" ], + "index": "pypi", "version": "==1.15.0" }, - "charset-normalizer": { - "hashes": [ - "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", - "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" - ], - "markers": "python_version >= '3.5'", - "version": "==2.0.12" - }, "click": { "hashes": [ - "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3", - "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b" + "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", + "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" ], - "markers": "python_version >= '3.6'", - "version": "==8.0.3" + "version": "==8.1.3" }, "ecdsa": { "hashes": [ - "sha256:5cf31d5b33743abe0dfc28999036c849a69d548f994b535e527ee3cb7f3ef676", - "sha256:b9f500bb439e4153d0330610f5d26baaf18d17b8ced1bc54410d189385ea68aa" + "sha256:190348041559e21b22a1d65cee485282ca11a6f81d503fddb84d5017e9ed1e49", + "sha256:80600258e7ed2f16b9aa1d7c295bd70194109ad5a30fdee0eaeefef1d4c559dd" ], "index": "pypi", - "version": "==0.17.0" + "version": "==0.18.0" }, "embit": { "hashes": [ - "sha256:d340107dc1604581df59f844d4eb76ec34b0219c2ac2cbc1837c14938a4730ee" + "sha256:5644ae6ed07bb71bf7fb15daf7f5af73d889180e623f5ff1f35a20ad01f0405e" ], "index": "pypi", - "version": "==0.4.12" + "version": "==0.5.0" }, "environs": { "hashes": [ @@ -185,64 +166,72 @@ }, "fastapi": { "hashes": [ - "sha256:dcfee92a7f9a72b5d4b7ca364bd2b009f8fc10d95ed5769be20e94f39f7e5a15", - "sha256:f0a618aff5f6942862f2d3f20f39b1c037e33314d1b8207fd1c3a2cca76dfd8c" + "sha256:cf0ff6db25b91d321050c4112baab0908c90f19b40bf257f9591d2f9780d1f22", + "sha256:d337563424ceada23857f73d5abe8dae0c28e4cccb53b2af06e78b7bb4a1c7d7" ], "index": "pypi", - "version": "==0.73.0" + "version": "==0.79.0" }, "h11": { "hashes": [ "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6", "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042" ], - "markers": "python_version >= '3.6'", "version": "==0.12.0" }, "httpcore": { "hashes": [ - "sha256:47d772f754359e56dd9d892d9593b6f9870a37aeb8ba51e9a88b09b3d68cfade", - "sha256:7503ec1c0f559066e7e39bc4003fd2ce023d01cf51793e3c173b864eb456ead1" + "sha256:1105b8b73c025f23ff7c36468e4432226cbb959176eab66864b8e31c4ee27fa6", + "sha256:18b68ab86a3ccf3e7dc0f43598eaddcf472b602aba29f9aa6ab85fe2ada3980b" ], - "markers": "python_version >= '3.6'", - "version": "==0.14.7" + "version": "==0.15.0" }, "httptools": { "hashes": [ - "sha256:04114db99605c9b56ea22a8ec4d7b1485b908128ed4f4a8f6438489c428da794", - "sha256:074afd8afdeec0fa6786cd4a1676e0c0be23dc9a017a86647efa6b695168104f", - "sha256:113816f9af7dcfc4aa71ebb5354d77365f666ecf96ac7ff2aa1d24b6bca44165", - "sha256:1a8f26327023fa1a947d36e60a0582149e182fbbc949c8a65ec8665754dbbe69", - "sha256:2119fa619a4c53311f594f25c0205d619350fcb32140ec5057f861952e9b2b4f", - "sha256:21e948034f70e47c8abfa2d5e6f1a5661f87a2cddc7bcc70f61579cc87897c70", - "sha256:32a10a5903b5bc0eb647d01cd1e95bec3bb614a9bf53f0af1e01360b2debdf81", - "sha256:3787c1f46e9722ef7f07ea5c76b0103037483d1b12e34a02c53ceca5afa4e09a", - "sha256:3f82eb106e1474c63dba36a176067e65b48385f4cecddf3616411aa5d1fbdfec", - "sha256:3f9b4856d46ba1f0c850f4e84b264a9a8b4460acb20e865ec00978ad9fbaa4cf", - "sha256:4137137de8976511a392e27bfdcf231bd926ac13d375e0414e927b08217d779e", - "sha256:4687dfc116a9f1eb22a7d797f0dc6f6e17190d406ca4e729634b38aa98044b17", - "sha256:47dba2345aaa01b87e4981e8756af441349340708d5b60712c98c55a4d28f4af", - "sha256:5a836bd85ae1fb4304f674808488dae403e136d274aa5bafd0e6ee456f11c371", - "sha256:6e676bc3bb911b11f3d7e2144b9a53600bf6b9b21e0e4437aa308e1eef094d97", - "sha256:72ee0e3fb9c6437ab3ae34e9abee67fcee6876f4f58504e3f613dd5882aafdb7", - "sha256:79717080dc3f8b1eeb7f820b9b81528acbc04be6041f323fdd97550da2062575", - "sha256:8ac842df4fc3952efa7820b277961ea55e068bbc54cb59a0820400de7ae358d8", - "sha256:9f475b642c48b1b78584bdd12a5143e2c512485664331eade9c29ef769a17598", - "sha256:b8ac7dee63af4346e02b1e6d32202e3b5b3706a9928bec6da6d7a5b066217422", - "sha256:c0ac2e0ce6733c55858932e7d37fcc7b67ba6bb23e9648593c55f663de031b93", - "sha256:c14576b737d9e6e4f2a86af04918dbe9b62f57ce8102a8695c9a382dbe405c7f", - "sha256:cdc3975db86c29817e6d13df14e037c931fc893a710fb71097777a4147090068", - "sha256:eda95634027200f4b2a6d499e7c2e7fa9b8ee57e045dfda26958ea0af27c070b" + "sha256:1a99346ebcb801b213c591540837340bdf6fd060a8687518d01c607d338b7424", + "sha256:1ee0b459257e222b878a6c09ccf233957d3a4dcb883b0847640af98d2d9aac23", + "sha256:20a45bcf22452a10fa8d58b7dbdb474381f6946bf5b8933e3662d572bc61bae4", + "sha256:29bf97a5c532da9c7a04de2c7a9c31d1d54f3abd65a464119b680206bbbb1055", + "sha256:2c9a930c378b3d15d6b695fb95ebcff81a7395b4f9775c4f10a076beb0b2c1ff", + "sha256:2db44a0b294d317199e9f80123e72c6b005c55b625b57fae36de68670090fa48", + "sha256:3194f6d6443befa8d4db16c1946b2fc428a3ceb8ab32eb6f09a59f86104dc1a0", + "sha256:34d2903dd2a3dd85d33705b6fde40bf91fc44411661283763fd0746723963c83", + "sha256:48e48530d9b995a84d1d89ae6b3ec4e59ea7d494b150ac3bbc5e2ac4acce92cd", + "sha256:54bbd295f031b866b9799dd39cb45deee81aca036c9bff9f58ca06726f6494f1", + "sha256:5d1fe6b6661022fd6cac541f54a4237496b246e6f1c0a6b41998ee08a1135afe", + "sha256:645373c070080e632480a3d251d892cb795be3d3a15f86975d0f1aca56fd230d", + "sha256:6a1a7dfc1f9c78a833e2c4904757a0f47ce25d08634dd2a52af394eefe5f9777", + "sha256:701e66b59dd21a32a274771238025d58db7e2b6ecebbab64ceff51b8e31527ae", + "sha256:72aa3fbe636b16d22e04b5a9d24711b043495e0ecfe58080addf23a1a37f3409", + "sha256:7af6bdbd21a2a25d6784f6d67f44f5df33ef39b6159543b9f9064d365c01f919", + "sha256:7ee9f226acab9085037582c059d66769862706e8e8cd2340470ceb8b3850873d", + "sha256:7f7bfb74718f52d5ed47d608d507bf66d3bc01d4a8b3e6dd7134daaae129357b", + "sha256:8e2eb957787cbb614a0f006bfc5798ff1d90ac7c4dd24854c84edbdc8c02369e", + "sha256:903f739c9fb78dab8970b0f3ea51f21955b24b45afa77b22ff0e172fc11ef111", + "sha256:98993805f1e3cdb53de4eed02b55dcc953cdf017ba7bbb2fd89226c086a6d855", + "sha256:9967d9758df505975913304c434cb9ab21e2c609ad859eb921f2f615a038c8de", + "sha256:a113789e53ac1fa26edf99856a61e4c493868e125ae0dd6354cf518948fbbd5c", + "sha256:a522d12e2ddbc2e91842ffb454a1aeb0d47607972c7d8fc88bd0838d97fb8a2a", + "sha256:abe829275cdd4174b4c4e65ad718715d449e308d59793bf3a931ee1bf7e7b86c", + "sha256:c286985b5e194ca0ebb2908d71464b9be8f17cc66d6d3e330e8d5407248f56ad", + "sha256:cd1295f52971097f757edfbfce827b6dbbfb0f7a74901ee7d4933dff5ad4c9af", + "sha256:ceafd5e960b39c7e0d160a1936b68eb87c5e79b3979d66e774f0c77d4d8faaed", + "sha256:d1f27bb0f75bef722d6e22dc609612bfa2f994541621cd2163f8c943b6463dfe", + "sha256:d3a4e165ca6204f34856b765d515d558dc84f1352033b8721e8d06c3e44930c3", + "sha256:d9b90bf58f3ba04e60321a23a8723a1ff2a9377502535e70495e5ada8e6e6722", + "sha256:f72b5d24d6730035128b238decdc4c0f2104b7056a7ca55cf047c106842ec890", + "sha256:fcddfe70553be717d9745990dfdb194e22ee0f60eb8f48c0794e7bfeda30d2d5", + "sha256:fdb9f9ed79bc6f46b021b3319184699ba1a22410a82204e6e89c774530069683" ], - "version": "==0.3.0" + "version": "==0.4.0" }, "httpx": { "hashes": [ - "sha256:d8e778f76d9bbd46af49e7f062467e3157a5a3d2ae4876a4bbfd8a51ed9c9cb4", - "sha256:e35e83d1d2b9b2a609ef367cc4c1e66fd80b750348b20cc9e19d1952fc2ca3f6" + "sha256:42974f577483e1e932c3cdc3cd2303e883cbfba17fe228b0f63589764d7b9c4b", + "sha256:f28eac771ec9eb4866d3fb4ab65abd42d38c424739e80c08d8d20570de60b0ef" ], "index": "pypi", - "version": "==0.22.0" + "version": "==0.23.0" }, "idna": { "hashes": [ @@ -253,11 +242,11 @@ }, "jinja2": { "hashes": [ - "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8", - "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7" + "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4", + "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4" ], "index": "pypi", - "version": "==3.0.3" + "version": "==3.0.1" }, "lnurl": { "hashes": [ @@ -267,96 +256,79 @@ "index": "pypi", "version": "==0.3.6" }, + "loguru": { + "hashes": [ + "sha256:066bd06758d0a513e9836fd9c6b5a75bfb3fd36841f4b996bc60b547a309d41c", + "sha256:4e2414d534a2ab57573365b3e6d0234dfb1d84b68b7f3b948e6fb743860a77c3" + ], + "index": "pypi", + "version": "==0.6.0" + }, "markupsafe": { "hashes": [ - "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", - "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", - "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", - "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194", - "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", - "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", - "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724", - "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", - "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646", - "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", - "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6", - "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a", - "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6", - "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad", - "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", - "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38", - "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac", - "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", - "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6", - "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047", - "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", - "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", - "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b", - "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", - "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", - "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a", - "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", - "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1", - "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9", - "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864", - "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", - "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee", - "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f", - "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", - "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", - "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", - "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", - "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b", - "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", - "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86", - "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6", - "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", - "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", - "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", - "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28", - "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e", - "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", - "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", - "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f", - "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d", - "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", - "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", - "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145", - "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", - "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c", - "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1", - "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a", - "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207", - "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", - "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53", - "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd", - "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134", - "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85", - "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9", - "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", - "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", - "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", - "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", - "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" + "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003", + "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88", + "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5", + "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7", + "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a", + "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603", + "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1", + "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135", + "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247", + "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6", + "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601", + "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77", + "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02", + "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e", + "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63", + "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f", + "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980", + "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b", + "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812", + "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff", + "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96", + "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1", + "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925", + "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a", + "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6", + "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e", + "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f", + "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4", + "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f", + "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3", + "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c", + "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a", + "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417", + "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a", + "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a", + "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37", + "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452", + "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933", + "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a", + "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7" ], - "markers": "python_version >= '3.6'", - "version": "==2.0.1" + "version": "==2.1.1" }, "marshmallow": { "hashes": [ - "sha256:04438610bc6dadbdddb22a4a55bcc7f6f8099e69580b2e67f5a681933a1f4400", - "sha256:4c05c1684e0e97fe779c62b91878f173b937fe097b356cd82f793464f5bc6138" + "sha256:00040ab5ea0c608e8787137627a8efae97fabd60552a05dc889c888f814e75eb", + "sha256:635fb65a3285a31a30f276f30e958070f5214c7196202caa5c7ecf28f5274bc7" ], - "markers": "python_version >= '3.6'", - "version": "==3.14.1" + "version": "==3.17.0" }, "outcome": { "hashes": [ - "sha256:c7dd9375cfd3c12db9801d080a3b63d4b0a261aa996c4c13152380587288d958", - "sha256:e862f01d4e626e63e8f92c38d1f8d5546d3f9cce989263c521b2e7990d186967" + "sha256:6f82bd3de45da303cf1f771ecafa1633750a358436a8bb60e06a1ceb745d2672", + "sha256:c4ab89a56575d6d38a05aa16daeaa333109c1f96167aba8901ab18b6b5e0f7f5" ], - "markers": "python_version >= '3.6'", - "version": "==1.1.0" + "version": "==1.2.0" + }, + "packaging": { + "hashes": [ + "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", + "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" + ], + "version": "==21.3" }, "psycopg2-binary": { "hashes": [ @@ -429,77 +401,79 @@ }, "pycryptodomex": { "hashes": [ - "sha256:1ca8e1b4c62038bb2da55451385246f51f412c5f5eabd64812c01766a5989b4a", - "sha256:298c00ea41a81a491d5b244d295d18369e5aac4b61b77b2de5b249ca61cd6659", - "sha256:2aa887683eee493e015545bd69d3d21ac8d5ad582674ec98f4af84511e353e45", - "sha256:2ce76ed0081fd6ac8c74edc75b9d14eca2064173af79843c24fa62573263c1f2", - "sha256:3da13c2535b7aea94cc2a6d1b1b37746814c74b6e80790daddd55ca5c120a489", - "sha256:406ec8cfe0c098fadb18d597dc2ee6de4428d640c0ccafa453f3d9b2e58d29e2", - "sha256:4d0db8df9ffae36f416897ad184608d9d7a8c2b46c4612c6bc759b26c073f750", - "sha256:530756d2faa40af4c1f74123e1d889bd07feae45bac2fd32f259a35f7aa74151", - "sha256:77931df40bb5ce5e13f4de2bfc982b2ddc0198971fbd947776c8bb5050896eb2", - "sha256:797a36bd1f69df9e2798e33edb4bd04e5a30478efc08f9428c087f17f65a7045", - "sha256:8085bd0ad2034352eee4d4f3e2da985c2749cb7344b939f4d95ead38c2520859", - "sha256:8536bc08d130cae6dcba1ea689f2913dfd332d06113904d171f2f56da6228e89", - "sha256:a4d412eba5679ede84b41dbe48b1bed8f33131ab9db06c238a235334733acc5e", - "sha256:aebecde2adc4a6847094d3bd6a8a9538ef3438a5ea84ac1983fcb167db614461", - "sha256:b276cc4deb4a80f9dfd47a41ebb464b1fe91efd8b1b8620cf5ccf8b824b850d6", - "sha256:b5a185ae79f899b01ca49f365bdf15a45d78d9856f09b0de1a41b92afce1a07f", - "sha256:c4d8977ccda886d88dc3ca789de2f1adc714df912ff3934b3d0a3f3d777deafb", - "sha256:c5dd3ffa663c982d7f1be9eb494a8924f6d40e2e2f7d1d27384cfab1b2ac0662", - "sha256:ca88f2f7020002638276439a01ffbb0355634907d1aa5ca91f3dc0c2e44e8f3b", - "sha256:d2cce1c82a7845d7e2e8a0956c6b7ed3f1661c9acf18eb120fc71e098ab5c6fe", - "sha256:d709572d64825d8d59ea112e11cc7faf6007f294e9951324b7574af4251e4de8", - "sha256:da8db8374295fb532b4b0c467e66800ef17d100e4d5faa2bbbd6df35502da125", - "sha256:e36c7e3b5382cd5669cf199c4a04a0279a43b2a3bdd77627e9b89778ac9ec08c", - "sha256:e95a4a6c54d27a84a4624d2af8bb9ee178111604653194ca6880c98dcad92f48", - "sha256:ee835def05622e0c8b1435a906491760a43d0c462f065ec9143ec4b8d79f8bff", - "sha256:f75009715dcf4a3d680c2338ab19dac5498f8121173a929872950f4fb3a48fbf", - "sha256:f8524b8bc89470cec7ac51734907818d3620fb1637f8f8b542d650ebec42a126" + "sha256:04cc393045a8f19dd110c975e30f38ed7ab3faf21ede415ea67afebd95a22380", + "sha256:0776bfaf2c48154ab54ea45392847c1283d2fcf64e232e85565f858baedfc1fa", + "sha256:0fadb9f7fa3150577800eef35f62a8a24b9ddf1563ff060d9bd3af22d3952c8c", + "sha256:18e2ab4813883ae63396c0ffe50b13554b32bb69ec56f0afaf052e7a7ae0d55b", + "sha256:191e73bc84a8064ad1874dba0ebadedd7cce4dedee998549518f2c74a003b2e1", + "sha256:35a8f7afe1867118330e2e0e0bf759c409e28557fb1fc2fbb1c6c937297dbe9a", + "sha256:3709f13ca3852b0b07fc04a2c03b379189232b24007c466be0f605dd4723e9d4", + "sha256:4540904c09704b6f831059c0dfb38584acb82cb97b0125cd52688c1f1e3fffa6", + "sha256:463119d7d22d0fc04a0f9122e9d3e6121c6648bcb12a052b51bd1eed1b996aa2", + "sha256:46b3f05f2f7ac7841053da4e0f69616929ca3c42f238c405f6c3df7759ad2780", + "sha256:48697790203909fab02a33226fda546604f4e2653f9d47bc5d3eb40879fa7c64", + "sha256:5676a132169a1c1a3712edf25250722ebc8c9102aa9abd814df063ca8362454f", + "sha256:65204412d0c6a8e3c41e21e93a5e6054a74fea501afa03046a388cf042e3377a", + "sha256:67e1e6a92151023ccdfcfbc0afb3314ad30080793b4c27956ea06ab1fb9bcd8a", + "sha256:6f5b6ba8aefd624834bc177a2ac292734996bb030f9d1b388e7504103b6fcddf", + "sha256:7341f1bb2dadb0d1a0047f34c3a58208a92423cdbd3244d998e4b28df5eac0ed", + "sha256:78d9621cf0ea35abf2d38fa2ca6d0634eab6c991a78373498ab149953787e5e5", + "sha256:8eecdf9cdc7343001d047f951b9cc805cd68cb6cd77b20ea46af5bffc5bd3dfb", + "sha256:94c7b60e1f52e1a87715571327baea0733708ab4723346598beca4a3b6879794", + "sha256:996e1ba717077ce1e6d4849af7a1426f38b07b3d173b879e27d5e26d2e958beb", + "sha256:a07a64709e366c2041cd5cfbca592b43998bf4df88f7b0ca73dca37071ccf1bd", + "sha256:b6306403228edde6e289f626a3908a2f7f67c344e712cf7c0a508bab3ad9e381", + "sha256:b9279adc16e4b0f590ceff581f53a80179b02cba9056010d733eb4196134a870", + "sha256:c4cb9cb492ea7dcdf222a8d19a1d09002798ea516aeae8877245206d27326d86", + "sha256:dd452a5af7014e866206d41751886c9b4bf379a339fdf2dbfc7dd16c0fb4f8e0", + "sha256:e2b12968522a0358b8917fc7b28865acac002f02f4c4c6020fcb264d76bfd06d", + "sha256:e3164a18348bd53c69b4435ebfb4ac8a4076291ffa2a70b54f0c4b80c7834b1d", + "sha256:e47bf8776a7e15576887f04314f5228c6527b99946e6638cf2f16da56d260cab", + "sha256:f8be976cec59b11f011f790b88aca67b4ea2bd286578d0bd3e31bcd19afcd3e4", + "sha256:fc9bc7a9b79fe5c750fc81a307052f8daabb709bdaabb0fb18fb136b66b653b5" ], "index": "pypi", - "version": "==3.14.1" + "version": "==3.15.0" }, "pydantic": { "hashes": [ - "sha256:085ca1de245782e9b46cefcf99deecc67d418737a1fd3f6a4f511344b613a5b3", - "sha256:086254884d10d3ba16da0588604ffdc5aab3f7f09557b998373e885c690dd398", - "sha256:0b6037175234850ffd094ca77bf60fb54b08b5b22bc85865331dd3bda7a02fa1", - "sha256:0fe476769acaa7fcddd17cadd172b156b53546ec3614a4d880e5d29ea5fbce65", - "sha256:1d5278bd9f0eee04a44c712982343103bba63507480bfd2fc2790fa70cd64cf4", - "sha256:2cc6a4cb8a118ffec2ca5fcb47afbacb4f16d0ab8b7350ddea5e8ef7bcc53a16", - "sha256:2ee7e3209db1e468341ef41fe263eb655f67f5c5a76c924044314e139a1103a2", - "sha256:3011b975c973819883842c5ab925a4e4298dffccf7782c55ec3580ed17dc464c", - "sha256:3c3b035103bd4e2e4a28da9da7ef2fa47b00ee4a9cf4f1a735214c1bcd05e0f6", - "sha256:4c68c3bc88dbda2a6805e9a142ce84782d3930f8fdd9655430d8576315ad97ce", - "sha256:574936363cd4b9eed8acdd6b80d0143162f2eb654d96cb3a8ee91d3e64bf4cf9", - "sha256:5a79330f8571faf71bf93667d3ee054609816f10a259a109a0738dac983b23c3", - "sha256:5e48ef4a8b8c066c4a31409d91d7ca372a774d0212da2787c0d32f8045b1e034", - "sha256:6c5b77947b9e85a54848343928b597b4f74fc364b70926b3c4441ff52620640c", - "sha256:742645059757a56ecd886faf4ed2441b9c0cd406079c2b4bee51bcc3fbcd510a", - "sha256:7bdfdadb5994b44bd5579cfa7c9b0e1b0e540c952d56f627eb227851cda9db77", - "sha256:815ddebb2792efd4bba5488bc8fde09c29e8ca3227d27cf1c6990fc830fd292b", - "sha256:8b5ac0f1c83d31b324e57a273da59197c83d1bb18171e512908fe5dc7278a1d6", - "sha256:96f240bce182ca7fe045c76bcebfa0b0534a1bf402ed05914a6f1dadff91877f", - "sha256:a733965f1a2b4090a5238d40d983dcd78f3ecea221c7af1497b845a9709c1721", - "sha256:ab624700dc145aa809e6f3ec93fb8e7d0f99d9023b713f6a953637429b437d37", - "sha256:b2571db88c636d862b35090ccf92bf24004393f85c8870a37f42d9f23d13e032", - "sha256:bbbc94d0c94dd80b3340fc4f04fd4d701f4b038ebad72c39693c794fd3bc2d9d", - "sha256:c0727bda6e38144d464daec31dff936a82917f431d9c39c39c60a26567eae3ed", - "sha256:c556695b699f648c58373b542534308922c46a1cda06ea47bc9ca45ef5b39ae6", - "sha256:c86229333cabaaa8c51cf971496f10318c4734cf7b641f08af0a6fbf17ca3054", - "sha256:c8d7da6f1c1049eefb718d43d99ad73100c958a5367d30b9321b092771e96c25", - "sha256:c8e9dcf1ac499679aceedac7e7ca6d8641f0193c591a2d090282aaf8e9445a46", - "sha256:cb23bcc093697cdea2708baae4f9ba0e972960a835af22560f6ae4e7e47d33f5", - "sha256:d1e4c28f30e767fd07f2ddc6f74f41f034d1dd6bc526cd59e63a82fe8bb9ef4c", - "sha256:d9c9bdb3af48e242838f9f6e6127de9be7063aad17b32215ccc36a09c5cf1070", - "sha256:dee5ef83a76ac31ab0c78c10bd7d5437bfdb6358c95b91f1ba7ff7b76f9996a1", - "sha256:e0896200b6a40197405af18828da49f067c2fa1f821491bc8f5bde241ef3f7d7", - "sha256:f5a64b64ddf4c99fe201ac2724daada8595ada0d102ab96d019c1555c2d6441d", - "sha256:f947352c3434e8b937e3aa8f96f47bdfe6d92779e44bb3f41e4c213ba6a32145" + "sha256:02eefd7087268b711a3ff4db528e9916ac9aa18616da7bca69c1871d0b7a091f", + "sha256:059b6c1795170809103a1538255883e1983e5b831faea6558ef873d4955b4a74", + "sha256:0bf07cab5b279859c253d26a9194a8906e6f4a210063b84b433cf90a569de0c1", + "sha256:1542636a39c4892c4f4fa6270696902acb186a9aaeac6f6cf92ce6ae2e88564b", + "sha256:177071dfc0df6248fd22b43036f936cfe2508077a72af0933d0c1fa269b18537", + "sha256:18f3e912f9ad1bdec27fb06b8198a2ccc32f201e24174cec1b3424dda605a310", + "sha256:1dd8fecbad028cd89d04a46688d2fcc14423e8a196d5b0a5c65105664901f810", + "sha256:1ed987c3ff29fff7fd8c3ea3a3ea877ad310aae2ef9889a119e22d3f2db0691a", + "sha256:447d5521575f18e18240906beadc58551e97ec98142266e521c34968c76c8761", + "sha256:494f7c8537f0c02b740c229af4cb47c0d39840b829ecdcfc93d91dcbb0779892", + "sha256:4988c0f13c42bfa9ddd2fe2f569c9d54646ce84adc5de84228cfe83396f3bd58", + "sha256:4ce9ae9e91f46c344bec3b03d6ee9612802682c1551aaf627ad24045ce090761", + "sha256:5d93d4e95eacd313d2c765ebe40d49ca9dd2ed90e5b37d0d421c597af830c195", + "sha256:61b6760b08b7c395975d893e0b814a11cf011ebb24f7d869e7118f5a339a82e1", + "sha256:72ccb318bf0c9ab97fc04c10c37683d9eea952ed526707fabf9ac5ae59b701fd", + "sha256:79b485767c13788ee314669008d01f9ef3bc05db9ea3298f6a50d3ef596a154b", + "sha256:7eb57ba90929bac0b6cc2af2373893d80ac559adda6933e562dcfb375029acee", + "sha256:8bc541a405423ce0e51c19f637050acdbdf8feca34150e0d17f675e72d119580", + "sha256:969dd06110cb780da01336b281f53e2e7eb3a482831df441fb65dd30403f4608", + "sha256:985ceb5d0a86fcaa61e45781e567a59baa0da292d5ed2e490d612d0de5796918", + "sha256:9bcf8b6e011be08fb729d110f3e22e654a50f8a826b0575c7196616780683380", + "sha256:9ce157d979f742a915b75f792dbd6aa63b8eccaf46a1005ba03aa8a986bde34a", + "sha256:9f659a5ee95c8baa2436d392267988fd0f43eb774e5eb8739252e5a7e9cf07e0", + "sha256:a4a88dcd6ff8fd47c18b3a3709a89adb39a6373f4482e04c1b765045c7e282fd", + "sha256:a955260d47f03df08acf45689bd163ed9df82c0e0124beb4251b1290fa7ae728", + "sha256:a9af62e9b5b9bc67b2a195ebc2c2662fdf498a822d62f902bf27cccb52dbbf49", + "sha256:ae72f8098acb368d877b210ebe02ba12585e77bd0db78ac04a1ee9b9f5dd2166", + "sha256:b83ba3825bc91dfa989d4eed76865e71aea3a6ca1388b59fc801ee04c4d8d0d6", + "sha256:c11951b404e08b01b151222a1cb1a9f0a860a8153ce8334149ab9199cd198131", + "sha256:c320c64dd876e45254bdd350f0179da737463eea41c43bacbee9d8c9d1021f11", + "sha256:c8098a724c2784bf03e8070993f6d46aa2eeca031f8d8a048dff277703e6e193", + "sha256:d12f96b5b64bec3f43c8e82b4aab7599d0157f11c798c9f9c528a72b9e0b339a", + "sha256:e565a785233c2d03724c4dc55464559639b1ba9ecf091288dd47ad9c629433bd", + "sha256:f0f047e11febe5c3198ed346b507e1d010330d56ad615a7e0a89fae604065a0e", + "sha256:fe4670cb32ea98ffbf5a1262f14c3e102cccd92b1869df3bb09538158ba90fe6" ], - "markers": "python_full_version >= '3.6.1'", - "version": "==1.9.0" + "version": "==1.9.1" }, "pyngrok": { "hashes": [ @@ -508,12 +482,20 @@ "index": "pypi", "version": "==5.1.0" }, + "pyparsing": { + "hashes": [ + "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", + "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" + ], + "version": "==3.0.9" + }, "pypng": { "hashes": [ - "sha256:76f8a1539ec56451da7ab7121f12a361969fe0f2d48d703d198ce2a99d6c5afd" + "sha256:4a43e969b8f5aaafb2a415536c1a8ec7e341cd6a3f957fd5b5f32a4cfeed902c", + "sha256:739c433ba96f078315de54c0db975aee537cbc3e1d0ae4ed9aab0ca1e427e2c1" ], "index": "pypi", - "version": "==0.0.21" + "version": "==0.20220715.0" }, "pyqrcode": { "hashes": [ @@ -525,18 +507,17 @@ }, "pyscss": { "hashes": [ - "sha256:f1df571569021a23941a538eb154405dde80bed35dc1ea7c5f3e18e0144746bf" + "sha256:8f35521ffe36afa8b34c7d6f3195088a7057c185c2b8f15ee459ab19748669ff" ], "index": "pypi", - "version": "==1.3.7" + "version": "==1.4.0" }, "python-dotenv": { "hashes": [ - "sha256:32b2bdc1873fd3a3c346da1c6db83d0053c3c62f28f1f38516070c4c8971b1d3", - "sha256:a5de49a31e953b45ff2d2fd434bbc2670e8db5273606c1e737cc6b93eff3655f" + "sha256:b7e3b04a59693c42c36f9ab1cc2acc46fa5df8c78e178fc33a8d4cd05c8d498f", + "sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938" ], - "markers": "python_full_version >= '3.5.0'", - "version": "==0.19.2" + "version": "==0.20.0" }, "pyyaml": { "hashes": [ @@ -581,13 +562,9 @@ "sha256:026c0de2ee8385d1255b9c2426cd4f03fe9177ac94c09979bc601946c8493aa0", "sha256:99142650756ef1998ce0661568f54a47dac8c638fb27e3816c02536575dbba8c" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.6.0.post0" }, "rfc3986": { - "extras": [ - "idna2008" - ], "hashes": [ "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835", "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97" @@ -625,18 +602,17 @@ }, "shortuuid": { "hashes": [ - "sha256:44a7a86bcf24dbaba2e626cf80c779926b7c3a0d31a3a013e0d3cd1077707d23", - "sha256:9435e87e5a64f3b92f7110c81f989a3b7bdb9358e22d2359829167da476cfc23" + "sha256:459f12fa1acc34ff213b1371467c0325169645a31ed989e268872339af7563d5", + "sha256:b2bb9eb7773170e253bb7ba25971023acb473517a8b76803d9618668cb1dd46f" ], "index": "pypi", - "version": "==1.0.8" + "version": "==1.0.9" }, "six": { "hashes": [ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.16.0" }, "sniffio": { @@ -644,7 +620,6 @@ "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663", "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de" ], - "markers": "python_version >= '3.5'", "version": "==1.2.0" }, "sqlalchemy": { @@ -693,11 +668,11 @@ }, "sqlalchemy-aio": { "hashes": [ - "sha256:7f77366f55d34891c87386dd0962a28b948b684e8ea5edb7daae4187c0b291bf", - "sha256:f767320427c22c66fa5840a1f17f3261110a8ddc8560558f4fbf12d31a66b17b" + "sha256:3f4aa392c38f032d6734826a4138a0f02ed3122d442ed142be1e5964f2a33b60", + "sha256:f531c7982662d71dfc0b117e77bb2ed544e25cd5361e76cf9f5208edcfb71f7b" ], "index": "pypi", - "version": "==0.16.0" + "version": "==0.17.0" }, "sse-starlette": { "hashes": [ @@ -709,30 +684,26 @@ }, "starlette": { "hashes": [ - "sha256:26a18cbda5e6b651c964c12c88b36d9898481cd428ed6e063f5f29c418f73050", - "sha256:57eab3cc975a28af62f6faec94d355a410634940f10b30d68d31cb5ec1b44ae8" + "sha256:5a60c5c2d051f3a8eb546136aa0c9399773a689595e099e0877704d5888279bf", + "sha256:c6d21096774ecb9639acad41b86b7706e52ba3bf1dc13ea4ed9ad593d47e24c7" ], - "markers": "python_version >= '3.6'", - "version": "==0.17.1" + "version": "==0.19.1" }, "typing-extensions": { "hashes": [ - "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42", - "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2" + "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02", + "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6" ], "index": "pypi", - "version": "==4.1.1" + "version": "==4.3.0" }, "uvicorn": { - "extras": [ - "standard" - ], "hashes": [ - "sha256:25850bbc86195a71a6477b3e4b3b7b4c861fb687fb96912972ce5324472b1011", - "sha256:e85872d84fb651cccc4c5d2a71cf7ead055b8fb4d8f1e78e36092282c0cf2aec" + "sha256:c19a057deb1c5bb060946e2e5c262fc01590c6529c0af2c3d9ce941e89bc30e0", + "sha256:cade07c403c397f9fe275492a48c1b869efd175d5d8a692df649e6e7e2ed8f4e" ], "index": "pypi", - "version": "==0.17.4" + "version": "==0.18.2" }, "uvloop": { "hashes": [ @@ -755,65 +726,80 @@ ], "version": "==0.16.0" }, - "watchgod": { + "watchfiles": { "hashes": [ - "sha256:48140d62b0ebe9dd9cf8381337f06351e1f2e70b2203fa9c6eff4e572ca84f29", - "sha256:d6c1ea21df37847ac0537ca0d6c2f4cdf513562e95f77bb93abbcf05573407b7" + "sha256:059bd9596429f8c13604b2eb30888a5661b3c79099edc506f11b63be7afe3ca4", + "sha256:09490d258be8fdd7f5141a39b468dede0b4aa4a52f2b2dbfb0f3835ae7c23eca", + "sha256:1bb5f0117c8b93f8e1b22ac0be60cfeb00332959a72e6bbe2073fea27ed086e5", + "sha256:3d3f0397c9128971398a5cbb0fb45852ab2fa4472ac9724c031071e1e39970c0", + "sha256:43d1d517faffa8955c2da0e6f64268e38442d43b50ca73cb686df25f891e49a1", + "sha256:4f712dbe9d8c0365bf46ffe0dd9c6a62cc0acf05ba951f1a53de2b4d5bb63299", + "sha256:59498853d3214d1e4d9b1cb3a06b0011a11f24d31708b1734d9cd7f5a30fe1af", + "sha256:5e3d4c92091d16bca1d61920575dab5d6dcbceda76dccd5fb91da0b7390b4ee9", + "sha256:5fa786d102e7eabef22b2147af531aa70194aabcb35335be81c07c26382b0050", + "sha256:750e40db5efcf3f5f11602dbc6fdf8e96a0eefdbccd271093efe9fa2e9d02ed2", + "sha256:7c80e3907d21ca3f1689f42632d239fdc40ffc1d5f32f564997480f85e94c474", + "sha256:8d635dcba3aab2909bf568765547696d7465d30e2e9c6f5ab99da877b58d29bb", + "sha256:a5f64674559fac56a6bf2f5e086cb3758740140c80711fe3e016f5443b84ef15", + "sha256:bcd085980389bc64fe509188a9caffa4fe13b2616e2e3e674cde58f916b2a8ee", + "sha256:c9e3756cd2ba17e5042e8c9399a08e4bdbe1a366156a164e8373bda30ca096d0", + "sha256:cbdb7814ca43f85ab8569206ab2c3bcd51dd5d1ba582914246784414e6ada62e", + "sha256:d5fb4f3b5c884d4f22f643b0697edbb04942bcad961a8f9a9bfadb73e7a1e229" ], - "version": "==0.7" + "version": "==0.16.0" }, "websockets": { "hashes": [ - "sha256:002071169d2e44ce8eb9e5ebac9fbce142ba4b5146eef1cfb16b177a27662657", - "sha256:05e7f098c76b0a4743716590bb8f9706de19f1ef5148d61d0cf76495ec3edb9c", - "sha256:08a42856158307e231b199671c4fce52df5786dd3d703f36b5d8ac76b206c485", - "sha256:0d93b7cadc761347d98da12ec1930b5c71b2096f1ceed213973e3cda23fead9c", - "sha256:10edd9d7d3581cfb9ff544ac09fc98cab7ee8f26778a5a8b2d5fd4b0684c5ba5", - "sha256:14e9cf68a08d1a5d42109549201aefba473b1d925d233ae19035c876dd845da9", - "sha256:181d2b25de5a437b36aefedaf006ecb6fa3aa1328ec0236cdde15f32f9d3ff6d", - "sha256:189ed478395967d6a98bb293abf04e8815349e17456a0a15511f1088b6cb26e4", - "sha256:1d858fb31e5ac992a2cdf17e874c95f8a5b1e917e1fb6b45ad85da30734b223f", - "sha256:1dafe98698ece09b8ccba81b910643ff37198e43521d977be76caf37709cf62b", - "sha256:3477146d1f87ead8df0f27e8960249f5248dceb7c2741e8bbec9aa5338d0c053", - "sha256:38db6e2163b021642d0a43200ee2dec8f4980bdbda96db54fde72b283b54cbfc", - "sha256:3a02ab91d84d9056a9ee833c254895421a6333d7ae7fff94b5c68e4fa8095519", - "sha256:3bbf080f3892ba1dc8838786ec02899516a9d227abe14a80ef6fd17d4fb57127", - "sha256:3ef6f73854cded34e78390dbdf40dfdcf0b89b55c0e282468ef92646fce8d13a", - "sha256:468f0031fdbf4d643f89403a66383247eb82803430b14fa27ce2d44d2662ca37", - "sha256:483edee5abed738a0b6a908025be47f33634c2ad8e737edd03ffa895bd600909", - "sha256:531d8eb013a9bc6b3ad101588182aa9b6dd994b190c56df07f0d84a02b85d530", - "sha256:5560558b0dace8312c46aa8915da977db02738ac8ecffbc61acfbfe103e10155", - "sha256:5bb6256de5a4fb1d42b3747b4e2268706c92965d75d0425be97186615bf2f24f", - "sha256:667c41351a6d8a34b53857ceb8343a45c85d438ee4fd835c279591db8aeb85be", - "sha256:6b014875fae19577a392372075e937ebfebf53fd57f613df07b35ab210f31534", - "sha256:6fdec1a0b3e5630c58e3d8704d2011c678929fce90b40908c97dfc47de8dca72", - "sha256:7bdd3d26315db0a9cf8a0af30ca95e0aa342eda9c1377b722e71ccd86bc5d1dd", - "sha256:7c9407719f42cb77049975410490c58a705da6af541adb64716573e550e5c9db", - "sha256:7d6673b2753f9c5377868a53445d0c321ef41ff3c8e3b6d57868e72054bfce5f", - "sha256:816ae7dac2c6522cfa620947ead0ca95ac654916eebf515c94d7c28de5601a6e", - "sha256:882c0b8bdff3bf1bd7f024ce17c6b8006042ec4cceba95cf15df57e57efa471c", - "sha256:8877861e3dee38c8d302eee0d5dbefa6663de3b46dc6a888f70cd7e82562d1f7", - "sha256:888a5fa2a677e0c2b944f9826c756475980f1b276b6302e606f5c4ff5635be9e", - "sha256:89e985d40d407545d5f5e2e58e1fdf19a22bd2d8cd54d20a882e29f97e930a0a", - "sha256:97b4b68a2ddaf5c4707ae79c110bfd874c5be3c6ac49261160fb243fa45d8bbb", - "sha256:98de71f86bdb29430fd7ba9997f47a6b10866800e3ea577598a786a785701bb0", - "sha256:9f304a22ece735a3da8a51309bc2c010e23961a8f675fae46fdf62541ed62123", - "sha256:9fd62c6dc83d5d35fb6a84ff82ec69df8f4657fff05f9cd6c7d9bec0dd57f0f6", - "sha256:a249139abc62ef333e9e85064c27fefb113b16ffc5686cefc315bdaef3eefbc8", - "sha256:b66e6d514f12c28d7a2d80bb2a48ef223342e99c449782d9831b0d29a9e88a17", - "sha256:b68b6caecb9a0c6db537aa79750d1b592a841e4f1a380c6196091e65b2ad35f9", - "sha256:baa83174390c0ff4fc1304fbe24393843ac7a08fdd59295759c4b439e06b1536", - "sha256:bb01ea7b5f52e7125bdc3c5807aeaa2d08a0553979cf2d96a8b7803ea33e15e7", - "sha256:cfae282c2aa7f0c4be45df65c248481f3509f8c40ca8b15ed96c35668ae0ff69", - "sha256:d0d81b46a5c87d443e40ce2272436da8e6092aa91f5fbeb60d1be9f11eff5b4c", - "sha256:d9b245db5a7e64c95816e27d72830e51411c4609c05673d1ae81eb5d23b0be54", - "sha256:ddab2dc69ee5ae27c74dbfe9d7bb6fee260826c136dca257faa1a41d1db61a89", - "sha256:e1b60fd297adb9fc78375778a5220da7f07bf54d2a33ac781319650413fc6a60", - "sha256:e259be0863770cb91b1a6ccf6907f1ac2f07eff0b7f01c249ed751865a70cb0d", - "sha256:e3872ae57acd4306ecf937d36177854e218e999af410a05c17168cd99676c512", - "sha256:e4819c6fb4f336fd5388372cb556b1f3a165f3f68e66913d1a2fc1de55dc6f58" + "sha256:07cdc0a5b2549bcfbadb585ad8471ebdc7bdf91e32e34ae3889001c1c106a6af", + "sha256:210aad7fdd381c52e58777560860c7e6110b6174488ef1d4b681c08b68bf7f8c", + "sha256:28dd20b938a57c3124028680dc1600c197294da5db4292c76a0b48efb3ed7f76", + "sha256:2f94fa3ae454a63ea3a19f73b95deeebc9f02ba2d5617ca16f0bbdae375cda47", + "sha256:31564a67c3e4005f27815634343df688b25705cccb22bc1db621c781ddc64c69", + "sha256:347974105bbd4ea068106ec65e8e8ebd86f28c19e529d115d89bd8cc5cda3079", + "sha256:379e03422178436af4f3abe0aa8f401aa77ae2487843738542a75faf44a31f0c", + "sha256:3eda1cb7e9da1b22588cefff09f0951771d6ee9fa8dbe66f5ae04cc5f26b2b55", + "sha256:51695d3b199cd03098ae5b42833006a0f43dc5418d3102972addc593a783bc02", + "sha256:54c000abeaff6d8771a4e2cef40900919908ea7b6b6a30eae72752607c6db559", + "sha256:5b936bf552e4f6357f5727579072ff1e1324717902127ffe60c92d29b67b7be3", + "sha256:6075fd24df23133c1b078e08a9b04a3bc40b31a8def4ee0b9f2c8865acce913e", + "sha256:661f641b44ed315556a2fa630239adfd77bd1b11cb0b9d96ed8ad90b0b1e4978", + "sha256:6ea6b300a6bdd782e49922d690e11c3669828fe36fc2471408c58b93b5535a98", + "sha256:6ed1d6f791eabfd9808afea1e068f5e59418e55721db8b7f3bfc39dc831c42ae", + "sha256:7934e055fd5cd9dee60f11d16c8d79c4567315824bacb1246d0208a47eca9755", + "sha256:7ab36e17af592eec5747c68ef2722a74c1a4a70f3772bc661079baf4ae30e40d", + "sha256:7f6d96fdb0975044fdd7953b35d003b03f9e2bcf85f2d2cf86285ece53e9f991", + "sha256:83e5ca0d5b743cde3d29fda74ccab37bdd0911f25bd4cdf09ff8b51b7b4f2fa1", + "sha256:85506b3328a9e083cc0a0fb3ba27e33c8db78341b3eb12eb72e8afd166c36680", + "sha256:8af75085b4bc0b5c40c4a3c0e113fa95e84c60f4ed6786cbb675aeb1ee128247", + "sha256:8b1359aba0ff810d5830d5ab8e2c4a02bebf98a60aa0124fb29aa78cfdb8031f", + "sha256:8fbd7d77f8aba46d43245e86dd91a8970eac4fb74c473f8e30e9c07581f852b2", + "sha256:907e8247480f287aa9bbc9391bd6de23c906d48af54c8c421df84655eef66af7", + "sha256:93d5ea0b5da8d66d868b32c614d2b52d14304444e39e13a59566d4acb8d6e2e4", + "sha256:97bc9d41e69a7521a358f9b8e44871f6cdeb42af31815c17aed36372d4eec667", + "sha256:994cdb1942a7a4c2e10098d9162948c9e7b235df755de91ca33f6e0481366fdb", + "sha256:a141de3d5a92188234afa61653ed0bbd2dde46ad47b15c3042ffb89548e77094", + "sha256:a1e15b230c3613e8ea82c9fc6941b2093e8eb939dd794c02754d33980ba81e36", + "sha256:aad5e300ab32036eb3fdc350ad30877210e2f51bceaca83fb7fef4d2b6c72b79", + "sha256:b529fdfa881b69fe563dbd98acce84f3e5a67df13de415e143ef053ff006d500", + "sha256:b9c77f0d1436ea4b4dc089ed8335fa141e6a251a92f75f675056dac4ab47a71e", + "sha256:bb621ec2dbbbe8df78a27dbd9dd7919f9b7d32a73fafcb4d9252fc4637343582", + "sha256:c7250848ce69559756ad0086a37b82c986cd33c2d344ab87fea596c5ac6d9442", + "sha256:c8d1d14aa0f600b5be363077b621b1b4d1eb3fbf90af83f9281cda668e6ff7fd", + "sha256:d1655a6fc7aecd333b079d00fb3c8132d18988e47f19740c69303bf02e9883c6", + "sha256:d6353ba89cfc657a3f5beabb3b69be226adbb5c6c7a66398e17809b0ce3c4731", + "sha256:da4377904a3379f0c1b75a965fff23b28315bcd516d27f99a803720dfebd94d4", + "sha256:e49ea4c1a9543d2bd8a747ff24411509c29e4bdcde05b5b0895e2120cb1a761d", + "sha256:e4e08305bfd76ba8edab08dcc6496f40674f44eb9d5e23153efa0a35750337e8", + "sha256:e6fa05a680e35d0fcc1470cb070b10e6fe247af54768f488ed93542e71339d6f", + "sha256:e7e6f2d6fd48422071cc8a6f8542016f350b79cc782752de531577d35e9bd677", + "sha256:e904c0381c014b914136c492c8fa711ca4cced4e9b3d110e5e7d436d0fc289e8", + "sha256:ec2b0ab7edc8cd4b0eb428b38ed89079bdc20c6bdb5f889d353011038caac2f9", + "sha256:ef5ce841e102278c1c2e98f043db99d6755b1c58bde475516aef3a008ed7f28e", + "sha256:f351c7d7d92f67c0609329ab2735eee0426a03022771b00102816a72715bb00b", + "sha256:fab7c640815812ed5f10fbee7abbf58788d602046b7bb3af9b1ac753a6d5e916", + "sha256:fc06cc8073c8e87072138ba1e431300e2d408f054b27047d047b549455066ff4" ], - "version": "==10.1" + "version": "==10.3" } }, "develop": { @@ -829,7 +815,6 @@ "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==21.4.0" }, "black": { @@ -839,63 +824,79 @@ "index": "pypi", "version": "==20.8b1" }, + "certifi": { + "hashes": [ + "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d", + "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412" + ], + "version": "==2022.6.15" + }, + "charset-normalizer": { + "hashes": [ + "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5", + "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413" + ], + "version": "==2.1.0" + }, "click": { "hashes": [ - "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3", - "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b" + "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", + "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" ], - "markers": "python_version >= '3.6'", - "version": "==8.0.3" + "version": "==8.1.3" }, "coverage": { - "extras": [ - "toml" - ], "hashes": [ - "sha256:1245ab82e8554fa88c4b2ab1e098ae051faac5af829efdcf2ce6b34dccd5567c", - "sha256:1bc6d709939ff262fd1432f03f080c5042dc6508b6e0d3d20e61dd045456a1a0", - "sha256:25e73d4c81efa8ea3785274a2f7f3bfbbeccb6fcba2a0bdd3be9223371c37554", - "sha256:276b13cc085474e482566c477c25ed66a097b44c6e77132f3304ac0b039f83eb", - "sha256:2aed4761809640f02e44e16b8b32c1a5dee5e80ea30a0ff0912158bde9c501f2", - "sha256:2dd70a167843b4b4b2630c0c56f1b586fe965b4f8ac5da05b6690344fd065c6b", - "sha256:352c68e233409c31048a3725c446a9e48bbff36e39db92774d4f2380d630d8f8", - "sha256:3f2b05757c92ad96b33dbf8e8ec8d4ccb9af6ae3c9e9bd141c7cc44d20c6bcba", - "sha256:448d7bde7ceb6c69e08474c2ddbc5b4cd13c9e4aa4a717467f716b5fc938a734", - "sha256:463e52616ea687fd323888e86bf25e864a3cc6335a043fad6bbb037dbf49bbe2", - "sha256:482fb42eea6164894ff82abbcf33d526362de5d1a7ed25af7ecbdddd28fc124f", - "sha256:56c4a409381ddd7bbff134e9756077860d4e8a583d310a6f38a2315b9ce301d0", - "sha256:56d296cbc8254a7dffdd7bcc2eb70be5a233aae7c01856d2d936f5ac4e8ac1f1", - "sha256:5e15d424b8153756b7c903bde6d4610be0c3daca3986173c18dd5c1a1625e4cd", - "sha256:618eeba986cea7f621d8607ee378ecc8c2504b98b3fdc4952b30fe3578304687", - "sha256:61d47a897c1e91f33f177c21de897267b38fbb45f2cd8e22a710bcef1df09ac1", - "sha256:621f6ea7260ea2ffdaec64fe5cb521669984f567b66f62f81445221d4754df4c", - "sha256:6a5cdc3adb4f8bb8d8f5e64c2e9e282bc12980ef055ec6da59db562ee9bdfefa", - "sha256:6c3f6158b02ac403868eea390930ae64e9a9a2a5bbfafefbb920d29258d9f2f8", - "sha256:704f89b87c4f4737da2860695a18c852b78ec7279b24eedacab10b29067d3a38", - "sha256:72128176fea72012063200b7b395ed8a57849282b207321124d7ff14e26988e8", - "sha256:78fbb2be068a13a5d99dce9e1e7d168db880870f7bc73f876152130575bd6167", - "sha256:7bff3a98f63b47464480de1b5bdd80c8fade0ba2832c9381253c9b74c4153c27", - "sha256:84f2436d6742c01136dd940ee158bfc7cf5ced3da7e4c949662b8703b5cd8145", - "sha256:9976fb0a5709988778ac9bc44f3d50fccd989987876dfd7716dee28beed0a9fa", - "sha256:9ad0a117b8dc2061ce9461ea4c1b4799e55edceb236522c5b8f958ce9ed8fa9a", - "sha256:9e3dd806f34de38d4c01416344e98eab2437ac450b3ae39c62a0ede2f8b5e4ed", - "sha256:9eb494070aa060ceba6e4bbf44c1bc5fa97bfb883a0d9b0c9049415f9e944793", - "sha256:9fde6b90889522c220dd56a670102ceef24955d994ff7af2cb786b4ba8fe11e4", - "sha256:9fff3ff052922cb99f9e52f63f985d4f7a54f6b94287463bc66b7cdf3eb41217", - "sha256:a06c358f4aed05fa1099c39decc8022261bb07dfadc127c08cfbd1391b09689e", - "sha256:a4f923b9ab265136e57cc14794a15b9dcea07a9c578609cd5dbbfff28a0d15e6", - "sha256:c5b81fb37db76ebea79aa963b76d96ff854e7662921ce742293463635a87a78d", - "sha256:d5ed164af5c9078596cfc40b078c3b337911190d3faeac830c3f1274f26b8320", - "sha256:d651fde74a4d3122e5562705824507e2f5b2d3d57557f1916c4b27635f8fbe3f", - "sha256:de73fca6fb403dd72d4da517cfc49fcf791f74eee697d3219f6be29adf5af6ce", - "sha256:e647a0be741edbb529a72644e999acb09f2ad60465f80757da183528941ff975", - "sha256:e92c7a5f7d62edff50f60a045dc9542bf939758c95b2fcd686175dd10ce0ed10", - "sha256:eeffd96882d8c06d31b65dddcf51db7c612547babc1c4c5db6a011abe9798525", - "sha256:f5a4551dfd09c3bd12fca8144d47fe7745275adf3229b7223c2f9e29a975ebda", - "sha256:fac0bcc5b7e8169bffa87f0dcc24435446d329cbc2b5486d155c2e0f3b493ae1" + "sha256:0895ea6e6f7f9939166cc835df8fa4599e2d9b759b02d1521b574e13b859ac32", + "sha256:0f211df2cba951ffcae210ee00e54921ab42e2b64e0bf2c0befc977377fb09b7", + "sha256:147605e1702d996279bb3cc3b164f408698850011210d133a2cb96a73a2f7996", + "sha256:24b04d305ea172ccb21bee5bacd559383cba2c6fcdef85b7701cf2de4188aa55", + "sha256:25b7ec944f114f70803d6529394b64f8749e93cbfac0fe6c5ea1b7e6c14e8a46", + "sha256:2b20286c2b726f94e766e86a3fddb7b7e37af5d0c635bdfa7e4399bc523563de", + "sha256:2dff52b3e7f76ada36f82124703f4953186d9029d00d6287f17c68a75e2e6039", + "sha256:2f8553878a24b00d5ab04b7a92a2af50409247ca5c4b7a2bf4eabe94ed20d3ee", + "sha256:3def6791adf580d66f025223078dc84c64696a26f174131059ce8e91452584e1", + "sha256:422fa44070b42fef9fb8dabd5af03861708cdd6deb69463adc2130b7bf81332f", + "sha256:4f89d8e03c8a3757aae65570d14033e8edf192ee9298303db15955cadcff0c63", + "sha256:5336e0352c0b12c7e72727d50ff02557005f79a0b8dcad9219c7c4940a930083", + "sha256:54d8d0e073a7f238f0666d3c7c0d37469b2aa43311e4024c925ee14f5d5a1cbe", + "sha256:5ef42e1db047ca42827a85e34abe973971c635f83aed49611b7f3ab49d0130f0", + "sha256:5f65e5d3ff2d895dab76b1faca4586b970a99b5d4b24e9aafffc0ce94a6022d6", + "sha256:6c3ccfe89c36f3e5b9837b9ee507472310164f352c9fe332120b764c9d60adbe", + "sha256:6d0b48aff8e9720bdec315d67723f0babd936a7211dc5df453ddf76f89c59933", + "sha256:6fe75dcfcb889b6800f072f2af5a331342d63d0c1b3d2bf0f7b4f6c353e8c9c0", + "sha256:79419370d6a637cb18553ecb25228893966bd7935a9120fa454e7076f13b627c", + "sha256:7bb00521ab4f99fdce2d5c05a91bddc0280f0afaee0e0a00425e28e209d4af07", + "sha256:80db4a47a199c4563d4a25919ff29c97c87569130375beca3483b41ad5f698e8", + "sha256:866ebf42b4c5dbafd64455b0a1cd5aa7b4837a894809413b930026c91e18090b", + "sha256:8af6c26ba8df6338e57bedbf916d76bdae6308e57fc8f14397f03b5da8622b4e", + "sha256:a13772c19619118903d65a91f1d5fea84be494d12fd406d06c849b00d31bf120", + "sha256:a697977157adc052284a7160569b36a8bbec09db3c3220642e6323b47cec090f", + "sha256:a9032f9b7d38bdf882ac9f66ebde3afb8145f0d4c24b2e600bc4c6304aafb87e", + "sha256:b5e28db9199dd3833cc8a07fa6cf429a01227b5d429facb56eccd765050c26cd", + "sha256:c77943ef768276b61c96a3eb854eba55633c7a3fddf0a79f82805f232326d33f", + "sha256:d230d333b0be8042ac34808ad722eabba30036232e7a6fb3e317c49f61c93386", + "sha256:d4548be38a1c810d79e097a38107b6bf2ff42151900e47d49635be69943763d8", + "sha256:d4e7ced84a11c10160c0697a6cc0b214a5d7ab21dfec1cd46e89fbf77cc66fae", + "sha256:d56f105592188ce7a797b2bd94b4a8cb2e36d5d9b0d8a1d2060ff2a71e6b9bbc", + "sha256:d714af0bdba67739598849c9f18efdcc5a0412f4993914a0ec5ce0f1e864d783", + "sha256:d774d9e97007b018a651eadc1b3970ed20237395527e22cbeb743d8e73e0563d", + "sha256:e0524adb49c716ca763dbc1d27bedce36b14f33e6b8af6dba56886476b42957c", + "sha256:e2618cb2cf5a7cc8d698306e42ebcacd02fb7ef8cfc18485c59394152c70be97", + "sha256:e36750fbbc422c1c46c9d13b937ab437138b998fe74a635ec88989afb57a3978", + "sha256:edfdabe7aa4f97ed2b9dd5dde52d2bb29cb466993bb9d612ddd10d0085a683cf", + "sha256:f22325010d8824594820d6ce84fa830838f581a7fd86a9235f0d2ed6deb61e29", + "sha256:f23876b018dfa5d3e98e96f5644b109090f16a4acb22064e0f06933663005d39", + "sha256:f7bd0ffbcd03dc39490a1f40b2669cc414fae0c4e16b77bb26806a4d0b7d1452" ], - "markers": "python_version >= '3.7'", - "version": "==6.3.1" + "version": "==6.4.2" + }, + "idna": { + "hashes": [ + "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", + "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" + ], + "version": "==3.3" }, "iniconfig": { "hashes": [ @@ -904,31 +905,42 @@ ], "version": "==1.1.1" }, - "mypy": { + "mock": { "hashes": [ - "sha256:0038b21890867793581e4cb0d810829f5fd4441aa75796b53033af3aa30430ce", - "sha256:1171f2e0859cfff2d366da2c7092b06130f232c636a3f7301e3feb8b41f6377d", - "sha256:1b06268df7eb53a8feea99cbfff77a6e2b205e70bf31743e786678ef87ee8069", - "sha256:1b65714dc296a7991000b6ee59a35b3f550e0073411ac9d3202f6516621ba66c", - "sha256:1bf752559797c897cdd2c65f7b60c2b6969ffe458417b8d947b8340cc9cec08d", - "sha256:300717a07ad09525401a508ef5d105e6b56646f7942eb92715a1c8d610149714", - "sha256:3c5b42d0815e15518b1f0990cff7a705805961613e701db60387e6fb663fe78a", - "sha256:4365c60266b95a3f216a3047f1d8e3f895da6c7402e9e1ddfab96393122cc58d", - "sha256:50c7346a46dc76a4ed88f3277d4959de8a2bd0a0fa47fa87a4cde36fe247ac05", - "sha256:5b56154f8c09427bae082b32275a21f500b24d93c88d69a5e82f3978018a0266", - "sha256:74f7eccbfd436abe9c352ad9fb65872cc0f1f0a868e9d9c44db0893440f0c697", - "sha256:7b3f6f557ba4afc7f2ce6d3215d5db279bcf120b3cfd0add20a5d4f4abdae5bc", - "sha256:8c11003aaeaf7cc2d0f1bc101c1cc9454ec4cc9cb825aef3cafff8a5fdf4c799", - "sha256:8ca7f8c4b1584d63c9a0f827c37ba7a47226c19a23a753d52e5b5eddb201afcd", - "sha256:c89702cac5b302f0c5d33b172d2b55b5df2bede3344a2fbed99ff96bddb2cf00", - "sha256:d8f1ff62f7a879c9fe5917b3f9eb93a79b78aad47b533911b853a757223f72e7", - "sha256:d9d2b84b2007cea426e327d2483238f040c49405a6bf4074f605f0156c91a47a", - "sha256:e839191b8da5b4e5d805f940537efcaa13ea5dd98418f06dc585d2891d228cf0", - "sha256:f9fe20d0872b26c4bba1c1be02c5340de1019530302cf2dcc85c7f9fc3252ae0", - "sha256:ff3bf387c14c805ab1388185dd22d6b210824e164d4bb324b195ff34e322d166" + "sha256:122fcb64ee37cfad5b3f48d7a7d51875d7031aaf3d8be7c42e2bee25044eee62", + "sha256:7d3fbbde18228f4ff2f1f119a45cdffa458b4c0dee32eb4d2bb2f82554bac7bc" ], "index": "pypi", - "version": "==0.931" + "version": "==4.0.3" + }, + "mypy": { + "hashes": [ + "sha256:02ef476f6dcb86e6f502ae39a16b93285fef97e7f1ff22932b657d1ef1f28655", + "sha256:0d054ef16b071149917085f51f89555a576e2618d5d9dd70bd6eea6410af3ac9", + "sha256:19830b7dba7d5356d3e26e2427a2ec91c994cd92d983142cbd025ebe81d69cf3", + "sha256:1f7656b69974a6933e987ee8ffb951d836272d6c0f81d727f1d0e2696074d9e6", + "sha256:23488a14a83bca6e54402c2e6435467a4138785df93ec85aeff64c6170077fb0", + "sha256:23c7ff43fff4b0df93a186581885c8512bc50fc4d4910e0f838e35d6bb6b5e58", + "sha256:25c5750ba5609a0c7550b73a33deb314ecfb559c350bb050b655505e8aed4103", + "sha256:2ad53cf9c3adc43cf3bea0a7d01a2f2e86db9fe7596dfecb4496a5dda63cbb09", + "sha256:3fa7a477b9900be9b7dd4bab30a12759e5abe9586574ceb944bc29cddf8f0417", + "sha256:40b0f21484238269ae6a57200c807d80debc6459d444c0489a102d7c6a75fa56", + "sha256:4b21e5b1a70dfb972490035128f305c39bc4bc253f34e96a4adf9127cf943eb2", + "sha256:5a361d92635ad4ada1b1b2d3630fc2f53f2127d51cf2def9db83cba32e47c856", + "sha256:77a514ea15d3007d33a9e2157b0ba9c267496acf12a7f2b9b9f8446337aac5b0", + "sha256:855048b6feb6dfe09d3353466004490b1872887150c5bb5caad7838b57328cc8", + "sha256:9796a2ba7b4b538649caa5cecd398d873f4022ed2333ffde58eaf604c4d2cb27", + "sha256:98e02d56ebe93981c41211c05adb630d1d26c14195d04d95e49cd97dbc046dc5", + "sha256:b793b899f7cf563b1e7044a5c97361196b938e92f0a4343a5d27966a53d2ec71", + "sha256:d1ea5d12c8e2d266b5fb8c7a5d2e9c0219fedfeb493b7ed60cd350322384ac27", + "sha256:d2022bfadb7a5c2ef410d6a7c9763188afdb7f3533f22a0a32be10d571ee4bbe", + "sha256:d3348e7eb2eea2472db611486846742d5d52d1290576de99d59edeb7cd4a42ca", + "sha256:d744f72eb39f69312bc6c2abf8ff6656973120e2eb3f3ec4f758ed47e414a4bf", + "sha256:ef943c72a786b0f8d90fd76e9b39ce81fb7171172daf84bf43eaf937e9f220a9", + "sha256:f2899a3cbd394da157194f913a931edfd4be5f274a88041c9dc2d9cdcb1c315c" + ], + "index": "pypi", + "version": "==0.971" }, "mypy-extensions": { "hashes": [ @@ -942,7 +954,6 @@ "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" ], - "markers": "python_version >= '3.6'", "version": "==21.3" }, "pathspec": { @@ -957,7 +968,6 @@ "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" ], - "markers": "python_version >= '3.6'", "version": "==1.0.0" }, "py": { @@ -965,24 +975,30 @@ "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==1.11.0" }, "pyparsing": { "hashes": [ - "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea", - "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484" + "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", + "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" ], - "markers": "python_version >= '3.6'", - "version": "==3.0.7" + "version": "==3.0.9" }, "pytest": { "hashes": [ - "sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db", - "sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171" + "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c", + "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45" ], "index": "pypi", - "version": "==7.0.1" + "version": "==7.1.2" + }, + "pytest-asyncio": { + "hashes": [ + "sha256:7a97e37cfe1ed296e2e84941384bdd37c376453912d397ed39293e0916f521fa", + "sha256:ac4ebf3b6207259750bc32f4c1d8fcd7e79739edbc67ad0c58dd150b1d072fed" + ], + "index": "pypi", + "version": "==0.19.0" }, "pytest-cov": { "hashes": [ @@ -994,89 +1010,96 @@ }, "regex": { "hashes": [ - "sha256:04611cc0f627fc4a50bc4a9a2e6178a974c6a6a4aa9c1cca921635d2c47b9c87", - "sha256:0b5d6f9aed3153487252d00a18e53f19b7f52a1651bc1d0c4b5844bc286dfa52", - "sha256:0d2f5c3f7057530afd7b739ed42eb04f1011203bc5e4663e1e1d01bb50f813e3", - "sha256:11772be1eb1748e0e197a40ffb82fb8fd0d6914cd147d841d9703e2bef24d288", - "sha256:1333b3ce73269f986b1fa4d5d395643810074dc2de5b9d262eb258daf37dc98f", - "sha256:16f81025bb3556eccb0681d7946e2b35ff254f9f888cff7d2120e8826330315c", - "sha256:1a171eaac36a08964d023eeff740b18a415f79aeb212169080c170ec42dd5184", - "sha256:1d6301f5288e9bdca65fab3de6b7de17362c5016d6bf8ee4ba4cbe833b2eda0f", - "sha256:1e031899cb2bc92c0cf4d45389eff5b078d1936860a1be3aa8c94fa25fb46ed8", - "sha256:1f8c0ae0a0de4e19fddaaff036f508db175f6f03db318c80bbc239a1def62d02", - "sha256:2245441445099411b528379dee83e56eadf449db924648e5feb9b747473f42e3", - "sha256:22709d701e7037e64dae2a04855021b62efd64a66c3ceed99dfd684bfef09e38", - "sha256:24c89346734a4e4d60ecf9b27cac4c1fee3431a413f7aa00be7c4d7bbacc2c4d", - "sha256:25716aa70a0d153cd844fe861d4f3315a6ccafce22b39d8aadbf7fcadff2b633", - "sha256:2dacb3dae6b8cc579637a7b72f008bff50a94cde5e36e432352f4ca57b9e54c4", - "sha256:34316bf693b1d2d29c087ee7e4bb10cdfa39da5f9c50fa15b07489b4ab93a1b5", - "sha256:36b2d700a27e168fa96272b42d28c7ac3ff72030c67b32f37c05616ebd22a202", - "sha256:37978254d9d00cda01acc1997513f786b6b971e57b778fbe7c20e30ae81a97f3", - "sha256:38289f1690a7e27aacd049e420769b996826f3728756859420eeee21cc857118", - "sha256:385ccf6d011b97768a640e9d4de25412204fbe8d6b9ae39ff115d4ff03f6fe5d", - "sha256:3c7ea86b9ca83e30fa4d4cd0eaf01db3ebcc7b2726a25990966627e39577d729", - "sha256:49810f907dfe6de8da5da7d2b238d343e6add62f01a15d03e2195afc180059ed", - "sha256:519c0b3a6fbb68afaa0febf0d28f6c4b0a1074aefc484802ecb9709faf181607", - "sha256:51f02ca184518702975b56affde6c573ebad4e411599005ce4468b1014b4786c", - "sha256:552a39987ac6655dad4bf6f17dd2b55c7b0c6e949d933b8846d2e312ee80005a", - "sha256:596f5ae2eeddb79b595583c2e0285312b2783b0ec759930c272dbf02f851ff75", - "sha256:6014038f52b4b2ac1fa41a58d439a8a00f015b5c0735a0cd4b09afe344c94899", - "sha256:61ebbcd208d78658b09e19c78920f1ad38936a0aa0f9c459c46c197d11c580a0", - "sha256:6213713ac743b190ecbf3f316d6e41d099e774812d470422b3a0f137ea635832", - "sha256:637e27ea1ebe4a561db75a880ac659ff439dec7f55588212e71700bb1ddd5af9", - "sha256:6aa427c55a0abec450bca10b64446331b5ca8f79b648531138f357569705bc4a", - "sha256:6ca45359d7a21644793de0e29de497ef7f1ae7268e346c4faf87b421fea364e6", - "sha256:6db1b52c6f2c04fafc8da17ea506608e6be7086715dab498570c3e55e4f8fbd1", - "sha256:752e7ddfb743344d447367baa85bccd3629c2c3940f70506eb5f01abce98ee68", - "sha256:760c54ad1b8a9b81951030a7e8e7c3ec0964c1cb9fee585a03ff53d9e531bb8e", - "sha256:768632fd8172ae03852e3245f11c8a425d95f65ff444ce46b3e673ae5b057b74", - "sha256:7a0b9f6a1a15d494b35f25ed07abda03209fa76c33564c09c9e81d34f4b919d7", - "sha256:7e070d3aef50ac3856f2ef5ec7214798453da878bb5e5a16c16a61edf1817cc3", - "sha256:7e12949e5071c20ec49ef00c75121ed2b076972132fc1913ddf5f76cae8d10b4", - "sha256:7e26eac9e52e8ce86f915fd33380f1b6896a2b51994e40bb094841e5003429b4", - "sha256:85ffd6b1cb0dfb037ede50ff3bef80d9bf7fa60515d192403af6745524524f3b", - "sha256:8618d9213a863c468a865e9d2ec50221015f7abf52221bc927152ef26c484b4c", - "sha256:8acef4d8a4353f6678fd1035422a937c2170de58a2b29f7da045d5249e934101", - "sha256:8d2f355a951f60f0843f2368b39970e4667517e54e86b1508e76f92b44811a8a", - "sha256:90b6840b6448203228a9d8464a7a0d99aa8fa9f027ef95fe230579abaf8a6ee1", - "sha256:9187500d83fd0cef4669385cbb0961e227a41c0c9bc39219044e35810793edf7", - "sha256:93c20777a72cae8620203ac11c4010365706062aa13aaedd1a21bb07adbb9d5d", - "sha256:93cce7d422a0093cfb3606beae38a8e47a25232eea0f292c878af580a9dc7605", - "sha256:94c623c331a48a5ccc7d25271399aff29729fa202c737ae3b4b28b89d2b0976d", - "sha256:97f32dc03a8054a4c4a5ab5d761ed4861e828b2c200febd4e46857069a483916", - "sha256:9a2bf98ac92f58777c0fafc772bf0493e67fcf677302e0c0a630ee517a43b949", - "sha256:a602bdc8607c99eb5b391592d58c92618dcd1537fdd87df1813f03fed49957a6", - "sha256:a9d24b03daf7415f78abc2d25a208f234e2c585e5e6f92f0204d2ab7b9ab48e3", - "sha256:abfcb0ef78df0ee9df4ea81f03beea41849340ce33a4c4bd4dbb99e23ec781b6", - "sha256:b013f759cd69cb0a62de954d6d2096d648bc210034b79b1881406b07ed0a83f9", - "sha256:b02e3e72665cd02afafb933453b0c9f6c59ff6e3708bd28d0d8580450e7e88af", - "sha256:b52cc45e71657bc4743a5606d9023459de929b2a198d545868e11898ba1c3f59", - "sha256:ba37f11e1d020969e8a779c06b4af866ffb6b854d7229db63c5fdddfceaa917f", - "sha256:bb804c7d0bfbd7e3f33924ff49757de9106c44e27979e2492819c16972ec0da2", - "sha256:bf594cc7cc9d528338d66674c10a5b25e3cde7dd75c3e96784df8f371d77a298", - "sha256:c38baee6bdb7fe1b110b6b3aaa555e6e872d322206b7245aa39572d3fc991ee4", - "sha256:c73d2166e4b210b73d1429c4f1ca97cea9cc090e5302df2a7a0a96ce55373f1c", - "sha256:c9099bf89078675c372339011ccfc9ec310310bf6c292b413c013eb90ffdcafc", - "sha256:cf0db26a1f76aa6b3aa314a74b8facd586b7a5457d05b64f8082a62c9c49582a", - "sha256:d19a34f8a3429bd536996ad53597b805c10352a8561d8382e05830df389d2b43", - "sha256:da80047524eac2acf7c04c18ac7a7da05a9136241f642dd2ed94269ef0d0a45a", - "sha256:de2923886b5d3214be951bc2ce3f6b8ac0d6dfd4a0d0e2a4d2e5523d8046fdfb", - "sha256:defa0652696ff0ba48c8aff5a1fac1eef1ca6ac9c660b047fc8e7623c4eb5093", - "sha256:e54a1eb9fd38f2779e973d2f8958fd575b532fe26013405d1afb9ee2374e7ab8", - "sha256:e5c31d70a478b0ca22a9d2d76d520ae996214019d39ed7dd93af872c7f301e52", - "sha256:ebaeb93f90c0903233b11ce913a7cb8f6ee069158406e056f884854c737d2442", - "sha256:ecfe51abf7f045e0b9cdde71ca9e153d11238679ef7b5da6c82093874adf3338", - "sha256:f99112aed4fb7cee00c7f77e8b964a9b10f69488cdff626ffd797d02e2e4484f", - "sha256:fd914db437ec25bfa410f8aa0aa2f3ba87cdfc04d9919d608d02330947afaeab" + "sha256:00d2e907d3c5e4f85197c8d2263a9cc2d34bf234a9c6236ae42a3fb0bc09b759", + "sha256:0186edcda692c38381db8ac257c2d023fd2e08818d45dc5bee4ed84212045f51", + "sha256:06c509bd7dcb7966bdb03974457d548e54d8327bad5b0c917e87248edc43e2eb", + "sha256:0a3f3f45c5902eb4d90266002ccb035531ae9b9278f6d5e8028247c34d192099", + "sha256:0c1821146b429e6fdbd13ea10f26765e48d5284bc79749468cfbfe3ceb929f0d", + "sha256:0d93167b7d7731fa9ff9fdc1bae84ec9c7133b01a35f8cc04e926d48da6ce1f7", + "sha256:0fd8c3635fa03ef79d07c7b3ed693b3f3930ccb52c0c51761c3296a7525b135c", + "sha256:119091c675e6ad19da8770f89aa1d52f4ad2a2018d631956f3e90c45882df880", + "sha256:121981ba84309dabefd5e1debd49be6d51624e54b4d44bfc184cd8d555ff1df1", + "sha256:1244e9b9b4b81c9c34e8a84273ffaeebdc78abc98a5b02dcdd49845eb3c63bd7", + "sha256:12e1404dfb4e928d3273a10e3468877fe84bdcd3c50b655a2c9613cfc5d9fe63", + "sha256:13d74951c14708f00700bb29475129ecbc40e01b4029c62ee7bfe9d1f59f31ce", + "sha256:162a5939a6fdf48658d3565eeff35acdd207e07367bf5caaff3d9ea7cb77d7a9", + "sha256:1703490c5b850fa9cef1af00c58966756042e6ca22f4fb5bb857345cd535834f", + "sha256:18e6203cfd81df42a987175aaeed7ba46bcb42130cd81763e2d5edcff0006d5d", + "sha256:192c2784833aea6fc7b004730bf1b91b8b8c6b998b30271aaf3bd8adfef20a96", + "sha256:1948d3ceac5b2d55bc93159c1e0679a256a87a54c735be5cef4543a9e692dbb9", + "sha256:206a327e628bc529d64b21ff79a5e2564f5aec7dc7abcd4b2e8a4b271ec10550", + "sha256:2e5db20412f0db8798ff72473d16da5f13ec808e975b49188badb2462f529fa9", + "sha256:2f94b0befc811fe74a972b1739fffbf74c0dc1a91102aca8e324aa4f2c6991bd", + "sha256:303676797c4c7978726e74eb8255d68f7125a3a29da71ff453448f2117290e9a", + "sha256:34ae4f35db30caa4caf85c55069fcb7a05966a3a5ba6e9e1dab5477d84fbb08a", + "sha256:3c6df8be7d1dd35a0d9a200fbc29f888c4452c8882d284f87608046152e049e6", + "sha256:402fa998c5988d11ed34585eb65740dcebd0fd11844d12eb0a6b4be178eb9c64", + "sha256:40a28759d345c0bb1f5b0ac74ac04f5d48136019522c95c0ec4b07786f67ce20", + "sha256:414ae507ba88264444baf771fec43ce0adcd4c5dbb304d3e0716f3f4d4499d2e", + "sha256:42da079e31ae9818ffa7a35cdd16ab7104e3f7eca9c0958040aede827b2e55c6", + "sha256:473a7d21932ce7c314953b33c32e63df690181860edcdf14bba1278cdf71b07f", + "sha256:49fcb45931a693b0e901972c5e077ea2cf30ec39da699645c43cb8b1542c6e14", + "sha256:4c5913cb9769038bd03e42318955c2f15a688384a6a0b807bcfc8271603d9277", + "sha256:4cfeb71095c8d8380a5df5a38ff94d27a3f483717e509130a822b4d6400b7991", + "sha256:4dc74f0171eede67d79a79c06eca0fe5b7b280dbb8c27ad1fae4ced2ad66268f", + "sha256:5b1cffff2d9f832288fe516021cb81c95c57c0067b13a82f1d2daabdbc2f4270", + "sha256:601c99ac775b6c89699a48976f3dbb000b47d3ca59362c8abc9582e6d0780d91", + "sha256:667a06bb8d72b6da3d9cf38dac4ba969688868ed2279a692e993d2c0e1c30aba", + "sha256:673549a0136c7893f567ed71ab5225ed3701c79b17c0a7faee846c645fc24010", + "sha256:67bd3bdd27db7a6460384869dd4b9c54267d805b67d70b20495bb5767f8e051c", + "sha256:727edff0a4eaff3b6d26cbb50216feac9055aba7e6290eec23c061c2fe2fab55", + "sha256:782627a1cb8fbb1c78d8e841f5b71c2c683086c038f975bebdac7cce7678a96f", + "sha256:7d462ba84655abeddae4dfc517fe1afefb5430b3b5acb0a954de12a47aea7183", + "sha256:8ab39aa445d00902c43a1e951871bedc7f18d095a21eccba153d594faac34aea", + "sha256:8e2075ed4ea2e231e2e98b16cfa5dae87e9a6045a71104525e1efc29aa8faa8e", + "sha256:9daeccb2764bf4cc280c40c6411ae176bb0876948e536590a052b3d647254c95", + "sha256:9e4006942334fa954ebd32fa0728718ec870f95f4ba7cda9edc46dd49c294f22", + "sha256:9f1c8fffd4def0b76c0947b8cb261b266e31041785dc2dc2db7569407a2f54fe", + "sha256:a00cd58a30a1041c193777cb1bc090200b05ff4b073d5935738afd1023e63069", + "sha256:a0220a7a16fd4bfc700661f920510defd31ef7830ce992d5cc51777aa8ccd724", + "sha256:a048f91823862270905cb22ef88038b08aac852ce48e0ecc4b4bf1b895ec37d9", + "sha256:a3c47c71fde0c5d584402e67546c81af9951540f1f622d821e9c20761556473a", + "sha256:a6d9ea727fd1233ee746bf44dd37e7d4320b3ed8ff09e73d7638c969b28d280f", + "sha256:ab0709daedc1099bbd4371ae17eeedd4efc1cf70fcdcfe5de1374a0944b61f80", + "sha256:ab1cb36b411f16da6e057ef8e6657dd0af36f59a667f07e0b4b617e44e53d7b2", + "sha256:ae1c5b435d44aa91d48cc710f20c3485e0584a3ad3565d5ae031d61a35f674f4", + "sha256:b279b9bb401af41130fd2a09427105100bc8c624ed45da1c81c1c0d0aa639734", + "sha256:b72a4ec79a15f6066d14ae1c472b743af4b4ecee14420e8d6e4a336b49b8f21c", + "sha256:c2cd93725911c0159d597b90c96151070ef7e0e67604637e2f2abe06c34bf079", + "sha256:c7c5f914b0eb5242c09f91058b80295525897e873b522575ab235b48db125597", + "sha256:d07d849c9e2eca80adb85d3567302a47195a603ad7b1f0a07508e253c041f954", + "sha256:d2672d68cf6c8452b6758fc3cd2d8feac966d511eed79a68182a5297b473af9c", + "sha256:d35bbcbf70d14f724e7489746cf68efe122796578addd98f91428e144d0ad266", + "sha256:d40b4447784dbe0896a6d10a178f6724598161f942c56f5a60dc0ef7fe63f7a1", + "sha256:d561dcb0fb0ab858291837d51330696a45fd3ba6912a332a4ee130e5484b9e47", + "sha256:d7f5ccfff648093152cadf6d886c7bd922047532f72024c953a79c7553aac2fe", + "sha256:dce6b2ad817e3eb107f8704782b091b0631dd3adf47f14bdc086165d05b528b0", + "sha256:e1fdda3ec7e9785065b67941693995cab95b54023a21db9bf39e54cc7b2c3526", + "sha256:e2a262ec85c595fc8e1f3162cafc654d2219125c00ea3a190c173cea70d2cc7a", + "sha256:e2fc1e3928c1189c0382c547c17717c6d9f425fffe619ef94270fe4c6c8be0a6", + "sha256:ea27acd97a752cfefa9907da935e583efecb302e6e9866f37565968c8407ad58", + "sha256:ee769a438827e443ed428e66d0aa7131c653ecd86ddc5d4644a81ed1d93af0e7", + "sha256:f32e0d1c7e7b0b9c3cac76f3d278e7ee6b99c95672d2c1c6ea625033431837c0", + "sha256:f355caec5bbce20421dc26e53787b10e32fd0df68db2b795435217210c08d69c", + "sha256:f87e9108bb532f8a1fc6bf7e69b930a35c7b0267b8fef0a3ede0bcb4c5aaa531", + "sha256:f8a2fd2f62a77536e4e3193303bec380df40d99e253b1c8f9b6eafa07eaeff67", + "sha256:fbdf4fc6adf38fab1091c579ece3fe9f493bd0f1cfc3d2c76d2e52461ca4f8a9" ], - "version": "==2022.1.18" + "version": "==2022.7.9" + }, + "requests": { + "hashes": [ + "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", + "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" + ], + "index": "pypi", + "version": "==2.28.1" }, "toml": { "hashes": [ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.10.2" }, "tomli": { @@ -1088,41 +1111,47 @@ }, "typed-ast": { "hashes": [ - "sha256:0eb77764ea470f14fcbb89d51bc6bbf5e7623446ac4ed06cbd9ca9495b62e36e", - "sha256:1098df9a0592dd4c8c0ccfc2e98931278a6c6c53cb3a3e2cf7e9ee3b06153344", - "sha256:183b183b7771a508395d2cbffd6db67d6ad52958a5fdc99f450d954003900266", - "sha256:18fe320f354d6f9ad3147859b6e16649a0781425268c4dde596093177660e71a", - "sha256:26a432dc219c6b6f38be20a958cbe1abffcc5492821d7e27f08606ef99e0dffd", - "sha256:294a6903a4d087db805a7656989f613371915fc45c8cc0ddc5c5a0a8ad9bea4d", - "sha256:31d8c6b2df19a777bc8826770b872a45a1f30cfefcfd729491baa5237faae837", - "sha256:33b4a19ddc9fc551ebabca9765d54d04600c4a50eda13893dadf67ed81d9a098", - "sha256:42c47c3b43fe3a39ddf8de1d40dbbfca60ac8530a36c9b198ea5b9efac75c09e", - "sha256:525a2d4088e70a9f75b08b3f87a51acc9cde640e19cc523c7e41aa355564ae27", - "sha256:58ae097a325e9bb7a684572d20eb3e1809802c5c9ec7108e85da1eb6c1a3331b", - "sha256:676d051b1da67a852c0447621fdd11c4e104827417bf216092ec3e286f7da596", - "sha256:74cac86cc586db8dfda0ce65d8bcd2bf17b58668dfcc3652762f3ef0e6677e76", - "sha256:8c08d6625bb258179b6e512f55ad20f9dfef019bbfbe3095247401e053a3ea30", - "sha256:90904d889ab8e81a956f2c0935a523cc4e077c7847a836abee832f868d5c26a4", - "sha256:963a0ccc9a4188524e6e6d39b12c9ca24cc2d45a71cfdd04a26d883c922b4b78", - "sha256:bbebc31bf11762b63bf61aaae232becb41c5bf6b3461b80a4df7e791fabb3aca", - "sha256:bc2542e83ac8399752bc16e0b35e038bdb659ba237f4222616b4e83fb9654985", - "sha256:c29dd9a3a9d259c9fa19d19738d021632d673f6ed9b35a739f48e5f807f264fb", - "sha256:c7407cfcad702f0b6c0e0f3e7ab876cd1d2c13b14ce770e412c0c4b9728a0f88", - "sha256:da0a98d458010bf4fe535f2d1e367a2e2060e105978873c04c04212fb20543f7", - "sha256:df05aa5b241e2e8045f5f4367a9f6187b09c4cdf8578bb219861c4e27c443db5", - "sha256:f290617f74a610849bd8f5514e34ae3d09eafd521dceaa6cf68b3f4414266d4e", - "sha256:f30ddd110634c2d7534b2d4e0e22967e88366b0d356b24de87419cc4410c41b7" + "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2", + "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1", + "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6", + "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62", + "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac", + "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d", + "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc", + "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2", + "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97", + "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35", + "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6", + "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1", + "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4", + "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c", + "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e", + "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec", + "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f", + "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72", + "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47", + "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72", + "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe", + "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6", + "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3", + "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66" ], - "markers": "python_version >= '3.6'", - "version": "==1.5.2" + "version": "==1.5.4" }, "typing-extensions": { "hashes": [ - "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42", - "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2" + "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02", + "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6" ], "index": "pypi", - "version": "==4.1.1" + "version": "==4.3.0" + }, + "urllib3": { + "hashes": [ + "sha256:8298d6d56d39be0e3bc13c1c97d133f9b45d797169a0e11cdd0e0489d786f7ec", + "sha256:879ba4d1e89654d9769ce13121e0f94310ea32e8d2f8cf587b77c08bbcdb30d6" + ], + "version": "==1.26.10" } } } diff --git a/Procfile b/Procfile deleted file mode 100644 index c0ca4887..00000000 --- a/Procfile +++ /dev/null @@ -1 +0,0 @@ -web: hypercorn -k trio --bind 0.0.0.0:5000 'lnbits.app:create_app()' diff --git a/README.md b/README.md index 020f617c..f0f9df62 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ LNbits (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: @@ -33,7 +33,7 @@ LNbits is inspired by all the great work of [opennode.com](https://www.opennode. ## 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 @@ -67,7 +67,7 @@ Wallets can be easily generated and given out to people at events (one click mul ## 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/ diff --git a/app.json b/app.json deleted file mode 100644 index 5f5d5bb1..00000000 --- a/app.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "scripts": { - "dokku": { - "predeploy": "quart migrate && quart assets" - } - } -} diff --git a/build.py b/build.py new file mode 100644 index 00000000..5cc8b7de --- /dev/null +++ b/build.py @@ -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"]) diff --git a/docs/devs/development.md b/docs/devs/development.md index 85346d16..f53b94bc 100644 --- a/docs/devs/development.md +++ b/docs/devs/development.md @@ -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: ```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: diff --git a/docs/devs/extensions.md b/docs/devs/extensions.md index 70e599e9..8c9a30a4 100644 --- a/docs/devs/extensions.md +++ b/docs/devs/extensions.md @@ -15,6 +15,7 @@ cp lnbits/extensions/example lnbits/extensions/mysuperplugin -r # Let's not use 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'. ``` +- 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: * 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. diff --git a/docs/devs/installation.md b/docs/devs/installation.md index cbf234cc..f4d6b145 100644 --- a/docs/devs/installation.md +++ b/docs/devs/installation.md @@ -7,46 +7,10 @@ nav_order: 1 # 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 -git clone https://github.com/lnbits/lnbits-legend.git -cd lnbits-legend/ +## Notes: -sudo apt-get install pipenv -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 Caddy for a reverse-proxy if you want to serve your install through a domain, alternatively you can use [ngrok](https://ngrok.com/). +* We recommend using Caddy for a reverse-proxy if you want to serve your install through a domain, alternatively you can use [ngrok](https://ngrok.com/). * Screen works well if you want LNbits to continue running when you close your terminal session. diff --git a/docs/guide/installation.md b/docs/guide/installation.md index 2806a4f5..f38f606d 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -4,8 +4,125 @@ title: Basic installation nav_order: 2 --- + + # 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 # on debian/ubuntu 'sudo apt-get -y install postgresql' @@ -22,53 +139,54 @@ createdb lnbits exit ``` -Download this repo and install the dependencies: +You need to edit the `.env` file. ```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= # postgres://:@/ - alter line bellow with your user, password and db name LNBITS_DATABASE_URL="postgres://postgres:postgres@localhost/lnbits" # save and exit -./venv/bin/uvicorn lnbits.__main__:app --port 5000 ``` +# Using LNbits + 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. 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 -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://:@/ - 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. +Take a look at [Polar](https://lightningpolar.com/) for an excellent way of spinning up a Lightning Network dev environment. # 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://:@/ - 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: @@ -78,17 +196,23 @@ Systemd is great for taking care of your LNbits instance. It will start it on bo [Unit] Description=LNbits -#Wants=lnd.service # 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) +# you can uncomment these lines if you know what you're doing +# it will make sure that lnbits starts after lnd (replace with your own backend service) +#Wants=lnd.service +#After=lnd.service [Service] -WorkingDirectory=/home/bitcoin/lnbits # replace with the absolute path of your lnbits installation -ExecStart=/home/bitcoin/lnbits/venv/bin/uvicorn lnbits.__main__:app --port 5000 # same here -User=bitcoin # replace with the user that you're running lnbits on +# replace with the absolute path of your lnbits installation +WorkingDirectory=/home/bitcoin/lnbits +# 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 TimeoutSec=120 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] WantedBy=multi-user.target @@ -101,11 +225,40 @@ sudo systemctl enable 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. -### Docker installation +## Docker installation 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. - -# 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. diff --git a/docs/guide/wallets.md b/docs/guide/wallets.md index 7a3b6a27..dfea66aa 100644 --- a/docs/guide/wallets.md +++ b/docs/guide/wallets.md @@ -17,7 +17,6 @@ A backend wallet can be configured using the following LNbits environment variab ### CLightning 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** - `CLIGHTNING_RPC`: /file/path/lightning-rpc diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..0ca2db01 --- /dev/null +++ b/flake.lock @@ -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 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..af25ba5c --- /dev/null +++ b/flake.nix @@ -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... + } + ); + }; +} diff --git a/lnbits/__main__.py b/lnbits/__main__.py index 8461eb42..90cb1997 100644 --- a/lnbits/__main__.py +++ b/lnbits/__main__.py @@ -1,36 +1,38 @@ import asyncio import uvloop +from loguru import logger from starlette.requests import Request -from .commands import bundle_vendored, migrate_databases, transpile_scss +from .commands import migrate_databases from .settings import ( DEBUG, + HOST, LNBITS_COMMIT, LNBITS_DATA_FOLDER, + LNBITS_DATABASE_URL, LNBITS_SITE_TITLE, PORT, - SERVICE_FEE, WALLET, ) uvloop.install() asyncio.create_task(migrate_databases()) -transpile_scss() -bundle_vendored() from .app import create_app app = create_app() -print( - f"""Starting LNbits with - - git version: {LNBITS_COMMIT} - - site title: {LNBITS_SITE_TITLE} - - debug: {DEBUG} - - data folder: {LNBITS_DATA_FOLDER} - - funding source: {WALLET.__class__.__name__} - - service fee: {SERVICE_FEE} -""" +logger.info("Starting LNbits") +logger.info(f"Host: {HOST}") +logger.info(f"Port: {PORT}") +logger.info(f"Debug: {DEBUG}") +logger.info(f"Site title: {LNBITS_SITE_TITLE}") +logger.info(f"Funding source: {WALLET.__class__.__name__}") +logger.info( + 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}") diff --git a/lnbits/app.py b/lnbits/app.py index ca770167..19482b06 100644 --- a/lnbits/app.py +++ b/lnbits/app.py @@ -1,14 +1,18 @@ import asyncio import importlib +import logging import sys import traceback import warnings +from http import HTTPStatus from fastapi import FastAPI, Request from fastapi.exceptions import RequestValidationError from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.gzip import GZipMiddleware +from fastapi.responses import JSONResponse from fastapi.staticfiles import StaticFiles +from loguru import logger import lnbits.settings from lnbits.core.tasks import register_task_listeners @@ -39,10 +43,21 @@ def create_app(config_object="lnbits.settings") -> FastAPI: """Create application factory. :param config_object: The configuration object to use. """ - app = FastAPI() - app.mount("/static", StaticFiles(directory="lnbits/static"), name="static") + configure_logger() + + 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( - "/core/static", StaticFiles(directory="lnbits/core/static"), name="core_static" + "/core/static", + StaticFiles(packages=[("lnbits.core", "static")]), + name="core_static", ) origins = ["*"] @@ -58,15 +73,19 @@ def create_app(config_object="lnbits.settings") -> FastAPI: async def validation_exception_handler( request: Request, exc: RequestValidationError ): - return template_renderer().TemplateResponse( - "error.html", - {"request": request, "err": f"`{exc.errors()}` is not a valid UUID."}, - ) + # Only the browser sends "text/html" request + # not fail proof, but everything else get's a JSON response - # return HTMLResponse( - # status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - # content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}), - # ) + if "text/html" in request.headers["accept"]: + return template_renderer().TemplateResponse( + "error.html", + {"request": request, "err": f"{exc.errors()} is not a valid UUID."}, + ) + + return JSONResponse( + status_code=HTTPStatus.NO_CONTENT, + content={"detail": exc.errors()}, + ) app.add_middleware(GZipMiddleware, minimum_size=1000) # app.add_middleware(ASGIProxyFix) @@ -88,14 +107,14 @@ def check_funding_source(app: FastAPI) -> None: error_message, balance = await WALLET.status() if not error_message: break - warnings.warn( - f" × The backend for {WALLET.__class__.__name__} isn't working properly: '{error_message}'", + logger.error( + f"The backend for {WALLET.__class__.__name__} isn't working properly: '{error_message}'", RuntimeWarning, ) - print("Retrying connection to backend in 5 seconds...") + logger.info("Retrying connection to backend in 5 seconds...") await asyncio.sleep(5) - print( - f" ✔️ {WALLET.__class__.__name__} seems to be connected and with a balance of {balance} msat." + logger.info( + 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: app.mount(s["path"], s["app"], s["name"]) + logger.trace(f"adding route for extension {ext_module}") app.include_router(ext_route) except Exception as e: - print(str(e)) + logger.error(str(e)) raise ImportError( 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): @app.exception_handler(Exception) 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() traceback.print_exception(etype, err, tb) exc = traceback.format_exc() - return template_renderer().TemplateResponse( - "error.html", {"request": request, "err": err} + + if "text/html" in request.headers["accept"]: + return template_renderer().TemplateResponse( + "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 = "{time:YYYY-MM-DD HH:mm:ss.SS} | {level} | {message}\n" + if lnbits.settings.DEBUG: + self.fmt: str = "{time:YYYY-MM-DD HH:mm:ss.SS} | {level: <4} | {name}:{function}:{line} | {message}\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()) diff --git a/lnbits/bolt11.py b/lnbits/bolt11.py index 74f73963..cc841585 100644 --- a/lnbits/bolt11.py +++ b/lnbits/bolt11.py @@ -1,15 +1,16 @@ -import bitstring # type: ignore -import re import hashlib -from typing import List, NamedTuple, Optional -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 re import time +from binascii import unhexlify from decimal import Decimal +from typing import List, NamedTuple, Optional + +import bitstring # type: ignore import embit 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): @@ -165,7 +166,7 @@ def lnencode(addr, privkey): if addr.amount: amount = Decimal(str(addr.amount)) # We can only send down to millisatoshi. - if amount * 10 ** 12 % 10: + if amount * 10**12 % 10: raise ValueError( "Cannot encode {}: too many decimal places".format(addr.amount) ) @@ -270,7 +271,7 @@ class LnAddr(object): def shorten_amount(amount): """Given an amount in bitcoin, shorten it""" # Convert to pico initially - amount = int(amount * 10 ** 12) + amount = int(amount * 10**12) units = ["p", "n", "u", "m", ""] for unit in units: if amount % 1000 == 0: @@ -289,7 +290,7 @@ def _unshorten_amount(amount: str) -> int: # * `u` (micro): multiply by 0.000001 # * `n` (nano): multiply by 0.000000001 # * `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] # BOLT #11: @@ -348,9 +349,9 @@ def _trim_to_bytes(barr): def _readable_scid(short_channel_id: int) -> str: return "{blockheight}x{transactionindex}x{outputindex}".format( - blockheight=((short_channel_id >> 40) & 0xffffff), - transactionindex=((short_channel_id >> 16) & 0xffffff), - outputindex=(short_channel_id & 0xffff), + blockheight=((short_channel_id >> 40) & 0xFFFFFF), + transactionindex=((short_channel_id >> 16) & 0xFFFFFF), + outputindex=(short_channel_id & 0xFFFF), ) diff --git a/lnbits/commands.py b/lnbits/commands.py index 95950760..0f7454f2 100644 --- a/lnbits/commands.py +++ b/lnbits/commands.py @@ -1,16 +1,19 @@ import asyncio -import warnings -import click import importlib -import re import os +import re +import warnings -from .db import SQLITE, POSTGRES, COCKROACH -from .core import db as core_db, migrations as core_migrations +import click +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 ( - get_valid_extensions, get_css_vendored, get_js_vendored, + get_valid_extensions, url_for_vendored, ) from .settings import LNBITS_PATH @@ -69,7 +72,7 @@ async def migrate_databases(): if match: version = int(match.group(1)) 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) if db.schema == None: @@ -110,4 +113,4 @@ async def migrate_databases(): async with ext_db.connect() as ext_conn: await run_migration(ext_conn, ext_migrations) - print(" ✔️ All migrations done.") + logger.info("✔️ All migrations done.") diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index a63f52c4..770e2906 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -1,15 +1,15 @@ -import json import datetime -from uuid import uuid4 -from typing import List, Optional, Dict, Any +import json +from typing import Any, Dict, List, Optional from urllib.parse import urlparse +from uuid import uuid4 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 . import db -from .models import User, Wallet, Payment, BalanceCheck +from .models import BalanceCheck, Payment, User, Wallet # accounts # -------- @@ -180,16 +180,28 @@ async def get_wallet_for_key( 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]: + 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( - """ + f""" SELECT * FROM apipayments - WHERE checking_id = ? OR hash = ? + WHERE {clause} LIMIT 1 """, - (checking_id_or_hash, checking_id_or_hash), + tuple(values), ) return Payment.from_row(row) if row else None diff --git a/lnbits/core/models.py b/lnbits/core/models.py index 88963b2b..ab73b702 100644 --- a/lnbits/core/models.py +++ b/lnbits/core/models.py @@ -1,12 +1,15 @@ -import json -import hmac 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 lnurl import encode as lnurl_encode # type: ignore -from typing import List, NamedTuple, Optional, Dict -from sqlite3 import Row +from loguru import logger from pydantic import BaseModel + +from lnbits.helpers import url_for from lnbits.settings import WALLET @@ -142,10 +145,12 @@ class Payment(BaseModel): status = await WALLET.get_invoice_status(self.checking_id) 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() elif not status.pending: - print( + logger.info( f" - marking '{'in' if self.is_in else 'out'}' {self.checking_id} as not pending anymore: {status}" ) await self.set_pending(status.pending) diff --git a/lnbits/core/services.py b/lnbits/core/services.py index 3d54e218..0b565ebb 100644 --- a/lnbits/core/services.py +++ b/lnbits/core/services.py @@ -6,14 +6,22 @@ from typing import Dict, Optional, Tuple from urllib.parse import parse_qs, urlparse import httpx +from fastapi import Depends from lnurl import LnurlErrorResponse from lnurl import decode as decode_lnurl # type: ignore +from loguru import logger from lnbits import bolt11 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.requestvars import g -from lnbits.settings import WALLET +from lnbits.settings import FAKE_WALLET, WALLET from lnbits.wallets.base import PaymentResponse, PaymentStatus from . import db @@ -48,15 +56,19 @@ async def create_invoice( description_hash: Optional[bytes] = None, extra: Optional[Dict] = None, webhook: Optional[str] = None, + internal: Optional[bool] = False, conn: Optional[Connection] = None, ) -> Tuple[str, str]: 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 ) if not ok: - raise InvoiceFailure(error_message or "Unexpected backend error.") + raise InvoiceFailure(error_message or "unexpected backend error.") 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 internal_checking_id = await check_internal(invoice.payment_hash, conn=conn) if internal_checking_id: + logger.debug(f"creating temporary internal payment with id {internal_id}") # create a new payment from this wallet await create_payment( checking_id=internal_id, @@ -129,6 +142,7 @@ async def pay_invoice( **payment_kwargs, ) else: + logger.debug(f"creating temporary payment with id {temp_id}") # create a temporary payment here so we can check if # the balance is enough in the next step await create_payment( @@ -142,6 +156,7 @@ async def pay_invoice( wallet = await get_wallet(wallet_id, conn=conn) assert wallet 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: raise PaymentFailure( 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.") 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 # so the other side only has access to his new money when we are sure # the payer has enough to deduct from @@ -163,11 +179,14 @@ async def pay_invoice( await internal_invoice_queue.put(internal_checking_id) else: + logger.debug(f"backend: sending payment {temp_id}") # actually pay the external invoice payment: PaymentResponse = await WALLET.pay_invoice( payment_request, fee_reserve_msat ) + logger.debug(f"backend: pay_invoice finished {temp_id}") if payment.checking_id: + logger.debug(f"creating final payment {payment.checking_id}") async with db.connect() as conn: await create_payment( checking_id=payment.checking_id, @@ -177,15 +196,18 @@ async def pay_invoice( conn=conn, **payment_kwargs, ) + logger.debug(f"deleting temporary payment {temp_id}") await delete_payment(temp_id, conn=conn) else: + logger.debug(f"backend payment failed, no checking_id {temp_id}") async with db.connect() as conn: + logger.debug(f"deleting temporary payment {temp_id}") await delete_payment(temp_id, conn=conn) raise PaymentFailure( payment.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 @@ -216,7 +238,7 @@ async def redeem_lnurl_withdraw( conn=conn, ) except: - print( + logger.warning( f"failed to create invoice on redeem_lnurl_withdraw from {lnurl}. params: {res}" ) return None @@ -243,12 +265,14 @@ async def redeem_lnurl_withdraw( async def perform_lnurlauth( - callback: str, conn: Optional[Connection] = None + callback: str, + wallet: WalletTypeInfo = Depends(require_admin_key), + conn: Optional[Connection] = None, ) -> Optional[LnurlErrorResponse]: cb = urlparse(callback) 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: """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: return status 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() elif not status.pending: - print( - f" - marking '{'in' if payment.is_in else 'out'}' {payment.checking_id} as not pending anymore: {status}" + logger.info( + f"marking '{'in' if payment.is_in else 'out'}' {payment.checking_id} as not pending anymore: {status}" ) await payment.set_pending(status.pending) return status diff --git a/lnbits/core/static/js/extensions.js b/lnbits/core/static/js/extensions.js index 85ace775..ec8f811c 100644 --- a/lnbits/core/static/js/extensions.js +++ b/lnbits/core/static/js/extensions.js @@ -1,4 +1,36 @@ new 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] }) diff --git a/lnbits/core/static/js/service-worker.js b/lnbits/core/static/js/service-worker.js new file mode 100644 index 00000000..041b9f32 --- /dev/null +++ b/lnbits/core/static/js/service-worker.js @@ -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) + }) + }) + ) + } +}) diff --git a/lnbits/core/static/js/wallet.js b/lnbits/core/static/js/wallet.js index 29a1025d..baa9f605 100644 --- a/lnbits/core/static/js/wallet.js +++ b/lnbits/core/static/js/wallet.js @@ -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) + }) +} diff --git a/lnbits/core/tasks.py b/lnbits/core/tasks.py index 1e3eaeab..5fea769d 100644 --- a/lnbits/core/tasks.py +++ b/lnbits/core/tasks.py @@ -1,7 +1,9 @@ import asyncio -import httpx from typing import List +import httpx +from loguru import logger + from lnbits.tasks import register_invoice_listener from . import db @@ -20,7 +22,7 @@ async def register_task_listeners(): async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue): while True: payment = await invoice_paid_queue.get() - + logger.debug("received invoice paid event") # send information to sse channel await dispatch_invoice_listener(payment) @@ -44,7 +46,7 @@ async def dispatch_invoice_listener(payment: Payment): try: send_channel.put_nowait(payment) except asyncio.QueueFull: - print("removing sse listener", send_channel) + logger.debug("removing sse listener", send_channel) api_invoice_listeners.remove(send_channel) @@ -52,6 +54,7 @@ async def dispatch_webhook(payment: Payment): async with httpx.AsyncClient() as client: data = payment.dict() try: + logger.debug("sending webhook", payment.webhook) r = await client.post(payment.webhook, json=data, timeout=40) await mark_webhook_sent(payment, r.status_code) except (httpx.ConnectError, httpx.RequestError): diff --git a/lnbits/core/templates/core/_api_docs.html b/lnbits/core/templates/core/_api_docs.html index 5df37676..383f2b12 100644 --- a/lnbits/core/templates/core/_api_docs.html +++ b/lnbits/core/templates/core/_api_docs.html @@ -48,7 +48,9 @@ {"X-Api-Key": "{{ wallet.inkey }}"}
Body (application/json)
{"out": false, "amount": <int>, "memo": <string>, "unit": <string>, "webhook": <url:string>}{"out": false, "amount": <int>, "memo": <string>, "unit": + <string>, "webhook": <url:string>, "internal": + <bool>}
Returns 201 CREATED (application/json) @@ -61,8 +63,8 @@ curl -X POST {{ request.base_url }}api/v1/payments -d '{"out": false, "amount": <int>, "memo": <string>, "webhook": - <url:string>, "unit": <string>}' -H "X-Api-Key: {{ wallet.inkey }}" -H - "Content-type: application/json"{{ wallet.inkey }}" -H "Content-type: application/json" diff --git a/lnbits/core/templates/core/extensions.html b/lnbits/core/templates/core/extensions.html index daeb660f..1b527903 100644 --- a/lnbits/core/templates/core/extensions.html +++ b/lnbits/core/templates/core/extensions.html @@ -2,10 +2,23 @@ %} {% block scripts %} {{ window_vars(user) }} {% endblock %} {% block page %} +
+
+ + + +
+
+
diff --git a/lnbits/core/templates/core/wallet.html b/lnbits/core/templates/core/wallet.html index 9453e1bf..acfcf700 100644 --- a/lnbits/core/templates/core/wallet.html +++ b/lnbits/core/templates/core/wallet.html @@ -4,7 +4,6 @@ {% block scripts %} {{ window_vars(user, wallet) }} - {% endblock %} {% block title %} {{ wallet.name }} - {{ SITE_TITLE }} {% endblock %} @@ -709,7 +708,7 @@ -
Warning
+
Warning

Login functionality to be released in v0.2, for now, 2: - raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="Key is invalid") - if invoiceData.out is True and wallet.wallet_type == 0: if not invoiceData.bolt11: raise HTTPException( @@ -246,8 +259,14 @@ async def api_payments_create( return await api_payments_pay_invoice( invoiceData.bolt11, wallet.wallet ) # admin key - # invoice key - return await api_payments_create_invoice(invoiceData, wallet.wallet) + elif not invoiceData.out: + # invoice key + 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): @@ -260,7 +279,7 @@ class CreateLNURLData(BaseModel): @core_app.post("/api/v1/payments/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 @@ -286,6 +305,12 @@ async def api_payments_pay_lnurl( detail=f"{domain} said: '{params.get('reason', '')}'", ) + if not params.get("pr"): + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=f"{domain} did not return a payment request.", + ) + invoice = bolt11.decode(params["pr"]) if invoice.amount_msat != data.amount: 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}.", ) - # if invoice.description_hash != data.description_hash: - # raise HTTPException( - # status_code=HTTPStatus.BAD_REQUEST, - # detail=f"{domain} returned an invalid invoice. Expected description_hash == {data.description_hash}, got {invoice.description_hash}.", - # ) + if invoice.description_hash != data.description_hash: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=f"{domain} returned an invalid invoice. Expected description_hash == {data.description_hash}, got {invoice.description_hash}.", + ) extra = {} @@ -305,7 +330,7 @@ async def api_payments_pay_lnurl( extra["success_action"] = params["successAction"] if data.comment: extra["comment"] = data.comment - + assert data.description is not None, "description is required" payment_hash = await pay_invoice( wallet_id=wallet.wallet.id, payment_request=params["pr"], @@ -322,19 +347,20 @@ async def api_payments_pay_lnurl( 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) - send_queue = asyncio.Queue(0) + send_queue: asyncio.Queue[tuple[str, Payment]] = asyncio.Queue(0) async def payment_received() -> None: while True: payment: Payment = await payment_queue.get() if payment.wallet_id == this_wallet_id: + logger.debug("payment receieved", payment) await send_queue.put(("payment-received", payment)) asyncio.create_task(payment_received()) @@ -359,21 +385,32 @@ async def api_payments_sse( request: Request, wallet: WalletTypeInfo = Depends(get_key_type) ): 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}") 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 try: if X_Api_Key.extra: - print("No key") + logger.warning("No key") except: 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) - 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: raise HTTPException( 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} 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} -@core_app.get( - "/api/v1/lnurlscan/{code}", dependencies=[Depends(WalletInvoiceKeyChecker())] -) -async def api_lnurlscan(code: str): +@core_app.get("/api/v1/lnurlscan/{code}") +async def api_lnurlscan(code: str, wallet: WalletTypeInfo = Depends(get_key_type)): try: url = lnurl.decode(code) domain = urlparse(url).netloc @@ -426,7 +465,7 @@ async def api_lnurlscan(code: str): params.update(kind="auth") 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()) else: async with httpx.AsyncClient() as client: @@ -542,14 +581,19 @@ async def api_payments_decode(data: DecodePayment): return {"message": "Failed to decode"} -@core_app.post("/api/v1/lnurlauth", dependencies=[Depends(WalletAdminKeyChecker())]) -async def api_perform_lnurlauth(callback: str): - err = await perform_lnurlauth(callback) +class Callback(BaseModel): + callback: str = Query(...) + + +@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: raise HTTPException( status_code=HTTPStatus.SERVICE_UNAVAILABLE, detail=err.reason ) - return "" diff --git a/lnbits/core/views/generic.py b/lnbits/core/views/generic.py index 16a2fbac..44666ce1 100644 --- a/lnbits/core/views/generic.py +++ b/lnbits/core/views/generic.py @@ -7,6 +7,7 @@ from fastapi.exceptions import HTTPException from fastapi.params import Depends, Query from fastapi.responses import FileResponse, RedirectResponse from fastapi.routing import APIRouter +from loguru import logger from pydantic.types import UUID4 from starlette.responses import HTMLResponse, JSONResponse @@ -17,10 +18,12 @@ from lnbits.helpers import template_renderer, url_for from lnbits.settings import ( LNBITS_ADMIN_USERS, LNBITS_ALLOWED_USERS, + LNBITS_CUSTOM_LOGO, LNBITS_SITE_TITLE, SERVICE_FEE, ) +from ...helpers import get_valid_extensions from ..crud import ( create_account, create_wallet, @@ -64,11 +67,21 @@ async def extensions( 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: + logger.info(f"Enabling extension: {extension_to_enable} for user {user.id}") await update_user_extension( user_id=user.id, extension=extension_to_enable, active=True ) elif extension_to_disable: + logger.info(f"Disabling extension: {extension_to_disable} for user {user.id}") await update_user_extension( user_id=user.id, extension=extension_to_disable, active=False ) @@ -108,6 +121,7 @@ async def wallet( if not user_id: user = await get_user((await create_account()).id) + logger.info(f"Create user {user.id}") else: user = await get_user(user_id) if not user: @@ -125,12 +139,16 @@ async def wallet( wallet = user.wallets[0] else: 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( f"/wallet?usr={user.id}&wal={wallet.id}", 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) if not wallet: return template_renderer().TemplateResponse( @@ -144,6 +162,7 @@ async def wallet( "user": user.dict(), "wallet": wallet.dict(), "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(...)): user = await get_user(usr) user_wallet_ids = [u.id for u in user.wallets] - print("USR", user_wallet_ids) if wal not in user_wallet_ids: raise HTTPException(HTTPStatus.FORBIDDEN, "Not your wallet.") else: await delete_wallet(user_id=user.id, wallet_id=wal) user_wallet_ids.remove(wal) + logger.debug("Deleted wallet {wal} of user {user.id}") if user_wallet_ids: return RedirectResponse( @@ -226,7 +245,9 @@ async def lnurl_balance_notify(request: Request, service: str): 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 with db.connect() as 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") async def manifest(usr: str): user = await get_user(usr) @@ -256,21 +282,23 @@ async def manifest(usr: str): raise HTTPException(status_code=HTTPStatus.NOT_FOUND) return { - "short_name": "LNbits", - "name": "LNbits Wallet", + "short_name": LNBITS_SITE_TITLE, + "name": LNBITS_SITE_TITLE + " Wallet", "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", "sizes": "900x900", } ], - "start_url": "/wallet?usr=" + usr, - "background_color": "#3367D6", - "description": "Weather forecast information", + "start_url": "/wallet?usr=" + usr + "&wal=" + user.wallets[0].id, + "background_color": "#1F2234", + "description": "Bitcoin Lightning Wallet", "display": "standalone", "scope": "/", - "theme_color": "#3367D6", + "theme_color": "#1F2234", "shortcuts": [ { "name": wallet.name, diff --git a/lnbits/core/views/public_api.py b/lnbits/core/views/public_api.py index 5f8be4e2..2d2cdd66 100644 --- a/lnbits/core/views/public_api.py +++ b/lnbits/core/views/public_api.py @@ -4,6 +4,7 @@ from http import HTTPStatus from urllib.parse import urlparse from fastapi import HTTPException +from loguru import logger from starlette.requests import Request from starlette.responses import HTMLResponse @@ -45,7 +46,7 @@ async def api_public_payment_longpolling(payment_hash): 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) response = None diff --git a/lnbits/db.py b/lnbits/db.py index 7bbfa5c5..66981784 100644 --- a/lnbits/db.py +++ b/lnbits/db.py @@ -5,6 +5,7 @@ import time from contextlib import asynccontextmanager from typing import Optional +from loguru import logger from sqlalchemy import create_engine from sqlalchemy_aio.base import AsyncConnection 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" - please 'mkdir {LNBITS_DATA_FOLDER}' and try again" ) - + logger.trace(f"database {self.type} added for {self.name}") self.schema = self.name if self.name.startswith("ext_"): self.schema = self.name[4:] diff --git a/lnbits/decorators.py b/lnbits/decorators.py index 76cb8a54..e65b9041 100644 --- a/lnbits/decorators.py +++ b/lnbits/decorators.py @@ -13,7 +13,11 @@ from starlette.requests import Request from lnbits.core.crud import get_user, get_wallet_for_key from lnbits.core.models import User, Wallet from lnbits.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): @@ -122,7 +126,7 @@ async def get_key_type( # 0: admin # 1: invoice # 2: invalid - pathname = r['path'].split('/')[1] + pathname = r["path"].split("/")[1] if not api_key_header and not api_key_query: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST) @@ -133,8 +137,12 @@ async def get_key_type( checker = WalletAdminKeyChecker(api_key=token) await checker.__call__(r) 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): - raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized.") + if (LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS) and ( + LNBITS_ADMIN_EXTENSIONS and pathname in LNBITS_ADMIN_EXTENSIONS + ): + raise HTTPException( + status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized." + ) return wallet except HTTPException as e: if e.status_code == HTTPStatus.BAD_REQUEST: @@ -148,8 +156,12 @@ async def get_key_type( checker = WalletInvoiceKeyChecker(api_key=token) await checker.__call__(r) 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): - raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized.") + if (LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS) and ( + LNBITS_ADMIN_EXTENSIONS and pathname in LNBITS_ADMIN_EXTENSIONS + ): + raise HTTPException( + status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized." + ) return wallet except HTTPException as e: if e.status_code == HTTPStatus.BAD_REQUEST: diff --git a/lnbits/extensions/bleskomat/__init__.py b/lnbits/extensions/bleskomat/__init__.py index cc89760e..bef362dc 100644 --- a/lnbits/extensions/bleskomat/__init__.py +++ b/lnbits/extensions/bleskomat/__init__.py @@ -9,7 +9,7 @@ db = Database("ext_bleskomat") bleskomat_static_files = [ { "path": "/bleskomat/static", - "app": StaticFiles(directory="lnbits/extensions/bleskomat/static"), + "app": StaticFiles(packages=[("lnbits", "extensions/bleskomat/static")]), "name": "bleskomat_static", } ] diff --git a/lnbits/extensions/bleskomat/exchange_rates.py b/lnbits/extensions/bleskomat/exchange_rates.py index dcdaa220..e5eb843f 100644 --- a/lnbits/extensions/bleskomat/exchange_rates.py +++ b/lnbits/extensions/bleskomat/exchange_rates.py @@ -1,7 +1,8 @@ -import httpx import json import os +import httpx + fiat_currencies = json.load( open( os.path.join( diff --git a/lnbits/extensions/bleskomat/lnurl_api.py b/lnbits/extensions/bleskomat/lnurl_api.py index 25ff0412..33e33a70 100644 --- a/lnbits/extensions/bleskomat/lnurl_api.py +++ b/lnbits/extensions/bleskomat/lnurl_api.py @@ -3,6 +3,7 @@ import math import traceback from http import HTTPStatus +from loguru import logger from starlette.requests import Request from . import bleskomat_ext @@ -122,7 +123,7 @@ async def api_bleskomat_lnurl(req: Request): except LnurlHttpError as e: return {"status": "ERROR", "reason": str(e)} except Exception as e: - print(str(e)) + logger.error(str(e)) return {"status": "ERROR", "reason": "Unexpected error"} return {"status": "OK"} diff --git a/lnbits/extensions/bleskomat/models.py b/lnbits/extensions/bleskomat/models.py index 267cc949..364cbe22 100644 --- a/lnbits/extensions/bleskomat/models.py +++ b/lnbits/extensions/bleskomat/models.py @@ -3,11 +3,12 @@ import time from typing import Dict from fastapi.params import Query +from loguru import logger from pydantic import BaseModel, validator from starlette.requests import Request from lnbits import bolt11 -from lnbits.core.services import pay_invoice, PaymentFailure +from lnbits.core.services import PaymentFailure, pay_invoice from . import db from .exchange_rates import exchange_rate_providers, fiat_currencies @@ -125,7 +126,7 @@ class BleskomatLnurl(BaseModel): except (ValueError, PermissionError, PaymentFailure) as e: raise LnurlValidationError("Failed to pay invoice: " + str(e)) except Exception as e: - print(str(e)) + logger.error(str(e)) raise LnurlValidationError("Unexpected error") async def use(self, conn) -> bool: diff --git a/lnbits/extensions/bleskomat/templates/bleskomat/_api_docs.html b/lnbits/extensions/bleskomat/templates/bleskomat/_api_docs.html index 210d534c..2a7160bd 100644 --- a/lnbits/extensions/bleskomat/templates/bleskomat/_api_docs.html +++ b/lnbits/extensions/bleskomat/templates/bleskomat/_api_docs.html @@ -62,4 +62,5 @@

+ diff --git a/lnbits/extensions/bleskomat/views_api.py b/lnbits/extensions/bleskomat/views_api.py index 0680de0d..e29e3fe7 100644 --- a/lnbits/extensions/bleskomat/views_api.py +++ b/lnbits/extensions/bleskomat/views_api.py @@ -1,6 +1,7 @@ from http import HTTPStatus from fastapi import Depends, Query +from loguru import logger from starlette.exceptions import HTTPException 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 ) except Exception as e: - print(e) + logger.error(e) raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=f'Failed to fetch BTC/{fiat_currency} currency pair from "{exchange_rate_provider}"', diff --git a/lnbits/extensions/copilot/__init__.py b/lnbits/extensions/copilot/__init__.py index 8a634267..806801ce 100644 --- a/lnbits/extensions/copilot/__init__.py +++ b/lnbits/extensions/copilot/__init__.py @@ -12,7 +12,7 @@ db = Database("ext_copilot") copilot_static_files = [ { "path": "/copilot/static", - "app": StaticFiles(directory="lnbits/extensions/copilot/static"), + "app": StaticFiles(packages=[("lnbits", "extensions/copilot/static")]), "name": "copilot_static", } ] diff --git a/lnbits/extensions/copilot/models.py b/lnbits/extensions/copilot/models.py index a279879d..b9b43ccf 100644 --- a/lnbits/extensions/copilot/models.py +++ b/lnbits/extensions/copilot/models.py @@ -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 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): diff --git a/lnbits/extensions/copilot/tasks.py b/lnbits/extensions/copilot/tasks.py index 351eb24b..f3c5cff8 100644 --- a/lnbits/extensions/copilot/tasks.py +++ b/lnbits/extensions/copilot/tasks.py @@ -25,7 +25,7 @@ async def wait_for_paid_invoices(): async def on_invoice_paid(payment: Payment) -> None: webhook = None data = None - if "copilot" != payment.extra.get("tag"): + if payment.extra.get("tag") != "copilot": # not an copilot invoice return diff --git a/lnbits/extensions/copilot/templates/copilot/_api_docs.html b/lnbits/extensions/copilot/templates/copilot/_api_docs.html index 64acba14..6105d169 100644 --- a/lnbits/extensions/copilot/templates/copilot/_api_docs.html +++ b/lnbits/extensions/copilot/templates/copilot/_api_docs.html @@ -14,6 +14,7 @@ label="API info" :content-inset-level="0.5" > + @@ -31,8 +32,8 @@ [<copilot_object>, ...]
Curl example
curl -X POST {{ request.base_url }}api/v1/copilot -d '{"title": - <string>, "animation": <string>, + >curl -X POST {{ request.base_url }}copilot/api/v1/copilot -d + '{"title": <string>, "animation": <string>, "show_message":<string>, "amount": <integer>, "lnurl_title": <string>}' -H "Content-type: application/json" -H "X-Api-Key: {{user.wallets[0].adminkey }}" @@ -59,11 +60,11 @@
Curl example
curl -X POST {{ request.base_url - }}api/v1/copilot/<copilot_id> -d '{"title": <string>, - "animation": <string>, "show_message":<string>, - "amount": <integer>, "lnurl_title": <string>}' -H - "Content-type: application/json" -H "X-Api-Key: - {{user.wallets[0].adminkey }}" + }}copilot/api/v1/copilot/<copilot_id> -d '{"title": + <string>, "animation": <string>, + "show_message":<string>, "amount": <integer>, + "lnurl_title": <string>}' -H "Content-type: application/json" + -H "X-Api-Key: {{user.wallets[0].adminkey }}"
@@ -87,8 +88,9 @@ [<copilot_object>, ...]
Curl example
curl -X GET {{ request.base_url }}api/v1/copilot/<copilot_id> - -H "X-Api-Key: {{ user.wallets[0].inkey }}" + >curl -X GET {{ request.base_url + }}copilot/api/v1/copilot/<copilot_id> -H "X-Api-Key: {{ + user.wallets[0].inkey }}"
@@ -110,8 +112,8 @@ [<copilot_object>, ...]
Curl example
curl -X GET {{ request.base_url }}api/v1/copilots -H "X-Api-Key: {{ - user.wallets[0].inkey }}" + >curl -X GET {{ request.base_url }}copilot/api/v1/copilots -H + "X-Api-Key: {{ user.wallets[0].inkey }}" @@ -136,7 +138,7 @@
Curl example
curl -X DELETE {{ request.base_url - }}api/v1/copilot/<copilot_id> -H "X-Api-Key: {{ + }}copilot/api/v1/copilot/<copilot_id> -H "X-Api-Key: {{ user.wallets[0].adminkey }}" @@ -161,9 +163,10 @@
Curl example
curl -X GET {{ request.base_url }}/api/v1/copilot/ws/<string, - copilot_id>/<string, comment>/<string, gif name> -H - "X-Api-Key: {{ user.wallets[0].adminkey }}" + >curl -X GET {{ request.base_url + }}copilot/api/v1/copilot/ws/<string, copilot_id>/<string, + comment>/<string, gif name> -H "X-Api-Key: {{ + user.wallets[0].adminkey }}" diff --git a/lnbits/extensions/discordbot/__init__.py b/lnbits/extensions/discordbot/__init__.py index ff60dd62..21989b24 100644 --- a/lnbits/extensions/discordbot/__init__.py +++ b/lnbits/extensions/discordbot/__init__.py @@ -9,7 +9,7 @@ db = Database("ext_discordbot") discordbot_static_files = [ { "path": "/discordbot/static", - "app": StaticFiles(directory="lnbits/extensions/discordbot/static"), + "app": StaticFiles(packages=[("lnbits", "extensions/discordbot/static")]), "name": "discordbot_static", } ] diff --git a/lnbits/extensions/discordbot/models.py b/lnbits/extensions/discordbot/models.py index 4be367f8..8b9cd822 100644 --- a/lnbits/extensions/discordbot/models.py +++ b/lnbits/extensions/discordbot/models.py @@ -1,8 +1,8 @@ from sqlite3 import Row +from typing import Optional from fastapi.param_functions import Query from pydantic import BaseModel -from typing import Optional class CreateUserData(BaseModel): @@ -11,6 +11,7 @@ class CreateUserData(BaseModel): admin_id: str = Query(...) discord_id: str = Query("") + class CreateUserWallet(BaseModel): user_id: str = Query(...) wallet_name: str = Query(...) @@ -23,6 +24,7 @@ class Users(BaseModel): admin: str discord_id: str + class Wallets(BaseModel): id: str admin: str diff --git a/lnbits/extensions/discordbot/templates/discordbot/_api_docs.html b/lnbits/extensions/discordbot/templates/discordbot/_api_docs.html index 40fcfb12..b57e4ab1 100644 --- a/lnbits/extensions/discordbot/templates/discordbot/_api_docs.html +++ b/lnbits/extensions/discordbot/templates/discordbot/_api_docs.html @@ -10,12 +10,19 @@ Discord Bot: Connect Discord users to LNbits.

- Connect your LNbits instance to a Discord Bot leveraging LNbits as a community based lightning node.
+ Connect your LNbits instance to a + Discord Bot + leveraging LNbits as a community based lightning node.
- Created by, Chris Lennon
+ Created by, + Chris Lennon +
- Based on User Manager, by Ben ArcBen Arc

@@ -27,6 +34,7 @@ label="API info" :content-inset-level="0.5" > + @@ -149,8 +157,9 @@ curl -X POST {{ request.base_url }}discordbot/api/v1/users -d '{"admin_id": "{{ user.id }}", "wallet_name": <string>, - "user_name": <string>, "discord_id": <string>}' -H "X-Api-Key: {{ - user.wallets[0].inkey }}" -H "Content-type: application/json" + "user_name": <string>, "discord_id": <string>}' -H + "X-Api-Key: {{ user.wallets[0].inkey }}" -H "Content-type: + application/json" diff --git a/lnbits/extensions/discordbot/templates/discordbot/index.html b/lnbits/extensions/discordbot/templates/discordbot/index.html index 782f8bb6..70965a70 100644 --- a/lnbits/extensions/discordbot/templates/discordbot/index.html +++ b/lnbits/extensions/discordbot/templates/discordbot/index.html @@ -136,7 +136,8 @@
-
LNbits Discord Bot Extension +
+ LNbits Discord Bot Extension
@@ -236,7 +237,12 @@ columns: [ {name: 'id', align: 'left', label: 'ID', field: 'id'}, {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: { rowsPerPage: 10 diff --git a/lnbits/extensions/discordbot/views_api.py b/lnbits/extensions/discordbot/views_api.py index 64d1df1a..6f213a89 100644 --- a/lnbits/extensions/discordbot/views_api.py +++ b/lnbits/extensions/discordbot/views_api.py @@ -109,9 +109,7 @@ async def api_discordbot_wallet_transactions( async def api_discordbot_users_wallets( user_id, wallet: WalletTypeInfo = Depends(get_key_type) ): - return [ - s_wallet.dict() for s_wallet in await get_discordbot_users_wallets(user_id) - ] + return [s_wallet.dict() for s_wallet in await get_discordbot_users_wallets(user_id)] @discordbot_ext.delete("/api/v1/wallets/{wallet_id}") diff --git a/lnbits/extensions/events/crud.py b/lnbits/extensions/events/crud.py index 9e04476d..4cc86ac4 100644 --- a/lnbits/extensions/events/crud.py +++ b/lnbits/extensions/events/crud.py @@ -16,7 +16,7 @@ async def create_ticket( INSERT INTO events.ticket (id, wallet, event, name, email, registered, paid) VALUES (?, ?, ?, ?, ?, ?, ?) """, - (payment_hash, wallet, event, name, email, False, False), + (payment_hash, wallet, event, name, email, False, True), ) ticket = await get_ticket(payment_hash) diff --git a/lnbits/extensions/events/templates/events/_api_docs.html b/lnbits/extensions/events/templates/events/_api_docs.html index a5c82174..d2fa890e 100644 --- a/lnbits/extensions/events/templates/events/_api_docs.html +++ b/lnbits/extensions/events/templates/events/_api_docs.html @@ -20,4 +20,5 @@

+ diff --git a/lnbits/extensions/events/templates/events/display.html b/lnbits/extensions/events/templates/events/display.html index 4c1f557f..4589c578 100644 --- a/lnbits/extensions/events/templates/events/display.html +++ b/lnbits/extensions/events/templates/events/display.html @@ -135,15 +135,7 @@ var self = this axios - .post( - '/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 - } - ) + .get('/events/api/v1/tickets/' + '{{ event_id }}') .then(function (response) { self.paymentReq = response.data.payment_request self.paymentCheck = response.data.payment_hash @@ -161,7 +153,17 @@ paymentChecker = setInterval(function () { 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) { if (res.data.paid) { clearInterval(paymentChecker) diff --git a/lnbits/extensions/events/templates/events/index.html b/lnbits/extensions/events/templates/events/index.html index 409ed2af..7f34a3e2 100644 --- a/lnbits/extensions/events/templates/events/index.html +++ b/lnbits/extensions/events/templates/events/index.html @@ -381,10 +381,10 @@ getTickets: function () { var self = this LNbits.api - .request( - 'GET', - '/events/api/v1/tickets?all_wallets=true', - this.g.user.wallets[0].inkey + .request( + 'GET', + '/events/api/v1/tickets?all_wallets=true', + this.g.user.wallets[0].inkey ) .then(function (response) { console.log(response) diff --git a/lnbits/extensions/events/templates/events/register.html b/lnbits/extensions/events/templates/events/register.html index 4dff9afb..43d43070 100644 --- a/lnbits/extensions/events/templates/events/register.html +++ b/lnbits/extensions/events/templates/events/register.html @@ -133,7 +133,10 @@ var self = this LNbits.api - .request('GET', '/events/api/v1/register/ticket/' + res) + .request( + 'GET', + '/events/api/v1/register/ticket/' + res.split('//')[1] + ) .then(function (response) { self.$q.notify({ type: 'positive', diff --git a/lnbits/extensions/events/templates/events/ticket.html b/lnbits/extensions/events/templates/events/ticket.html index a53f834f..21b7cfa8 100644 --- a/lnbits/extensions/events/templates/events/ticket.html +++ b/lnbits/extensions/events/templates/events/ticket.html @@ -13,9 +13,8 @@

diff --git a/lnbits/extensions/events/views_api.py b/lnbits/extensions/events/views_api.py index 87edb07d..45ee4de0 100644 --- a/lnbits/extensions/events/views_api.py +++ b/lnbits/extensions/events/views_api.py @@ -97,8 +97,8 @@ async def api_tickets( return [ticket.dict() for ticket in await get_tickets(wallet_ids)] -@events_ext.post("/api/v1/tickets/{event_id}/{sats}") -async def api_ticket_make_ticket(event_id, sats, data: CreateTicket): +@events_ext.get("/api/v1/tickets/{event_id}") +async def api_ticket_make_ticket(event_id): event = await get_event(event_id) if not event: raise HTTPException( @@ -107,37 +107,35 @@ async def api_ticket_make_ticket(event_id, sats, data: CreateTicket): try: payment_hash, payment_request = await create_invoice( wallet_id=event.wallet, - amount=int(sats), + amount=event.price_per_ticket, memo=f"{event_id}", extra={"tag": "events"}, ) except Exception as e: raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e)) - ticket = await create_ticket( - payment_hash=payment_hash, - wallet=event.wallet, - event=event_id, - name=data.name, - email=data.email, - ) - - if not ticket: - raise HTTPException( - 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) - +@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"]: - await set_ticket_paid(payment_hash=payment_hash) + ticket = await create_ticket( + payment_hash=payment_hash, + wallet=event.wallet, + event=event_id, + name=data.name, + email=data.email, + ) + + if not ticket: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail=f"Event could not be fetched." + ) + return {"paid": True, "ticket_id": ticket.id} except Exception: raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Not paid") diff --git a/lnbits/extensions/example/README.md b/lnbits/extensions/example/README.md new file mode 100644 index 00000000..27729459 --- /dev/null +++ b/lnbits/extensions/example/README.md @@ -0,0 +1,11 @@ +

Example Extension

+

*tagline*

+This is an example extension to help you organise and build you own. + +Try to include an image + + + +

If your extension has API endpoints, include useful ones here

+ +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" diff --git a/lnbits/extensions/example/__init__.py b/lnbits/extensions/example/__init__.py new file mode 100644 index 00000000..96cc6428 --- /dev/null +++ b/lnbits/extensions/example/__init__.py @@ -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 diff --git a/lnbits/extensions/example/example.config.json b/lnbits/extensions/example/example.config.json new file mode 100644 index 00000000..b8eec193 --- /dev/null +++ b/lnbits/extensions/example/example.config.json @@ -0,0 +1,6 @@ +{ + "name": "Build your own!!", + "short_description": "Join us, make an extension", + "icon": "info", + "contributors": ["github_username"] +} diff --git a/lnbits/extensions/example/migrations.py b/lnbits/extensions/example/migrations.py new file mode 100644 index 00000000..99d7c362 --- /dev/null +++ b/lnbits/extensions/example/migrations.py @@ -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} +# ); +# """ +# ) diff --git a/lnbits/extensions/example/models.py b/lnbits/extensions/example/models.py new file mode 100644 index 00000000..bfeb7517 --- /dev/null +++ b/lnbits/extensions/example/models.py @@ -0,0 +1,5 @@ +# from pydantic import BaseModel + +# class Example(BaseModel): +# id: str +# wallet: str diff --git a/lnbits/extensions/example/templates/example/index.html b/lnbits/extensions/example/templates/example/index.html new file mode 100644 index 00000000..d732ef37 --- /dev/null +++ b/lnbits/extensions/example/templates/example/index.html @@ -0,0 +1,59 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} + + +
+ Frameworks used by {{SITE_TITLE}} +
+ + + {% raw %} + + + {{ tool.name }} + {{ tool.language }} + + {% endraw %} + + + +

+ A magical "g" is always available, with info about the user, wallets and + extensions: +

+ {% raw %}{{ g }}{% endraw %} +
+
+{% endblock %} {% block scripts %} {{ window_vars(user) }} + +{% endblock %} diff --git a/lnbits/extensions/example/views.py b/lnbits/extensions/example/views.py new file mode 100644 index 00000000..252b4726 --- /dev/null +++ b/lnbits/extensions/example/views.py @@ -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()} + ) diff --git a/lnbits/extensions/example/views_api.py b/lnbits/extensions/example/views_api.py new file mode 100644 index 00000000..5b702717 --- /dev/null +++ b/lnbits/extensions/example/views_api.py @@ -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 diff --git a/lnbits/extensions/jukebox/__init__.py b/lnbits/extensions/jukebox/__init__.py index 702a6c67..4559dccf 100644 --- a/lnbits/extensions/jukebox/__init__.py +++ b/lnbits/extensions/jukebox/__init__.py @@ -12,7 +12,7 @@ db = Database("ext_jukebox") jukebox_static_files = [ { "path": "/jukebox/static", - "app": StaticFiles(directory="lnbits/extensions/jukebox/static"), + "app": StaticFiles(packages=[("lnbits", "extensions/jukebox/static")]), "name": "jukebox_static", } ] diff --git a/lnbits/extensions/jukebox/crud.py b/lnbits/extensions/jukebox/crud.py index caaac7e5..d160daee 100644 --- a/lnbits/extensions/jukebox/crud.py +++ b/lnbits/extensions/jukebox/crud.py @@ -41,7 +41,7 @@ async def update_jukebox( q = ", ".join([f"{field[0]} = ?" for field in data]) items = [f"{field[1]}" for field in data] items.append(juke_id) - q = q.replace("user", '"user"', 1) # hack to make user be "user"! + q = q.replace("user", '"user"', 1) # hack to make user be "user"! await db.execute(f"UPDATE jukebox.jukebox SET {q} WHERE id = ?", (items)) row = await db.fetchone("SELECT * FROM jukebox.jukebox WHERE id = ?", (juke_id,)) return Jukebox(**row) if row else None diff --git a/lnbits/extensions/jukebox/models.py b/lnbits/extensions/jukebox/models.py index 093961e4..90984b03 100644 --- a/lnbits/extensions/jukebox/models.py +++ b/lnbits/extensions/jukebox/models.py @@ -1,9 +1,9 @@ -from typing import NamedTuple from sqlite3 import Row +from typing import NamedTuple, Optional + from fastapi.param_functions import Query -from pydantic.main import BaseModel from pydantic import BaseModel -from typing import Optional +from pydantic.main import BaseModel class CreateJukeLinkData(BaseModel): diff --git a/lnbits/extensions/jukebox/static/js/index.js b/lnbits/extensions/jukebox/static/js/index.js index 049b600e..fe724450 100644 --- a/lnbits/extensions/jukebox/static/js/index.js +++ b/lnbits/extensions/jukebox/static/js/index.js @@ -3,9 +3,9 @@ Vue.component(VueQrcode.name, VueQrcode) var mapJukebox = obj => { - if(obj.sp_device){ + if (obj.sp_device) { obj._data = _.clone(obj) - + obj.sp_id = obj._data.id obj.device = obj._data.sp_device.split('-')[0] playlists = obj._data.sp_playlists.split(',') @@ -17,11 +17,9 @@ var mapJukebox = obj => { obj.playlist = playlistsar.join() console.log(obj) return obj - } - else { + } else { return } - } new Vue({ @@ -87,14 +85,14 @@ new Vue({ var link = _.findWhere(this.JukeboxLinks, {id: linkId}) this.qrCodeDialog.data = _.clone(link) - + this.qrCodeDialog.data.url = window.location.protocol + '//' + window.location.host this.qrCodeDialog.show = true }, getJukeboxes() { self = this - + LNbits.api .request( 'GET', @@ -103,8 +101,7 @@ new Vue({ ) .then(function (response) { self.JukeboxLinks = response.data.map(function (obj) { - - return mapJukebox(obj) + return mapJukebox(obj) }) console.log(self.JukeboxLinks) }) @@ -154,7 +151,7 @@ new Vue({ submitSpotifyKeys() { self = this self.jukeboxDialog.data.user = self.g.user.id - + LNbits.api .request( 'POST', diff --git a/lnbits/extensions/jukebox/tasks.py b/lnbits/extensions/jukebox/tasks.py index 02241c7b..70a2e65d 100644 --- a/lnbits/extensions/jukebox/tasks.py +++ b/lnbits/extensions/jukebox/tasks.py @@ -16,7 +16,7 @@ async def wait_for_paid_invoices(): async def on_invoice_paid(payment: Payment) -> None: - if "jukebox" != payment.extra.get("tag"): + if payment.extra.get("tag") != "jukebox": # not a jukebox invoice return await update_jukebox_payment(payment.payment_hash, paid=True) diff --git a/lnbits/extensions/jukebox/templates/jukebox/_api_docs.html b/lnbits/extensions/jukebox/templates/jukebox/_api_docs.html index 791a55e7..4bae4965 100644 --- a/lnbits/extensions/jukebox/templates/jukebox/_api_docs.html +++ b/lnbits/extensions/jukebox/templates/jukebox/_api_docs.html @@ -24,6 +24,8 @@ label="API info" :content-inset-level="0.5" > + + @@ -37,8 +39,8 @@ [<jukebox_object>, ...]
Curl example
curl -X GET {{ request.base_url }}api/v1/jukebox -H "X-Api-Key: {{ - user.wallets[0].adminkey }}" + >curl -X GET {{ request.base_url }}jukebox/api/v1/jukebox -H + "X-Api-Key: {{ user.wallets[0].adminkey }}"
@@ -59,8 +61,9 @@ <jukebox_object>
Curl example
curl -X GET {{ request.base_url }}api/v1/jukebox/<juke_id> -H - "X-Api-Key: {{ user.wallets[0].adminkey }}" + >curl -X GET {{ request.base_url + }}jukebox/api/v1/jukebox/<juke_id> -H "X-Api-Key: {{ + user.wallets[0].adminkey }}" @@ -86,8 +89,8 @@ <jukbox_object>
Curl example
curl -X POST {{ request.base_url }}api/v1/jukebox/ -d '{"user": - <string, user_id>, "title": <string>, + >curl -X POST {{ request.base_url }}jukebox/api/v1/jukebox/ -d + '{"user": <string, user_id>, "title": <string>, "wallet":<string>, "sp_user": <string, spotify_user_account>, "sp_secret": <string, spotify_user_secret>, "sp_access_token": <string, @@ -116,8 +119,9 @@ <jukebox_object>
Curl example
curl -X DELETE {{ request.base_url }}api/v1/jukebox/<juke_id> - -H "X-Api-Key: {{ user.wallets[0].adminkey }}" + >curl -X DELETE {{ request.base_url + }}jukebox/api/v1/jukebox/<juke_id> -H "X-Api-Key: {{ + user.wallets[0].adminkey }}" diff --git a/lnbits/extensions/jukebox/templates/jukebox/index.html b/lnbits/extensions/jukebox/templates/jukebox/index.html index 9b4efbd5..a67767fb 100644 --- a/lnbits/extensions/jukebox/templates/jukebox/index.html +++ b/lnbits/extensions/jukebox/templates/jukebox/index.html @@ -117,7 +117,7 @@ > @@ -170,16 +170,25 @@
- + 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 - +
+ here
. + >Open the Spotify Developer Dashboard
+ - + - 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 +

+ + + {% raw %}{{ locationcb }}{{ jukeboxDialog.data.sp_id }}{% endraw + %} + + Click to copy URL +
{% raw %}{{ locationcb }}{{ jukeboxDialog.data.sp_id }}{% endraw - %} Click to copy URL - -
- Settings can be found - here. + >Open the Spotify Application Settings +

+

+ After adding the redirect URI, click the "Authorise access" button + below. +

@@ -281,7 +301,7 @@ None: - if "livestream" != payment.extra.get("tag"): + if payment.extra.get("tag") != "livestream": # not a livestream invoice return track = await get_track(payment.extra.get("track", -1)) if not track: - print("this should never happen", payment) + logger.error("this should never happen", payment) return if payment.extra.get("shared_with"): - print("payment was shared already", payment) + logger.error("payment was shared already", payment) return producer = await get_producer(track.producer) diff --git a/lnbits/extensions/livestream/templates/livestream/_api_docs.html b/lnbits/extensions/livestream/templates/livestream/_api_docs.html index 4c497d7f..34b6518d 100644 --- a/lnbits/extensions/livestream/templates/livestream/_api_docs.html +++ b/lnbits/extensions/livestream/templates/livestream/_api_docs.html @@ -17,6 +17,8 @@ label="API info" :content-inset-level="0.5" > + + [<livestream_object>, ...]
Curl example
curl -X GET {{ request.url_root }}api/v1/livestream -H "X-Api-Key: {{ - user.wallets[0].inkey }}" + >curl -X GET {{ request.base_url }}livestream/api/v1/livestream -H + "X-Api-Key: {{ user.wallets[0].inkey }}" @@ -59,8 +61,8 @@
Curl example
curl -X PUT {{ request.url_root - }}api/v1/livestream/track/<track_id> -H "X-Api-Key: {{ + >curl -X PUT {{ request.base_url }} + livestream/api/v1/livestream/track/<track_id> -H "X-Api-Key: {{ user.wallets[0].inkey }}" @@ -81,8 +83,8 @@
Curl example
curl -X PUT {{ request.url_root - }}api/v1/livestream/fee/<fee_pct> -H "X-Api-Key: {{ + >curl -X PUT {{ request.base_url }} + livestream/api/v1/livestream/fee/<fee_pct> -H "X-Api-Key: {{ user.wallets[0].inkey }}" @@ -109,11 +111,12 @@
Curl example
curl -X POST {{ request.url_root }}api/v1/livestream/tracks -d - '{"name": <string>, "download_url": <string>, - "price_msat": <integer>, "producer_id": <integer>, - "producer_name": <string>}' -H "Content-type: application/json" - -H "X-Api-Key: {{ user.wallets[0].adminkey }}" + >curl -X POST {{ request.base_url }} + livestream/api/v1/livestream/tracks -d '{"name": <string>, + "download_url": <string>, "price_msat": <integer>, + "producer_id": <integer>, "producer_name": <string>}' -H + "Content-type: application/json" -H "X-Api-Key: {{ + user.wallets[0].adminkey }}" @@ -123,6 +126,7 @@ dense expand-separator label="Delete a withdraw link" + class="q-pb-md" > @@ -136,8 +140,8 @@
Curl example
curl -X DELETE {{ request.url_root - }}api/v1/livestream/tracks/<track_id> -H "X-Api-Key: {{ + >curl -X DELETE {{ request.base_url }} + livestream/api/v1/livestream/tracks/<track_id> -H "X-Api-Key: {{ user.wallets[0].inkey }}"
diff --git a/lnbits/extensions/livestream/views.py b/lnbits/extensions/livestream/views.py index 4f1bd1c5..97f803a3 100644 --- a/lnbits/extensions/livestream/views.py +++ b/lnbits/extensions/livestream/views.py @@ -1,5 +1,4 @@ from http import HTTPStatus -# from mmap import MAP_DENYWRITE from fastapi.param_functions import Depends from fastapi.params import Query @@ -14,6 +13,8 @@ from lnbits.decorators import check_user_exists from . import livestream_ext, livestream_renderer from .crud import get_livestream_by_track, get_track +# from mmap import MAP_DENYWRITE + @livestream_ext.get("/", response_class=HTMLResponse) async def index(request: Request, user: User = Depends(check_user_exists)): diff --git a/lnbits/extensions/lnaddress/crud.py b/lnbits/extensions/lnaddress/crud.py index 3b5822b4..25338215 100644 --- a/lnbits/extensions/lnaddress/crud.py +++ b/lnbits/extensions/lnaddress/crud.py @@ -1,6 +1,8 @@ from datetime import datetime, timedelta from typing import List, Optional, Union +from loguru import logger + from lnbits.helpers import urlsafe_short_hash from . import db @@ -186,9 +188,9 @@ async def purge_addresses(domain_id: str): ) # give user 1 day to topup is address 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"]) if paid and expired: - print("DELETE PAID_EXP", r["username"]) + logger.debug("DELETE PAID_EXP", r["username"]) await delete_address(r["id"]) diff --git a/lnbits/extensions/lnaddress/lnurl.py b/lnbits/extensions/lnaddress/lnurl.py index fa26fa91..e6a1ff48 100644 --- a/lnbits/extensions/lnaddress/lnurl.py +++ b/lnbits/extensions/lnaddress/lnurl.py @@ -9,6 +9,7 @@ from lnurl import ( # type: ignore LnurlPayActionResponse, LnurlPayResponse, ) +from loguru import logger from starlette.requests import Request from starlette.responses import HTMLResponse @@ -38,13 +39,12 @@ async def lnurl_response(username: str, domain: str, request: Request): "maxSendable": 1000000000, } - print("RESP", resp) + logger.debug("RESP", resp) return resp @lnaddress_ext.get("/lnurl/cb/{address_id}", name="lnaddress.lnurl_callback") async def lnurl_callback(address_id, amount: int = Query(...)): - print("PING") address = await get_address(address_id) if not address: return LnurlErrorResponse(reason=f"Address not found").dict() diff --git a/lnbits/extensions/lnaddress/tasks.py b/lnbits/extensions/lnaddress/tasks.py index 9702c70b..9abe10c3 100644 --- a/lnbits/extensions/lnaddress/tasks.py +++ b/lnbits/extensions/lnaddress/tasks.py @@ -43,13 +43,13 @@ async def call_webhook_on_paid(payment_hash): 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 set_address_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 set_address_renewed( diff --git a/lnbits/extensions/lnaddress/templates/lnaddress/_api_docs.html b/lnbits/extensions/lnaddress/templates/lnaddress/_api_docs.html index ab7ab4bd..49155f27 100644 --- a/lnbits/extensions/lnaddress/templates/lnaddress/_api_docs.html +++ b/lnbits/extensions/lnaddress/templates/lnaddress/_api_docs.html @@ -31,6 +31,7 @@ label="API info" :content-inset-level="0.5" > + @@ -45,7 +46,7 @@ JSON list of users
Curl example
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 }}"
@@ -81,7 +82,7 @@ >
Curl example
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": <string>, "cf_token": <string>,"cf_zone_id": <string>,"webhook": <Optional string> ,"cost": <integer>}' -H "X-Api-Key: {{ @@ -101,7 +102,7 @@ {"X-Api-Key": <string>}
Curl example
curl -X DELETE {{ request.url_root + >curl -X DELETE {{ request.base_url }}lnaddress/api/v1/domains/<domain_id> -H "X-Api-Key: {{ user.wallets[0].inkey }}" @@ -122,7 +123,7 @@ JSON list of addresses
Curl example
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 }}" @@ -142,14 +143,20 @@ JSON list of addresses
Curl example
curl -X GET {{ request.url_root + >curl -X GET {{ request.base_url }}lnaddress/api/v1/address/<domain>/<username>/<wallet_key> -H "X-Api-Key: {{ user.wallets[0].inkey }}"
- + {"X-Api-Key": <string>}
Curl example
curl -X POST {{ request.url_root + >curl -X POST {{ request.base_url }}lnaddress/api/v1/address/<domain_id> -d '{"domain": <string>, "username": <string>,"email": <Optional string>, "wallet_endpoint": <string>, "wallet_key": diff --git a/lnbits/extensions/lnaddress/templates/lnaddress/display.html b/lnbits/extensions/lnaddress/templates/lnaddress/display.html index 7164752c..9436525d 100644 --- a/lnbits/extensions/lnaddress/templates/lnaddress/display.html +++ b/lnbits/extensions/lnaddress/templates/lnaddress/display.html @@ -372,7 +372,7 @@ } data.wallet_endpoint = data.wallet_endpoint ?? '{{ root_url }}' data.duration = parseInt(data.duration) - + axios .post('/lnaddress/api/v1/address/{{ domain_id }}', data) .then(response => { diff --git a/lnbits/extensions/lnaddress/templates/lnaddress/index.html b/lnbits/extensions/lnaddress/templates/lnaddress/index.html index dffef837..4c833932 100644 --- a/lnbits/extensions/lnaddress/templates/lnaddress/index.html +++ b/lnbits/extensions/lnaddress/templates/lnaddress/index.html @@ -191,9 +191,13 @@ type="text" label="Cloudflare API token" > - + Your API key in cloudflare diff --git a/lnbits/extensions/lndhub/decorators.py b/lnbits/extensions/lndhub/decorators.py index 14931164..4698e9b9 100644 --- a/lnbits/extensions/lndhub/decorators.py +++ b/lnbits/extensions/lndhub/decorators.py @@ -1,14 +1,12 @@ from base64 import b64decode -from fastapi.param_functions import Security - -from fastapi.security.api_key import APIKeyHeader from fastapi import Request, status +from fastapi.param_functions import Security +from fastapi.security.api_key import APIKeyHeader from starlette.exceptions import HTTPException from lnbits.decorators import WalletTypeInfo, get_key_type # type: ignore - api_key_header_auth = APIKeyHeader( name="AUTHORIZATION", auto_error=False, diff --git a/lnbits/extensions/lndhub/templates/lndhub/_instructions.html b/lnbits/extensions/lndhub/templates/lndhub/_instructions.html index 4db79aba..005bced5 100644 --- a/lnbits/extensions/lndhub/templates/lndhub/_instructions.html +++ b/lnbits/extensions/lndhub/templates/lndhub/_instructions.html @@ -31,5 +31,6 @@
+
diff --git a/lnbits/extensions/lndhub/views.py b/lnbits/extensions/lndhub/views.py index 4b015c09..38a33a34 100644 --- a/lnbits/extensions/lndhub/views.py +++ b/lnbits/extensions/lndhub/views.py @@ -1,8 +1,10 @@ -from lnbits.decorators import check_user_exists -from . import lndhub_ext, lndhub_renderer from fastapi import Request from fastapi.params import Depends + from lnbits.core.models import User +from lnbits.decorators import check_user_exists + +from . import lndhub_ext, lndhub_renderer @lndhub_ext.get("/") diff --git a/lnbits/extensions/lndhub/views_api.py b/lnbits/extensions/lndhub/views_api.py index 8cbf5b01..a3160fa9 100644 --- a/lnbits/extensions/lndhub/views_api.py +++ b/lnbits/extensions/lndhub/views_api.py @@ -1,6 +1,5 @@ -import time import asyncio - +import time from base64 import urlsafe_b64encode 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.services import create_invoice, pay_invoice 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 .decorators import check_wallet, require_admin_key diff --git a/lnbits/extensions/lnticket/crud.py b/lnbits/extensions/lnticket/crud.py index 8fe17090..3254ad43 100644 --- a/lnbits/extensions/lnticket/crud.py +++ b/lnbits/extensions/lnticket/crud.py @@ -1,11 +1,12 @@ -from lnbits.core.models import Wallet from typing import List, Optional, Union +import httpx + +from lnbits.core.models import Wallet from lnbits.helpers import urlsafe_short_hash from . import db -from .models import CreateFormData, CreateTicketData, Tickets, Forms -import httpx +from .models import CreateFormData, CreateTicketData, Forms, Tickets async def create_ticket( diff --git a/lnbits/extensions/lnticket/models.py b/lnbits/extensions/lnticket/models.py index 50ffc1e1..a7a3cf8c 100644 --- a/lnbits/extensions/lnticket/models.py +++ b/lnbits/extensions/lnticket/models.py @@ -1,4 +1,5 @@ from typing import Optional + from fastapi.param_functions import Query from pydantic import BaseModel diff --git a/lnbits/extensions/lnticket/tasks.py b/lnbits/extensions/lnticket/tasks.py index 2d79fb51..7e672115 100644 --- a/lnbits/extensions/lnticket/tasks.py +++ b/lnbits/extensions/lnticket/tasks.py @@ -1,5 +1,7 @@ import asyncio +from loguru import logger + from lnbits.core.models import Payment 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: - if "lnticket" != payment.extra.get("tag"): + if payment.extra.get("tag") != "lnticket": # not a lnticket invoice return ticket = await get_ticket(payment.checking_id) if not ticket: - print("this should never happen", payment) + logger.error("this should never happen", payment) return await payment.set_pending(False) diff --git a/lnbits/extensions/lnticket/templates/lnticket/_api_docs.html b/lnbits/extensions/lnticket/templates/lnticket/_api_docs.html index 69328f38..e9340924 100644 --- a/lnbits/extensions/lnticket/templates/lnticket/_api_docs.html +++ b/lnbits/extensions/lnticket/templates/lnticket/_api_docs.html @@ -19,4 +19,5 @@

+ diff --git a/lnbits/extensions/lnticket/templates/lnticket/index.html b/lnbits/extensions/lnticket/templates/lnticket/index.html index 6572d98a..6dafb4c2 100644 --- a/lnbits/extensions/lnticket/templates/lnticket/index.html +++ b/lnbits/extensions/lnticket/templates/lnticket/index.html @@ -120,7 +120,7 @@ - {{ col.label }} + {{ col.label }} @@ -145,11 +145,13 @@ icon="launch" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" @click="ticketCard(props)" - > Click to show ticket + > Click to show ticket - {{ 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 }} @@ -410,7 +412,7 @@ }) }) }, - ticketCard(ticket){ + ticketCard(ticket) { this.ticketDialog.show = true let {date, email, ltext, name} = ticket.row this.ticketDialog.data = { @@ -469,7 +471,7 @@ }, updateformDialog: function (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.wallet = link.wallet diff --git a/lnbits/extensions/lnurldevice/lnurl.py b/lnbits/extensions/lnurldevice/lnurl.py index 520a85eb..5e25dadb 100644 --- a/lnbits/extensions/lnurldevice/lnurl.py +++ b/lnbits/extensions/lnurldevice/lnurl.py @@ -1,30 +1,26 @@ import base64 import hashlib +import hmac from http import HTTPStatus +from io import BytesIO from typing import Optional -from embit import bech32 -from embit import compact -import base64 -from io import BytesIO -import hmac - +from embit import bech32, compact from fastapi import Request from fastapi.param_functions import Query from starlette.exceptions import HTTPException 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.utils.exchange_rates import fiat_amount_as_satoshis from . import lnurldevice_ext from .crud import ( create_lnurldevicepayment, get_lnurldevice, get_lnurldevicepayment, - update_lnurldevicepayment, get_lnurlpayload, + update_lnurldevicepayment, ) @@ -150,7 +146,7 @@ async def lnurl_v1_params( "defaultDescription": device.title, } price_msat = int(price_msat * ((device.profit / 100) + 1) / 1000) - print(price_msat) + lnurldevicepayment = await create_lnurldevicepayment( deviceid=device.id, payload=p, @@ -204,7 +200,7 @@ async def lnurl_callback( extra={"tag": "withdraw"}, ) return {"status": "OK"} - print(lnurldevicepayment.sats) + payment_hash, payment_request = await create_invoice( wallet_id=device.wallet, amount=lnurldevicepayment.sats / 1000, diff --git a/lnbits/extensions/lnurldevice/migrations.py b/lnbits/extensions/lnurldevice/migrations.py index 67065347..c7899282 100644 --- a/lnbits/extensions/lnurldevice/migrations.py +++ b/lnbits/extensions/lnurldevice/migrations.py @@ -38,7 +38,7 @@ async def m001_initial(db): async def m002_redux(db): """ - Moves everything from lnurlpos to lnurldevices + Moves everything from lnurlpos to lnurldevice """ try: for row in [ diff --git a/lnbits/extensions/lnurldevice/templates/lnurldevice/_api_docs.html b/lnbits/extensions/lnurldevice/templates/lnurldevice/_api_docs.html index af69b76e..7f9afa27 100644 --- a/lnbits/extensions/lnurldevice/templates/lnurldevice/_api_docs.html +++ b/lnbits/extensions/lnurldevice/templates/lnurldevice/_api_docs.html @@ -17,6 +17,12 @@ label="API info" :content-inset-level="0.5" > + [<lnurldevice_object>, ...]
Curl example
curl -X POST {{ request.base_url }}api/v1/lnurldevice -d '{"title": - <string>, "message":<string>, "currency": + >curl -X POST {{ request.base_url }}lnurldevice/api/v1/lnurlpos -d + '{"title": <string>, "message":<string>, "currency": <integer>}' -H "Content-type: application/json" -H "X-Api-Key: {{user.wallets[0].adminkey }}" @@ -71,7 +77,7 @@
Curl example
curl -X POST {{ request.base_url - }}api/v1/lnurlpos/<lnurldevice_id> -d ''{"title": + }}lnurldevice/api/v1/lnurlpos/<lnurldevice_id> -d ''{"title": <string>, "message":<string>, "currency": <integer>} -H "Content-type: application/json" -H "X-Api-Key: {{user.wallets[0].adminkey }}" @@ -104,8 +110,8 @@
Curl example
curl -X GET {{ request.base_url - }}api/v1/lnurlpos/<lnurldevice_id> -H "X-Api-Key: {{ - user.wallets[0].inkey }}" + }}lnurldevice/api/v1/lnurlpos/<lnurldevice_id> -H "X-Api-Key: + {{ user.wallets[0].inkey }}" @@ -120,7 +126,7 @@ GET - /lnurldevice/api/v1/lnurlposs
Headers
{"X-Api-Key": <invoice_key>}
@@ -133,7 +139,7 @@ [<lnurldevice_object>, ...]
Curl example
curl -X GET {{ request.base_url }}api/v1/lnurldevices -H + >curl -X GET {{ request.base_url }}lnurldevice/api/v1/lnurlpos -H "X-Api-Key: {{ user.wallets[0].inkey }}"
@@ -159,8 +165,8 @@
Curl example
curl -X DELETE {{ request.base_url - }}api/v1/lnurlpos/<lnurldevice_id> -H "X-Api-Key: {{ - user.wallets[0].adminkey }}" + }}lnurldevice/api/v1/lnurlpos/<lnurldevice_id> -H "X-Api-Key: + {{ user.wallets[0].adminkey }}" diff --git a/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html b/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html index b51e2556..24d19484 100644 --- a/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html +++ b/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html @@ -480,12 +480,11 @@ updatedData ) .then(function (response) { - self.lnurldeviceLinks = _.reject( - self.lnurldeviceLinks, - function (obj) { - return obj.id === updatedData.id - } - ) + self.lnurldeviceLinks = _.reject(self.lnurldeviceLinks, function ( + obj + ) { + return obj.id === updatedData.id + }) self.lnurldeviceLinks.push(maplnurldevice(response.data)) self.formDialoglnurldevice.show = false self.clearFormDialoglnurldevice() diff --git a/lnbits/extensions/lnurlp/__init__.py b/lnbits/extensions/lnurlp/__init__.py index 40981748..e2486df0 100644 --- a/lnbits/extensions/lnurlp/__init__.py +++ b/lnbits/extensions/lnurlp/__init__.py @@ -12,7 +12,7 @@ db = Database("ext_lnurlp") lnurlp_static_files = [ { "path": "/lnurlp/static", - "app": StaticFiles(directory="lnbits/extensions/lnurlp/static"), + "app": StaticFiles(packages=[("lnbits", "extensions/lnurlp/static")]), "name": "lnurlp_static", } ] diff --git a/lnbits/extensions/lnurlp/crud.py b/lnbits/extensions/lnurlp/crud.py index 4215faf6..9cb01fde 100644 --- a/lnbits/extensions/lnurlp/crud.py +++ b/lnbits/extensions/lnurlp/crud.py @@ -1,14 +1,16 @@ from typing import List, Optional, Union from lnbits.db import SQLITE + from . import db -from .models import PayLink, CreatePayLinkData +from .models import CreatePayLinkData, PayLink async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink: returning = "" if db.type == SQLITE else "RETURNING ID" method = db.execute if db.type == SQLITE else db.fetchone + result = await (method)( f""" INSERT INTO lnurlp.pay_links ( @@ -22,9 +24,10 @@ async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink: success_text, success_url, comment_chars, - currency + currency, + fiat_base_multiplier ) - VALUES (?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?) {returning} """, ( @@ -37,6 +40,7 @@ async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink: data.success_url, data.comment_chars, data.currency, + data.fiat_base_multiplier, ), ) if db.type == SQLITE: diff --git a/lnbits/extensions/lnurlp/lnurl.py b/lnbits/extensions/lnurlp/lnurl.py index 173b4823..2ba49e85 100644 --- a/lnbits/extensions/lnurlp/lnurl.py +++ b/lnbits/extensions/lnurlp/lnurl.py @@ -33,7 +33,7 @@ async def api_lnurl_response(request: Request, link_id): resp = LnurlPayResponse( callback=request.url_for("lnurlp.api_lnurl_callback", link_id=link.id), - min_sendable=math.ceil(link.min * rate) * 1000, + min_sendable=round(link.min * rate) * 1000, max_sendable=round(link.max * rate) * 1000, metadata=link.lnurlpay_metadata, ) diff --git a/lnbits/extensions/lnurlp/migrations.py b/lnbits/extensions/lnurlp/migrations.py index 428bde2c..81dd62f8 100644 --- a/lnbits/extensions/lnurlp/migrations.py +++ b/lnbits/extensions/lnurlp/migrations.py @@ -50,3 +50,13 @@ async def m003_min_max_comment_fiat(db): await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN max INTEGER;") await db.execute("UPDATE lnurlp.pay_links SET max = min;") await db.execute("DROP TABLE lnurlp.invoices") + + +async def m004_fiat_base_multiplier(db): + """ + Store the multiplier for fiat prices. We store the price in cents and + remember to multiply by 100 when we use it to convert to Dollars. + """ + await db.execute( + "ALTER TABLE lnurlp.pay_links ADD COLUMN fiat_base_multiplier INTEGER DEFAULT 1;" + ) diff --git a/lnbits/extensions/lnurlp/models.py b/lnbits/extensions/lnurlp/models.py index 6fc9cc1a..4bd438a4 100644 --- a/lnbits/extensions/lnurlp/models.py +++ b/lnbits/extensions/lnurlp/models.py @@ -1,30 +1,33 @@ import json -from urllib.parse import urlparse, urlunparse, parse_qs, urlencode, ParseResult -from starlette.requests import Request -from fastapi.param_functions import Query -from typing import Optional, Dict -from lnbits.lnurl import encode as lnurl_encode # type: ignore -from lnurl.types import LnurlPayMetadata # type: ignore from sqlite3 import Row +from 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 CreatePayLinkData(BaseModel): description: str - min: int = Query(0.01, ge=0.01) - max: int = Query(0.01, ge=0.01) + min: float = Query(1, ge=0.01) + max: float = Query(1, ge=0.01) currency: str = Query(None) comment_chars: int = Query(0, ge=0, lt=800) webhook_url: str = Query(None) success_text: str = Query(None) success_url: str = Query(None) + fiat_base_multiplier: int = Query(100, ge=1) class PayLink(BaseModel): id: int wallet: str description: str - min: int + min: float served_meta: int served_pr: int webhook_url: Optional[str] @@ -32,11 +35,15 @@ class PayLink(BaseModel): success_url: Optional[str] currency: Optional[str] comment_chars: int - max: int + max: float + fiat_base_multiplier: int @classmethod def from_row(cls, row: Row) -> "PayLink": data = dict(row) + if data["currency"] and data["fiat_base_multiplier"]: + data["min"] /= data["fiat_base_multiplier"] + data["max"] /= data["fiat_base_multiplier"] return cls(**data) def lnurl(self, req: Request) -> str: diff --git a/lnbits/extensions/lnurlp/static/js/index.js b/lnbits/extensions/lnurlp/static/js/index.js index e18d6161..1713e77f 100644 --- a/lnbits/extensions/lnurlp/static/js/index.js +++ b/lnbits/extensions/lnurlp/static/js/index.js @@ -35,6 +35,7 @@ new Vue({ rowsPerPage: 10 } }, + nfcTagWriting: false, formDialog: { show: false, fixedAmount: true, @@ -205,6 +206,42 @@ new Vue({ .catch(err => { LNbits.utils.notifyApiError(err) }) + }, + writeNfcTag: async function (lnurl) { + try { + if (typeof NDEFReader == 'undefined') { + throw { + toString: function () { + return 'NFC not supported on this device or browser.' + } + } + } + + const ndef = new NDEFReader() + + this.nfcTagWriting = true + this.$q.notify({ + message: 'Tap your NFC tag to write the LNURL-pay link to it.' + }) + + await ndef.write({ + records: [{recordType: 'url', data: 'lightning:' + lnurl, lang: 'en'}] + }) + + this.nfcTagWriting = false + this.$q.notify({ + type: 'positive', + message: 'NFC tag written successfully.' + }) + } catch (error) { + this.nfcTagWriting = false + this.$q.notify({ + type: 'negative', + message: error + ? error.toString() + : 'An unexpected error has occurred.' + }) + } } }, created() { diff --git a/lnbits/extensions/lnurlp/tasks.py b/lnbits/extensions/lnurlp/tasks.py index b632fa13..525d36ce 100644 --- a/lnbits/extensions/lnurlp/tasks.py +++ b/lnbits/extensions/lnurlp/tasks.py @@ -1,5 +1,6 @@ import asyncio import json + import httpx from lnbits.core import db as core_db @@ -19,7 +20,7 @@ async def wait_for_paid_invoices(): async def on_invoice_paid(payment: Payment) -> None: - if "lnurlp" != payment.extra.get("tag"): + if payment.extra.get("tag") != "lnurlp": # not an lnurlp invoice return diff --git a/lnbits/extensions/lnurlp/templates/lnurlp/_api_docs.html b/lnbits/extensions/lnurlp/templates/lnurlp/_api_docs.html index 80d1478d..abb37e90 100644 --- a/lnbits/extensions/lnurlp/templates/lnurlp/_api_docs.html +++ b/lnbits/extensions/lnurlp/templates/lnurlp/_api_docs.html @@ -4,6 +4,7 @@ label="API info" :content-inset-level="0.5" > + @@ -17,8 +18,8 @@ [<pay_link_object>, ...]
Curl example
curl -X GET {{ request.base_url }}api/v1/links -H "X-Api-Key: {{ - user.wallets[0].inkey }}" + >curl -X GET {{ request.base_url }}lnurlp/api/v1/links -H "X-Api-Key: + {{ user.wallets[0].inkey }}"
@@ -39,8 +40,8 @@ {"lnurl": <string>}
Curl example
curl -X GET {{ request.base_url }}api/v1/links/<pay_id> -H - "X-Api-Key: {{ user.wallets[0].inkey }}" + >curl -X GET {{ request.base_url }}lnurlp/api/v1/links/<pay_id> + -H "X-Api-Key: {{ user.wallets[0].inkey }}" @@ -51,6 +52,7 @@ expand-separator label="Create a pay link" > + POST /lnurlp/api/v1/links @@ -68,11 +70,11 @@ {"lnurl": <string>}
Curl example
curl -X POST {{ request.base_url }}api/v1/links -d '{"description": - <string>, "amount": <integer>, "max": <integer>, - "min": <integer>, "comment_chars": <integer>}' -H - "Content-type: application/json" -H "X-Api-Key: {{ - user.wallets[0].adminkey }}" + >curl -X POST {{ request.base_url }}lnurlp/api/v1/links -d + '{"description": <string>, "amount": <integer>, "max": + <integer>, "min": <integer>, "comment_chars": + <integer>}' -H "Content-type: application/json" -H "X-Api-Key: + {{ user.wallets[0].adminkey }}"
@@ -99,8 +101,8 @@ {"lnurl": <string>}
Curl example
curl -X PUT {{ request.base_url }}api/v1/links/<pay_id> -d - '{"description": <string>, "amount": <integer>}' -H + >curl -X PUT {{ request.base_url }}lnurlp/api/v1/links/<pay_id> + -d '{"description": <string>, "amount": <integer>}' -H "Content-type: application/json" -H "X-Api-Key: {{ user.wallets[0].adminkey }}" @@ -126,8 +128,9 @@
Curl example
curl -X DELETE {{ request.base_url }}api/v1/links/<pay_id> -H - "X-Api-Key: {{ user.wallets[0].adminkey }}" + >curl -X DELETE {{ request.base_url + }}lnurlp/api/v1/links/<pay_id> -H "X-Api-Key: {{ + user.wallets[0].adminkey }}" diff --git a/lnbits/extensions/lnurlp/templates/lnurlp/display.html b/lnbits/extensions/lnurlp/templates/lnurlp/display.html index 08e4de15..944e764b 100644 --- a/lnbits/extensions/lnurlp/templates/lnurlp/display.html +++ b/lnbits/extensions/lnurlp/templates/lnurlp/display.html @@ -14,10 +14,17 @@
-
+
Copy LNURL +
diff --git a/lnbits/extensions/lnurlp/templates/lnurlp/index.html b/lnbits/extensions/lnurlp/templates/lnurlp/index.html index c535f2fb..9677a027 100644 --- a/lnbits/extensions/lnurlp/templates/lnurlp/index.html +++ b/lnbits/extensions/lnurlp/templates/lnurlp/index.html @@ -99,7 +99,8 @@ @click="openUpdateDialog(props.row.id)" icon="edit" color="light-blue" - > + > + + > +
+ > +
@@ -200,7 +203,8 @@ type="number" label="Comment maximum characters" hint="Tell wallets to prompt users for a comment that will be sent along with the payment. LNURLp will store the comment and send it in the webhook." - > + > + + > +
Shareable link + + data.max: raise HTTPException( @@ -87,12 +84,18 @@ async def api_link_create_or_update( ) if data.currency == None and ( - round(data.min) != data.min or round(data.max) != data.max + round(data.min) != data.min or round(data.max) != data.max or data.min < 1 ): raise HTTPException( detail="Must use full satoshis.", status_code=HTTPStatus.BAD_REQUEST ) + # database only allows int4 entries for min and max. For fiat currencies, + # we multiply by data.fiat_base_multiplier (usually 100) to save the value in cents. + if data.currency and data.fiat_base_multiplier: + data.min *= data.fiat_base_multiplier + data.max *= data.fiat_base_multiplier + if "success_url" in data and data.success_url[:8] != "https://": raise HTTPException( detail="Success URL must be secure https://...", @@ -115,7 +118,7 @@ async def api_link_create_or_update( link = await update_pay_link(**data.dict(), link_id=link_id) else: link = await create_pay_link(data, wallet_id=wallet.wallet.id) - return {**link.dict(), "lnurl": link.lnurl} + return {**link.dict(), "lnurl": link.lnurl(request)} @lnurlp_ext.delete("/api/v1/links/{link_id}") diff --git a/lnbits/extensions/lnurlpayout/__init__.py b/lnbits/extensions/lnurlpayout/__init__.py index 1626e2e5..9962290c 100644 --- a/lnbits/extensions/lnurlpayout/__init__.py +++ b/lnbits/extensions/lnurlpayout/__init__.py @@ -1,4 +1,5 @@ import asyncio + from fastapi import APIRouter from lnbits.db import Database diff --git a/lnbits/extensions/lnurlpayout/config.json.example b/lnbits/extensions/lnurlpayout/config.json.example index 1e72c0c1..b4160d7b 100644 --- a/lnbits/extensions/lnurlpayout/config.json.example +++ b/lnbits/extensions/lnurlpayout/config.json.example @@ -2,5 +2,5 @@ "name": "LNURLPayout", "short_description": "Autodump wallet funds to LNURLpay", "icon": "exit_to_app", - "contributors": ["arcbtc"] + "contributors": ["arcbtc","talvasconcelos"] } diff --git a/lnbits/extensions/lnurlpayout/crud.py b/lnbits/extensions/lnurlpayout/crud.py index 6cbf6c54..0f9f98ac 100644 --- a/lnbits/extensions/lnurlpayout/crud.py +++ b/lnbits/extensions/lnurlpayout/crud.py @@ -3,7 +3,7 @@ from typing import List, Optional, Union from lnbits.helpers import urlsafe_short_hash from . import db -from .models import lnurlpayout, CreateLnurlPayoutData +from .models import CreateLnurlPayoutData, lnurlpayout async def create_lnurlpayout( diff --git a/lnbits/extensions/lnurlpayout/tasks.py b/lnbits/extensions/lnurlpayout/tasks.py index 7f2a8324..b621876c 100644 --- a/lnbits/extensions/lnurlpayout/tasks.py +++ b/lnbits/extensions/lnurlpayout/tasks.py @@ -2,6 +2,7 @@ import asyncio from http import HTTPStatus import httpx +from loguru import logger from starlette.exceptions import HTTPException from lnbits.core import db as core_db @@ -27,7 +28,7 @@ async def on_invoice_paid(payment: Payment) -> None: try: # Check its got a payout associated with it lnurlpayout_link = await get_lnurlpayout_from_wallet(payment.wallet_id) - print("LNURLpayout", lnurlpayout_link) + logger.debug("LNURLpayout", lnurlpayout_link) if lnurlpayout_link: # Check the wallet balance is more than the threshold diff --git a/lnbits/extensions/lnurlpayout/templates/lnurlpayout/_api_docs.html b/lnbits/extensions/lnurlpayout/templates/lnurlpayout/_api_docs.html index 7febea44..4f921bb5 100644 --- a/lnbits/extensions/lnurlpayout/templates/lnurlpayout/_api_docs.html +++ b/lnbits/extensions/lnurlpayout/templates/lnurlpayout/_api_docs.html @@ -4,6 +4,12 @@ label="API info" :content-inset-level="0.5" > + @@ -32,6 +38,7 @@ expand-separator label="Create a lnurlpayout" > + int: diff --git a/lnbits/extensions/offlineshop/helpers.py b/lnbits/extensions/offlineshop/helpers.py index db2c19cc..86a653aa 100644 --- a/lnbits/extensions/offlineshop/helpers.py +++ b/lnbits/extensions/offlineshop/helpers.py @@ -1,6 +1,6 @@ import base64 -import struct import hmac +import struct import time @@ -8,8 +8,8 @@ def hotp(key, counter, digits=6, digest="sha1"): key = base64.b32decode(key.upper() + "=" * ((8 - len(key)) % 8)) counter = struct.pack(">Q", counter) mac = hmac.new(key, counter, digest).digest() - offset = mac[-1] & 0x0f - binary = struct.unpack(">L", mac[offset : offset + 4])[0] & 0x7fffffff + offset = mac[-1] & 0x0F + binary = struct.unpack(">L", mac[offset : offset + 4])[0] & 0x7FFFFFFF return str(binary)[-digits:].zfill(digits) diff --git a/lnbits/extensions/offlineshop/models.py b/lnbits/extensions/offlineshop/models.py index 06225351..0128fdb8 100644 --- a/lnbits/extensions/offlineshop/models.py +++ b/lnbits/extensions/offlineshop/models.py @@ -1,14 +1,15 @@ -import json import base64 import hashlib +import json from collections import OrderedDict +from typing import Dict, List, Optional -from typing import Optional, List, Dict from lnurl import encode as lnurl_encode # type: ignore -from lnurl.types import LnurlPayMetadata # type: ignore from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore +from lnurl.types import LnurlPayMetadata # type: ignore from pydantic import BaseModel from starlette.requests import Request + from .helpers import totp shop_counters: Dict = {} diff --git a/lnbits/extensions/offlineshop/templates/offlineshop/_api_docs.html b/lnbits/extensions/offlineshop/templates/offlineshop/_api_docs.html index ac655697..0a4b9df8 100644 --- a/lnbits/extensions/offlineshop/templates/offlineshop/_api_docs.html +++ b/lnbits/extensions/offlineshop/templates/offlineshop/_api_docs.html @@ -47,6 +47,7 @@ label="API info" :content-inset-level="0.5" > + Curl example curl -X GET {{ request.base_url - }}/offlineshop/api/v1/offlineshop/items -H "Content-Type: + }}offlineshop/api/v1/offlineshop/items -H "Content-Type: application/json" -H "X-Api-Key: {{ user.wallets[0].inkey }}" -d '{"name": <string>, "description": <string>, "image": <data-uri string>, "price": <integer>, "unit": <"sat" @@ -96,7 +97,7 @@ >
Curl example
curl -X GET {{ request.base_url }}/offlineshop/api/v1/offlineshop -H + >curl -X GET {{ request.base_url }}offlineshop/api/v1/offlineshop -H "X-Api-Key: {{ user.wallets[0].inkey }}"
@@ -118,7 +119,7 @@
Curl example
curl -X GET {{ request.base_url - }}/offlineshop/api/v1/offlineshop/items/<item_id> -H + }}offlineshop/api/v1/offlineshop/items/<item_id> -H "Content-Type: application/json" -H "X-Api-Key: {{ user.wallets[0].inkey }}" -d '{"name": <string>, "description": <string>, "image": <data-uri string>, "price": @@ -127,7 +128,13 @@
- + DELETE @@ -138,7 +145,7 @@
Curl example
curl -X GET {{ request.base_url - }}/offlineshop/api/v1/offlineshop/items/<item_id> -H "X-Api-Key: + }}offlineshop/api/v1/offlineshop/items/<item_id> -H "X-Api-Key: {{ user.wallets[0].inkey }}"
diff --git a/lnbits/extensions/offlineshop/views.py b/lnbits/extensions/offlineshop/views.py index e1d3a66e..34bb7a03 100644 --- a/lnbits/extensions/offlineshop/views.py +++ b/lnbits/extensions/offlineshop/views.py @@ -3,18 +3,18 @@ from datetime import datetime from http import HTTPStatus from typing import List +from fastapi import HTTPException, Request from fastapi.params import Depends, Query from starlette.responses import HTMLResponse -from lnbits.decorators import check_user_exists -from lnbits.core.models import Payment, User from lnbits.core.crud import get_standalone_payment +from lnbits.core.models import Payment, User from lnbits.core.views.api import api_payment +from lnbits.decorators import check_user_exists from . import offlineshop_ext, offlineshop_renderer -from .models import Item from .crud import get_item, get_shop -from fastapi import Request, HTTPException +from .models import Item @offlineshop_ext.get("/", response_class=HTMLResponse) diff --git a/lnbits/extensions/paywall/templates/paywall/_api_docs.html b/lnbits/extensions/paywall/templates/paywall/_api_docs.html index ceadf2f0..0fd8bdd3 100644 --- a/lnbits/extensions/paywall/templates/paywall/_api_docs.html +++ b/lnbits/extensions/paywall/templates/paywall/_api_docs.html @@ -4,6 +4,7 @@ label="API info" :content-inset-level="0.5" > + @@ -17,8 +18,8 @@ [<paywall_object>, ...]
Curl example
curl -X GET {{ request.url_root }}api/v1/paywalls -H "X-Api-Key: {{ - user.wallets[0].inkey }}" + >curl -X GET {{ request.base_url }}paywall/api/v1/paywalls -H + "X-Api-Key: {{ user.wallets[0].inkey }}"
@@ -48,11 +49,11 @@ >
Curl example
curl -X POST {{ request.url_root }}api/v1/paywalls -d '{"url": - <string>, "memo": <string>, "description": <string>, - "amount": <integer>, "remembers": <boolean>}' -H - "Content-type: application/json" -H "X-Api-Key: {{ - user.wallets[0].adminkey }}" + >curl -X POST {{ request.base_url }}paywall/api/v1/paywalls -d + '{"url": <string>, "memo": <string>, "description": + <string>, "amount": <integer>, "remembers": + <boolean>}' -H "Content-type: application/json" -H "X-Api-Key: + {{ user.wallets[0].adminkey }}"
@@ -80,8 +81,8 @@ >
Curl example
curl -X POST {{ request.url_root - }}api/v1/paywalls/<paywall_id>/invoice -d '{"amount": + >curl -X POST {{ request.base_url + }}paywall/api/v1/paywalls/<paywall_id>/invoice -d '{"amount": <integer>}' -H "Content-type: application/json" @@ -111,8 +112,8 @@ >
Curl example
curl -X POST {{ request.url_root - }}api/v1/paywalls/<paywall_id>/check_invoice -d + >curl -X POST {{ request.base_url + }}paywall/api/v1/paywalls/<paywall_id>/check_invoice -d '{"payment_hash": <string>}' -H "Content-type: application/json" @@ -137,8 +138,8 @@
Curl example
curl -X DELETE {{ request.url_root - }}api/v1/paywalls/<paywall_id> -H "X-Api-Key: {{ + >curl -X DELETE {{ request.base_url + }}paywall/api/v1/paywalls/<paywall_id> -H "X-Api-Key: {{ user.wallets[0].adminkey }}" diff --git a/lnbits/extensions/paywall/views_api.py b/lnbits/extensions/paywall/views_api.py index 3d1c2575..8052c63b 100644 --- a/lnbits/extensions/paywall/views_api.py +++ b/lnbits/extensions/paywall/views_api.py @@ -54,8 +54,7 @@ async def api_paywall_delete( @paywall_ext.post("/api/v1/paywalls/invoice/{paywall_id}") async def api_paywall_create_invoice( - data: CreatePaywallInvoice, - paywall_id: str = Query(None) + data: CreatePaywallInvoice, paywall_id: str = Query(None) ): paywall = await get_paywall(paywall_id) if data.amount < paywall.amount: @@ -78,7 +77,9 @@ async def api_paywall_create_invoice( @paywall_ext.post("/api/v1/paywalls/check_invoice/{paywall_id}") -async def api_paywal_check_invoice(data: CheckPaywallInvoice, paywall_id: str = Query(None)): +async def api_paywal_check_invoice( + data: CheckPaywallInvoice, paywall_id: str = Query(None) +): paywall = await get_paywall(paywall_id) payment_hash = data.payment_hash if not paywall: diff --git a/lnbits/extensions/satsdice/templates/satsdice/_api_docs.html b/lnbits/extensions/satsdice/templates/satsdice/_api_docs.html index fb43b90d..e85e9586 100644 --- a/lnbits/extensions/satsdice/templates/satsdice/_api_docs.html +++ b/lnbits/extensions/satsdice/templates/satsdice/_api_docs.html @@ -4,6 +4,7 @@ label="API info" :content-inset-level="0.5" > + @@ -17,8 +18,8 @@ [<satsdice_link_object>, ...]
Curl example
curl -X GET {{ request.base_url }}api/v1/links -H "X-Api-Key: {{ - user.wallets[0].inkey }}" + >curl -X GET {{ request.base_url }}satsdice/api/v1/links -H + "X-Api-Key: {{ user.wallets[0].inkey }}"
@@ -44,8 +45,9 @@ {"lnurl": <string>}
Curl example
curl -X GET {{ request.base_url }}api/v1/links/<satsdice_id> -H - "X-Api-Key: {{ user.wallets[0].inkey }}" + >curl -X GET {{ request.base_url + }}satsdice/api/v1/links/<satsdice_id> -H "X-Api-Key: {{ + user.wallets[0].inkey }}" @@ -73,8 +75,8 @@ {"lnurl": <string>}
Curl example
curl -X POST {{ request.base_url }}api/v1/links -d '{"title": - <string>, "min_satsdiceable": <integer>, + >curl -X POST {{ request.base_url }}satsdice/api/v1/links -d + '{"title": <string>, "min_satsdiceable": <integer>, "max_satsdiceable": <integer>, "uses": <integer>, "wait_time": <integer>, "is_unique": <boolean>}' -H "Content-type: application/json" -H "X-Api-Key: {{ @@ -109,8 +111,9 @@ {"lnurl": <string>}
Curl example
curl -X PUT {{ request.base_url }}api/v1/links/<satsdice_id> -d - '{"title": <string>, "min_satsdiceable": <integer>, + >curl -X PUT {{ request.base_url + }}satsdice/api/v1/links/<satsdice_id> -d '{"title": + <string>, "min_satsdiceable": <integer>, "max_satsdiceable": <integer>, "uses": <integer>, "wait_time": <integer>, "is_unique": <boolean>}' -H "Content-type: application/json" -H "X-Api-Key: {{ @@ -137,8 +140,9 @@
Curl example
curl -X DELETE {{ request.base_url }}api/v1/links/<satsdice_id> - -H "X-Api-Key: {{ user.wallets[0].adminkey }}" + >curl -X DELETE {{ request.base_url + }}satsdice/api/v1/links/<satsdice_id> -H "X-Api-Key: {{ + user.wallets[0].adminkey }}" @@ -165,8 +169,8 @@
Curl example
curl -X GET {{ request.base_url - }}api/v1/links/<the_hash>/<lnurl_id> -H "X-Api-Key: {{ - user.wallets[0].inkey }}" + }}satsdice/api/v1/links/<the_hash>/<lnurl_id> -H + "X-Api-Key: {{ user.wallets[0].inkey }}" @@ -186,7 +190,7 @@ >
Curl example
curl -X GET {{ request.base_url }}/satsdice/img/<lnurl_id>" + >curl -X GET {{ request.base_url }}satsdice/img/<lnurl_id>" diff --git a/lnbits/extensions/satspay/tasks.py b/lnbits/extensions/satspay/tasks.py index 87f74b7d..d325405b 100644 --- a/lnbits/extensions/satspay/tasks.py +++ b/lnbits/extensions/satspay/tasks.py @@ -1,5 +1,7 @@ import asyncio +from loguru import logger + from lnbits.core.models import Payment from lnbits.extensions.satspay.crud import check_address_balance, get_charge from lnbits.tasks import register_invoice_listener @@ -17,13 +19,13 @@ async def wait_for_paid_invoices(): async def on_invoice_paid(payment: Payment) -> None: - if "charge" != payment.extra.get("tag"): + if payment.extra.get("tag") != "charge": # not a charge invoice return charge = await get_charge(payment.memo) if not charge: - print("this should never happen", payment) + logger.error("this should never happen", payment) return await payment.set_pending(False) diff --git a/lnbits/extensions/satspay/templates/satspay/_api_docs.html b/lnbits/extensions/satspay/templates/satspay/_api_docs.html index d834db20..336ab899 100644 --- a/lnbits/extensions/satspay/templates/satspay/_api_docs.html +++ b/lnbits/extensions/satspay/templates/satspay/_api_docs.html @@ -15,6 +15,7 @@ label="API info" :content-inset-level="0.5" > + @@ -32,7 +33,7 @@ [<charge_object>, ...]
Curl example
curl -X POST {{ request.base_url }}api/v1/charge -d + >curl -X POST {{ request.base_url }}satspay/api/v1/charge -d '{"onchainwallet": <string, watchonly_wallet_id>, "description": <string>, "webhook":<string>, "time": <integer>, "amount": <integer>, "lnbitswallet": @@ -60,12 +61,13 @@ [<charge_object>, ...]
Curl example
curl -X POST {{ request.base_url }}api/v1/charge/<charge_id> - -d '{"onchainwallet": <string, watchonly_wallet_id>, - "description": <string>, "webhook":<string>, "time": - <integer>, "amount": <integer>, "lnbitswallet": - <string, lnbits_wallet_id>}' -H "Content-type: - application/json" -H "X-Api-Key: {{user.wallets[0].adminkey }}" + >curl -X POST {{ request.base_url + }}satspay/api/v1/charge/<charge_id> -d '{"onchainwallet": + <string, watchonly_wallet_id>, "description": <string>, + "webhook":<string>, "time": <integer>, "amount": + <integer>, "lnbitswallet": <string, lnbits_wallet_id>}' + -H "Content-type: application/json" -H "X-Api-Key: + {{user.wallets[0].adminkey }}"
@@ -89,8 +91,9 @@ [<charge_object>, ...]
Curl example
curl -X GET {{ request.base_url }}api/v1/charge/<charge_id> - -H "X-Api-Key: {{ user.wallets[0].inkey }}" + >curl -X GET {{ request.base_url + }}satspay/api/v1/charge/<charge_id> -H "X-Api-Key: {{ + user.wallets[0].inkey }}" @@ -112,8 +115,8 @@ [<charge_object>, ...]
Curl example
curl -X GET {{ request.base_url }}api/v1/charges -H "X-Api-Key: {{ - user.wallets[0].inkey }}" + >curl -X GET {{ request.base_url }}satspay/api/v1/charges -H + "X-Api-Key: {{ user.wallets[0].inkey }}" @@ -123,7 +126,6 @@ dense expand-separator label="Delete a pay link" - class="q-pb-md" > @@ -138,13 +140,19 @@
Curl example
curl -X DELETE {{ request.base_url - }}api/v1/charge/<charge_id> -H "X-Api-Key: {{ + }}satspay/api/v1/charge/<charge_id> -H "X-Api-Key: {{ user.wallets[0].adminkey }}"
- + Curl example curl -X GET {{ request.base_url - }}api/v1/charges/balance/<charge_id> -H "X-Api-Key: {{ + }}satspay/api/v1/charges/balance/<charge_id> -H "X-Api-Key: {{ user.wallets[0].inkey }}" diff --git a/lnbits/extensions/splitpayments/__init__.py b/lnbits/extensions/splitpayments/__init__.py index df6feb94..9989728e 100644 --- a/lnbits/extensions/splitpayments/__init__.py +++ b/lnbits/extensions/splitpayments/__init__.py @@ -12,7 +12,7 @@ db = Database("ext_splitpayments") splitpayments_static_files = [ { "path": "/splitpayments/static", - "app": StaticFiles(directory="lnbits/extensions/splitpayments/static"), + "app": StaticFiles(packages=[("lnbits", "extensions/splitpayments/static")]), "name": "splitpayments_static", } ] diff --git a/lnbits/extensions/splitpayments/migrations.py b/lnbits/extensions/splitpayments/migrations.py index 735afc6c..b3921c42 100644 --- a/lnbits/extensions/splitpayments/migrations.py +++ b/lnbits/extensions/splitpayments/migrations.py @@ -14,3 +14,41 @@ async def m001_initial(db): ); """ ) + + +async def m002_float_percent(db): + """ + Add float percent and migrates the existing data. + """ + await db.execute("ALTER TABLE splitpayments.targets RENAME TO splitpayments_old") + await db.execute( + """ + CREATE TABLE splitpayments.targets ( + wallet TEXT NOT NULL, + source TEXT NOT NULL, + percent REAL NOT NULL CHECK (percent >= 0 AND percent <= 100), + alias TEXT, + + UNIQUE (source, wallet) + ); + """ + ) + + for row in [ + list(row) + for row in await db.fetchall("SELECT * FROM splitpayments.splitpayments_old") + ]: + await db.execute( + """ + INSERT INTO splitpayments.targets ( + wallet, + source, + percent, + alias + ) + VALUES (?, ?, ?, ?) + """, + (row[0], row[1], row[2], row[3]), + ) + + await db.execute("DROP TABLE splitpayments.splitpayments_old") diff --git a/lnbits/extensions/splitpayments/models.py b/lnbits/extensions/splitpayments/models.py index 3264bca7..4b95ed18 100644 --- a/lnbits/extensions/splitpayments/models.py +++ b/lnbits/extensions/splitpayments/models.py @@ -7,14 +7,14 @@ from pydantic import BaseModel class Target(BaseModel): wallet: str source: str - percent: int + percent: float alias: Optional[str] class TargetPutList(BaseModel): wallet: str = Query(...) alias: str = Query("") - percent: int = Query(..., ge=1) + percent: float = Query(..., ge=0.01) class TargetPut(BaseModel): diff --git a/lnbits/extensions/splitpayments/static/js/index.js b/lnbits/extensions/splitpayments/static/js/index.js index dea469e5..5d326231 100644 --- a/lnbits/extensions/splitpayments/static/js/index.js +++ b/lnbits/extensions/splitpayments/static/js/index.js @@ -105,7 +105,7 @@ new Vue({ if (currentTotal > 100 && isPercent) { let diff = (currentTotal - 100) / (100 - this.targets[index].percent) this.targets.forEach((target, t) => { - if (t !== index) target.percent -= Math.round(diff * target.percent) + if (t !== index) target.percent -= +(diff * target.percent).toFixed(2) }) } @@ -119,7 +119,7 @@ new Vue({ '/splitpayments/api/v1/targets', this.selectedWallet.adminkey, { - "targets": this.targets + targets: this.targets .filter(isTargetComplete) .map(({wallet, percent, alias}) => ({wallet, percent, alias})) } diff --git a/lnbits/extensions/splitpayments/tasks.py b/lnbits/extensions/splitpayments/tasks.py index c54c067f..0948e849 100644 --- a/lnbits/extensions/splitpayments/tasks.py +++ b/lnbits/extensions/splitpayments/tasks.py @@ -1,6 +1,8 @@ import asyncio import json +from loguru import logger + from lnbits.core import db as core_db from lnbits.core.crud import create_payment from lnbits.core.models import Payment @@ -20,7 +22,7 @@ async def wait_for_paid_invoices(): async def on_invoice_paid(payment: Payment) -> None: - if "splitpayments" == payment.extra.get("tag") or payment.extra.get("splitted"): + if payment.extra.get("tag") == "splitpayments" or payment.extra.get("splitted"): # already splitted, ignore return @@ -34,7 +36,9 @@ async def on_invoice_paid(payment: Payment) -> None: amount_left = payment.amount - sum([amount for _, amount in transfers]) if amount_left < 0: - print("splitpayments failure: amount_left is negative.", payment.payment_hash) + logger.error( + "splitpayments failure: amount_left is negative.", payment.payment_hash + ) return if not targets: diff --git a/lnbits/extensions/splitpayments/templates/splitpayments/_api_docs.html b/lnbits/extensions/splitpayments/templates/splitpayments/_api_docs.html index 78b5362c..4b5ed979 100644 --- a/lnbits/extensions/splitpayments/templates/splitpayments/_api_docs.html +++ b/lnbits/extensions/splitpayments/templates/splitpayments/_api_docs.html @@ -28,6 +28,12 @@ label="API info" :content-inset-level="0.5" > +
Curl example
curl -X GET {{ request.base_url }}api/v1/livestream -H "X-Api-Key: {{ - user.wallets[0].inkey }}" + >curl -X GET {{ request.base_url }}splitpayments/api/v1/targets -H + "X-Api-Key: {{ user.wallets[0].inkey }}"
@@ -63,6 +69,7 @@ dense expand-separator label="Set Target Wallets" + class="q-pb-md" > @@ -78,7 +85,7 @@
Curl example
curl -X PUT {{ request.base_url }}api/v1/splitpayments/targets -H + >curl -X PUT {{ request.base_url }}splitpayments/api/v1/targets -H "X-Api-Key: {{ user.wallets[0].adminkey }}" -H 'Content-Type: application/json' -d '{"targets": [{"wallet": <wallet id or invoice key>, "alias": <name to identify this>, "percent": <number diff --git a/lnbits/extensions/splitpayments/templates/splitpayments/index.html b/lnbits/extensions/splitpayments/templates/splitpayments/index.html index 1aae4e33..5862abc1 100644 --- a/lnbits/extensions/splitpayments/templates/splitpayments/index.html +++ b/lnbits/extensions/splitpayments/templates/splitpayments/index.html @@ -58,14 +58,14 @@ >
- - +
+
Clear - +
- +
Save Targets - - +
+
diff --git a/lnbits/extensions/streamalerts/templates/streamalerts/_api_docs.html b/lnbits/extensions/streamalerts/templates/streamalerts/_api_docs.html index 33b52f15..346ab4ec 100644 --- a/lnbits/extensions/streamalerts/templates/streamalerts/_api_docs.html +++ b/lnbits/extensions/streamalerts/templates/streamalerts/_api_docs.html @@ -15,4 +15,5 @@ >

+ diff --git a/lnbits/extensions/streamalerts/views_api.py b/lnbits/extensions/streamalerts/views_api.py index 0a678d8b..bb2998ee 100644 --- a/lnbits/extensions/streamalerts/views_api.py +++ b/lnbits/extensions/streamalerts/views_api.py @@ -123,7 +123,7 @@ async def api_create_donation(data: CreateDonation, request: Request): completelinktext="Back to Stream!", webhook=webhook_base + "/streamalerts/api/v1/postdonation", description=description, - **charge_details + **charge_details, ) charge = await create_charge(user=charge_details["user"], data=create_charge_data) await create_donation( diff --git a/lnbits/extensions/subdomains/cloudflare.py b/lnbits/extensions/subdomains/cloudflare.py index 8ada2a90..679ca843 100644 --- a/lnbits/extensions/subdomains/cloudflare.py +++ b/lnbits/extensions/subdomains/cloudflare.py @@ -1,5 +1,8 @@ +import json + +import httpx + from lnbits.extensions.subdomains.models import Domains -import httpx, json async def cloudflare_create_subdomain( diff --git a/lnbits/extensions/subdomains/tasks.py b/lnbits/extensions/subdomains/tasks.py index 75223e82..d8f35161 100644 --- a/lnbits/extensions/subdomains/tasks.py +++ b/lnbits/extensions/subdomains/tasks.py @@ -19,7 +19,7 @@ async def wait_for_paid_invoices(): async def on_invoice_paid(payment: Payment) -> None: - if "lnsubdomain" != payment.extra.get("tag"): + if payment.extra.get("tag") != "lnsubdomain": # not an lnurlp invoice return diff --git a/lnbits/extensions/subdomains/templates/subdomains/_api_docs.html b/lnbits/extensions/subdomains/templates/subdomains/_api_docs.html index b839c641..db3b2477 100644 --- a/lnbits/extensions/subdomains/templates/subdomains/_api_docs.html +++ b/lnbits/extensions/subdomains/templates/subdomains/_api_docs.html @@ -22,5 +22,6 @@ >

+ diff --git a/lnbits/extensions/tipjar/templates/tipjar/_api_docs.html b/lnbits/extensions/tipjar/templates/tipjar/_api_docs.html index 42788bad..cfb8136b 100644 --- a/lnbits/extensions/tipjar/templates/tipjar/_api_docs.html +++ b/lnbits/extensions/tipjar/templates/tipjar/_api_docs.html @@ -4,13 +4,13 @@ Tip Jar: Receive tips with messages!

- Your personal Bitcoin tip page, which supports - lightning and on-chain payments. - Notifications, including a donation message, - can be sent via webhook. + Your personal Bitcoin tip page, which supports lightning and on-chain + payments. Notifications, including a donation message, can be sent via + webhook. Created by, Fitti

+ diff --git a/lnbits/extensions/tipjar/templates/tipjar/index.html b/lnbits/extensions/tipjar/templates/tipjar/index.html index dda49842..19fca6e4 100644 --- a/lnbits/extensions/tipjar/templates/tipjar/index.html +++ b/lnbits/extensions/tipjar/templates/tipjar/index.html @@ -322,11 +322,7 @@ var self = this LNbits.api - .request( - 'GET', - '/tipjar/api/v1/tips', - this.g.user.wallets[0].inkey - ) + .request('GET', '/tipjar/api/v1/tips', this.g.user.wallets[0].inkey) .then(function (response) { self.tips = response.data.map(function (obj) { return mapTipJar(obj) diff --git a/lnbits/extensions/tpos/__init__.py b/lnbits/extensions/tpos/__init__.py index c62981d7..3ce618aa 100644 --- a/lnbits/extensions/tpos/__init__.py +++ b/lnbits/extensions/tpos/__init__.py @@ -1,7 +1,10 @@ +import asyncio + from fastapi import APIRouter from lnbits.db import Database from lnbits.helpers import template_renderer +from lnbits.tasks import catch_everything_and_restart db = Database("ext_tpos") @@ -12,5 +15,11 @@ def tpos_renderer(): return template_renderer(["lnbits/extensions/tpos/templates"]) -from .views_api import * # noqa +from .tasks import wait_for_paid_invoices from .views import * # noqa +from .views_api import * # noqa + + +def tpos_start(): + loop = asyncio.get_event_loop() + loop.create_task(catch_everything_and_restart(wait_for_paid_invoices)) diff --git a/lnbits/extensions/tpos/config.json b/lnbits/extensions/tpos/config.json index c5789afb..3bd1a71a 100644 --- a/lnbits/extensions/tpos/config.json +++ b/lnbits/extensions/tpos/config.json @@ -2,5 +2,5 @@ "name": "TPoS", "short_description": "A shareable PoS terminal!", "icon": "dialpad", - "contributors": ["talvasconcelos", "arcbtc"] + "contributors": ["talvasconcelos", "arcbtc", "leesalminen"] } diff --git a/lnbits/extensions/tpos/crud.py b/lnbits/extensions/tpos/crud.py index 1a198769..94e2c006 100644 --- a/lnbits/extensions/tpos/crud.py +++ b/lnbits/extensions/tpos/crud.py @@ -10,10 +10,17 @@ async def create_tpos(wallet_id: str, data: CreateTposData) -> TPoS: tpos_id = urlsafe_short_hash() await db.execute( """ - INSERT INTO tpos.tposs (id, wallet, name, currency) - VALUES (?, ?, ?, ?) + INSERT INTO tpos.tposs (id, wallet, name, currency, tip_options, tip_wallet) + VALUES (?, ?, ?, ?, ?, ?) """, - (tpos_id, wallet_id, data.name, data.currency), + ( + tpos_id, + wallet_id, + data.name, + data.currency, + data.tip_options, + data.tip_wallet, + ), ) tpos = await get_tpos(tpos_id) @@ -23,7 +30,7 @@ async def create_tpos(wallet_id: str, data: CreateTposData) -> TPoS: async def get_tpos(tpos_id: str) -> Optional[TPoS]: row = await db.fetchone("SELECT * FROM tpos.tposs WHERE id = ?", (tpos_id,)) - return TPoS.from_row(row) if row else None + return TPoS(**row) if row else None async def get_tposs(wallet_ids: Union[str, List[str]]) -> List[TPoS]: @@ -35,7 +42,7 @@ async def get_tposs(wallet_ids: Union[str, List[str]]) -> List[TPoS]: f"SELECT * FROM tpos.tposs WHERE wallet IN ({q})", (*wallet_ids,) ) - return [TPoS.from_row(row) for row in rows] + return [TPoS(**row) for row in rows] async def delete_tpos(tpos_id: str) -> None: diff --git a/lnbits/extensions/tpos/migrations.py b/lnbits/extensions/tpos/migrations.py index 7a7fff0d..565c05ab 100644 --- a/lnbits/extensions/tpos/migrations.py +++ b/lnbits/extensions/tpos/migrations.py @@ -12,3 +12,25 @@ async def m001_initial(db): ); """ ) + + +async def m002_addtip_wallet(db): + """ + Add tips to tposs table + """ + await db.execute( + """ + ALTER TABLE tpos.tposs ADD tip_wallet TEXT NULL; + """ + ) + + +async def m003_addtip_options(db): + """ + Add tips to tposs table + """ + await db.execute( + """ + ALTER TABLE tpos.tposs ADD tip_options TEXT NULL; + """ + ) diff --git a/lnbits/extensions/tpos/models.py b/lnbits/extensions/tpos/models.py index 653a055c..36bca79b 100644 --- a/lnbits/extensions/tpos/models.py +++ b/lnbits/extensions/tpos/models.py @@ -1,11 +1,15 @@ from sqlite3 import Row +from typing import Optional +from fastapi import Query from pydantic import BaseModel class CreateTposData(BaseModel): name: str currency: str + tip_options: str = Query(None) + tip_wallet: str = Query(None) class TPoS(BaseModel): @@ -13,6 +17,8 @@ class TPoS(BaseModel): wallet: str name: str currency: str + tip_options: Optional[str] + tip_wallet: Optional[str] @classmethod def from_row(cls, row: Row) -> "TPoS": diff --git a/lnbits/extensions/tpos/tasks.py b/lnbits/extensions/tpos/tasks.py new file mode 100644 index 00000000..af9663cc --- /dev/null +++ b/lnbits/extensions/tpos/tasks.py @@ -0,0 +1,70 @@ +import asyncio +import json + +from lnbits.core import db as core_db +from lnbits.core.crud import create_payment +from lnbits.core.models import Payment +from lnbits.helpers import urlsafe_short_hash +from lnbits.tasks import internal_invoice_queue, register_invoice_listener + +from .crud import get_tpos + + +async def wait_for_paid_invoices(): + invoice_queue = asyncio.Queue() + register_invoice_listener(invoice_queue) + + while True: + payment = await invoice_queue.get() + await on_invoice_paid(payment) + + +async def on_invoice_paid(payment: Payment) -> None: + if payment.extra.get("tag") == "tpos" and payment.extra.get("tipSplitted"): + # already splitted, ignore + return + + # now we make some special internal transfers (from no one to the receiver) + tpos = await get_tpos(payment.extra.get("tposId")) + tipAmount = payment.extra.get("tipAmount") + + if tipAmount is None: + # no tip amount + return + + tipAmount = tipAmount * 1000 + amount = payment.amount - tipAmount + + # mark the original payment with one extra key, "splitted" + # (this prevents us from doing this process again and it's informative) + # and reduce it by the amount we're going to send to the producer + await core_db.execute( + """ + UPDATE apipayments + SET extra = ?, amount = ? + WHERE hash = ? + AND checking_id NOT LIKE 'internal_%' + """, + ( + json.dumps(dict(**payment.extra, tipSplitted=True)), + amount, + payment.payment_hash, + ), + ) + + # perform the internal transfer using the same payment_hash + internal_checking_id = f"internal_{urlsafe_short_hash()}" + await create_payment( + wallet_id=tpos.tip_wallet, + checking_id=internal_checking_id, + payment_request="", + payment_hash=payment.payment_hash, + amount=tipAmount, + memo=f"Tip for {payment.memo}", + pending=False, + extra={"tipSplitted": True}, + ) + + # manually send this for now + await internal_invoice_queue.put(internal_checking_id) + return diff --git a/lnbits/extensions/tpos/templates/tpos/_api_docs.html b/lnbits/extensions/tpos/templates/tpos/_api_docs.html index 7897383d..cbb21be1 100644 --- a/lnbits/extensions/tpos/templates/tpos/_api_docs.html +++ b/lnbits/extensions/tpos/templates/tpos/_api_docs.html @@ -4,6 +4,7 @@ label="API info" :content-inset-level="0.5" > + @@ -17,7 +18,7 @@ [<tpos_object>, ...]
Curl example
curl -X GET {{ request.base_url }}api/v1/tposs -H "X-Api-Key: + >curl -X GET {{ request.base_url }}tpos/api/v1/tposs -H "X-Api-Key: <invoice_key>"
@@ -42,7 +43,7 @@ >
Curl example
curl -X POST {{ request.base_url }}api/v1/tposs -d '{"name": + >curl -X POST {{ request.base_url }}tpos/api/v1/tposs -d '{"name": <string>, "currency": <string>}' -H "Content-type: application/json" -H "X-Api-Key: <admin_key>" @@ -69,8 +70,8 @@
Curl example
curl -X DELETE {{ request.base_url }}api/v1/tposs/<tpos_id> -H - "X-Api-Key: <admin_key>" + >curl -X DELETE {{ request.base_url + }}tpos/api/v1/tposs/<tpos_id> -H "X-Api-Key: <admin_key>"
diff --git a/lnbits/extensions/tpos/templates/tpos/index.html b/lnbits/extensions/tpos/templates/tpos/index.html index a8971211..edbb2aa8 100644 --- a/lnbits/extensions/tpos/templates/tpos/index.html +++ b/lnbits/extensions/tpos/templates/tpos/index.html @@ -54,7 +54,8 @@ > - {{ col.value }} + {{ (col.name == 'tip_options' && col.value ? + JSON.parse(col.value).join(", ") : col.value) }} + +
parseInt(str)) + ) + : JSON.stringify([]), + tip_wallet: this.formDialog.data.tip_wallet || '' } var self = this diff --git a/lnbits/extensions/tpos/templates/tpos/tpos.html b/lnbits/extensions/tpos/templates/tpos/tpos.html index 49d88140..ebc6595e 100644 --- a/lnbits/extensions/tpos/templates/tpos/tpos.html +++ b/lnbits/extensions/tpos/templates/tpos/tpos.html @@ -1,5 +1,13 @@ -{% extends "public.html" %} {% block toolbar_title %}{{ tpos.name }}{% endblock -%} {% block footer %}{% endblock %} {% block page_container %} +{% extends "public.html" %} {% block toolbar_title %} {{ tpos.name }} + +{% endblock %} {% block footer %}{% endblock %} {% block page_container %} @@ -43,16 +51,6 @@ color="primary" >3 - C 9 - OK #C + OK
@@ -168,7 +167,12 @@

{% raw %}{{ famount }}{% endraw %}

- {% raw %}{{ fsat }}{% endraw %} sat + {% raw %}{{ fsat }} + sat + ( + {{ tipAmountSat }} tip) + {% endraw %}
@@ -176,6 +180,35 @@
+ + + +
+ Would you like to leave a tip? +
+
+ {% raw %}{{ tip }}{% endraw %}% +
+ +
+ Close +
+
+
+ @@ -214,6 +247,10 @@ {% endblock %} {% block styles %} {% endblock %} {% block scripts %} @@ -241,14 +277,19 @@ return { tposId: '{{ tpos.id }}', currency: '{{ tpos.currency }}', + tip_options: null, exchangeRate: null, stack: [], + tipAmount: 0.0, invoiceDialog: { show: false, data: null, dismissMsg: null, paymentChecker: null }, + tipDialog: { + show: false + }, urlDialog: { show: false }, @@ -267,30 +308,71 @@ }, sat: function () { if (!this.exchangeRate) return 0 - return Math.ceil((this.amount / this.exchangeRate) * 100000000) + return Math.ceil( + ((this.amount - this.tipAmount) / this.exchangeRate) * 100000000 + ) + }, + tipAmountSat: function () { + if (!this.exchangeRate) return 0 + return Math.ceil((this.tipAmount / this.exchangeRate) * 100000000) }, fsat: function () { - console.log('sat', this.sat, LNbits.utils.formatSat(this.sat)) return LNbits.utils.formatSat(this.sat) } }, methods: { closeInvoiceDialog: function () { this.stack = [] + this.tipAmount = 0.0 var dialog = this.invoiceDialog setTimeout(function () { clearInterval(dialog.paymentChecker) dialog.dismissMsg() }, 3000) }, + processTipSelection: function (selectedTipOption) { + this.tipDialog.show = false + + if (selectedTipOption) { + const tipAmount = parseFloat( + parseFloat((selectedTipOption / 100) * this.amount) + ) + const subtotal = parseFloat(this.amount) + const grandTotal = parseFloat((tipAmount + subtotal).toFixed(2)) + const totalString = grandTotal.toFixed(2).toString() + + this.stack = [] + for (var i = 0; i < totalString.length; i++) { + const char = totalString[i] + + if (char !== '.') { + this.stack.push(char) + } + } + + this.tipAmount = tipAmount + } + + this.showInvoice() + }, + submitForm: function () { + if (this.tip_options) { + this.showTipModal() + } else { + this.showInvoice() + } + }, + showTipModal: function () { + this.tipDialog.show = true + }, showInvoice: function () { var self = this var dialog = this.invoiceDialog - console.log(this.sat, this.tposId) axios .post('/tpos/api/v1/tposs/' + this.tposId + '/invoices', null, { params: { - amount: this.sat + amount: this.sat, + tipAmount: this.tipAmountSat } }) .then(function (response) { @@ -339,9 +421,13 @@ created: function () { var getRates = this.getRates getRates() + this.tip_options = + '{{ tpos.tip_options | tojson }}' == 'null' + ? null + : JSON.parse('{{ tpos.tip_options }}') setInterval(function () { getRates() - }, 20000) + }, 120000) } }) diff --git a/lnbits/extensions/tpos/views.py b/lnbits/extensions/tpos/views.py index 2d78ecce..e1f1d21e 100644 --- a/lnbits/extensions/tpos/views.py +++ b/lnbits/extensions/tpos/views.py @@ -8,6 +8,7 @@ from starlette.responses import HTMLResponse from lnbits.core.models import User from lnbits.decorators import check_user_exists +from lnbits.settings import LNBITS_CUSTOM_LOGO, LNBITS_SITE_TITLE from . import tpos_ext, tpos_renderer from .crud import get_tpos @@ -31,5 +32,47 @@ async def tpos(request: Request, tpos_id): ) return tpos_renderer().TemplateResponse( - "tpos/tpos.html", {"request": request, "tpos": tpos} + "tpos/tpos.html", + { + "request": request, + "tpos": tpos, + "web_manifest": f"/tpos/manifest/{tpos_id}.webmanifest", + }, ) + + +@tpos_ext.get("/manifest/{tpos_id}.webmanifest") +async def manifest(tpos_id: str): + tpos = await get_tpos(tpos_id) + if not tpos: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="TPoS does not exist." + ) + + return { + "short_name": LNBITS_SITE_TITLE, + "name": tpos.name + " - " + LNBITS_SITE_TITLE, + "icons": [ + { + "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", + "sizes": "900x900", + } + ], + "start_url": "/tpos/" + tpos_id, + "background_color": "#1F2234", + "description": "Bitcoin Lightning tPOS", + "display": "standalone", + "scope": "/tpos/" + tpos_id, + "theme_color": "#1F2234", + "shortcuts": [ + { + "name": tpos.name + " - " + LNBITS_SITE_TITLE, + "short_name": tpos.name, + "description": tpos.name + " - " + LNBITS_SITE_TITLE, + "url": "/tpos/" + tpos_id, + } + ], + } diff --git a/lnbits/extensions/tpos/views_api.py b/lnbits/extensions/tpos/views_api.py index ae457b61..9609956e 100644 --- a/lnbits/extensions/tpos/views_api.py +++ b/lnbits/extensions/tpos/views_api.py @@ -2,6 +2,7 @@ from http import HTTPStatus from fastapi import Query from fastapi.params import Depends +from loguru import logger from starlette.exceptions import HTTPException from lnbits.core.crud import get_user @@ -16,7 +17,7 @@ from .models import CreateTposData @tpos_ext.get("/api/v1/tposs", status_code=HTTPStatus.OK) async def api_tposs( - all_wallets: bool = Query(None), wallet: WalletTypeInfo = Depends(get_key_type) + all_wallets: bool = Query(False), wallet: WalletTypeInfo = Depends(get_key_type) ): wallet_ids = [wallet.wallet.id] if all_wallets: @@ -52,7 +53,9 @@ async def api_tpos_delete( @tpos_ext.post("/api/v1/tposs/{tpos_id}/invoices", status_code=HTTPStatus.CREATED) -async def api_tpos_create_invoice(amount: int = Query(..., ge=1), tpos_id: str = None): +async def api_tpos_create_invoice( + amount: int = Query(..., ge=1), tipAmount: int = None, tpos_id: str = None +): tpos = await get_tpos(tpos_id) if not tpos: @@ -60,12 +63,15 @@ async def api_tpos_create_invoice(amount: int = Query(..., ge=1), tpos_id: str = status_code=HTTPStatus.NOT_FOUND, detail="TPoS does not exist." ) + if tipAmount: + amount += tipAmount + try: payment_hash, payment_request = await create_invoice( wallet_id=tpos.wallet, amount=amount, memo=f"{tpos.name}", - extra={"tag": "tpos"}, + extra={"tag": "tpos", "tipAmount": tipAmount, "tposId": tpos_id}, ) except Exception as e: raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e)) @@ -84,7 +90,8 @@ async def api_tpos_check_invoice(tpos_id: str, payment_hash: str): ) try: status = await api_payment(payment_hash) + except Exception as exc: - print(exc) + logger.error(exc) return {"paid": False} return status diff --git a/lnbits/extensions/usermanager/models.py b/lnbits/extensions/usermanager/models.py index 67facec6..15f50e28 100644 --- a/lnbits/extensions/usermanager/models.py +++ b/lnbits/extensions/usermanager/models.py @@ -1,8 +1,8 @@ from sqlite3 import Row +from typing import Optional from fastapi.param_functions import Query from pydantic import BaseModel -from typing import Optional class CreateUserData(BaseModel): diff --git a/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html b/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html index 9d2901f6..886589e6 100644 --- a/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html +++ b/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html @@ -28,6 +28,7 @@ label="API info" :content-inset-level="0.5" > + @@ -97,7 +98,7 @@ GET - /usermanager/api/v1/wallets<wallet_id>
Headers
{"X-Api-Key": <string>} @@ -109,7 +110,7 @@
Curl example
curl -X GET {{ request.base_url - }}usermanager/api/v1/wallets<wallet_id> -H "X-Api-Key: {{ + }}usermanager/api/v1/transactions/<wallet_id> -H "X-Api-Key: {{ user.wallets[0].inkey }}"
diff --git a/lnbits/extensions/usermanager/views_api.py b/lnbits/extensions/usermanager/views_api.py index a36b36bb..7e7b7653 100644 --- a/lnbits/extensions/usermanager/views_api.py +++ b/lnbits/extensions/usermanager/views_api.py @@ -75,7 +75,7 @@ async def api_usermanager_activate_extension( raise HTTPException( status_code=HTTPStatus.NOT_FOUND, detail="User does not exist." ) - update_user_extension(user_id=userid, extension=extension, active=active) + await update_user_extension(user_id=userid, extension=extension, active=active) return {"extension": "updated"} diff --git a/lnbits/extensions/watchonly/templates/watchonly/_api_docs.html b/lnbits/extensions/watchonly/templates/watchonly/_api_docs.html index e8217192..94b44a44 100644 --- a/lnbits/extensions/watchonly/templates/watchonly/_api_docs.html +++ b/lnbits/extensions/watchonly/templates/watchonly/_api_docs.html @@ -20,6 +20,7 @@ label="API info" :content-inset-level="0.5" > + @@ -37,8 +38,8 @@ [<wallets_object>, ...]
Curl example
curl -X GET {{ request.base_url }}api/v1/wallet -H "X-Api-Key: {{ - user.wallets[0].inkey }}" + >curl -X GET {{ request.base_url }}watchonly/api/v1/wallet -H + "X-Api-Key: {{ user.wallets[0].inkey }}"
@@ -66,8 +67,9 @@ [<wallet_object>, ...]
Curl example
curl -X GET {{ request.base_url }}api/v1/wallet/<wallet_id> - -H "X-Api-Key: {{ user.wallets[0].inkey }}" + >curl -X GET {{ request.base_url + }}watchonly/api/v1/wallet/<wallet_id> -H "X-Api-Key: {{ + user.wallets[0].inkey }}"
@@ -89,9 +91,10 @@ [<wallet_object>, ...]
Curl example
curl -X POST {{ request.base_url }}api/v1/wallet -d '{"title": - <string>, "masterpub": <string>}' -H "Content-type: - application/json" -H "X-Api-Key: {{ user.wallets[0].adminkey }}" + >curl -X POST {{ request.base_url }}watchonly/api/v1/wallet -d + '{"title": <string>, "masterpub": <string>}' -H + "Content-type: application/json" -H "X-Api-Key: {{ + user.wallets[0].adminkey }}"
@@ -116,7 +119,7 @@
Curl example
curl -X DELETE {{ request.base_url - }}api/v1/wallet/<wallet_id> -H "X-Api-Key: {{ + }}watchonly/api/v1/wallet/<wallet_id> -H "X-Api-Key: {{ user.wallets[0].adminkey }}" @@ -142,7 +145,7 @@
Curl example
curl -X GET {{ request.base_url - }}api/v1/addresses/<wallet_id> -H "X-Api-Key: {{ + }}watchonly/api/v1/addresses/<wallet_id> -H "X-Api-Key: {{ user.wallets[0].inkey }}" @@ -173,8 +176,9 @@ [<address_object>, ...]
Curl example
curl -X GET {{ request.base_url }}api/v1/address/<wallet_id> - -H "X-Api-Key: {{ user.wallets[0].inkey }}" + >curl -X GET {{ request.base_url + }}watchonly/api/v1/address/<wallet_id> -H "X-Api-Key: {{ + user.wallets[0].inkey }}" @@ -202,8 +206,8 @@ [<mempool_object>, ...]
Curl example
curl -X GET {{ request.base_url }}api/v1/mempool -H "X-Api-Key: {{ - user.wallets[0].adminkey }}" + >curl -X GET {{ request.base_url }}watchonly/api/v1/mempool -H + "X-Api-Key: {{ user.wallets[0].adminkey }}" @@ -233,9 +237,9 @@ [<mempool_object>, ...]
Curl example
curl -X PUT {{ request.base_url }}api/v1/mempool -d '{"endpoint": - <string>}' -H "Content-type: application/json" -H "X-Api-Key: - {{ user.wallets[0].adminkey }}" + >curl -X PUT {{ request.base_url }}watchonly/api/v1/mempool -d + '{"endpoint": <string>}' -H "Content-type: application/json" + -H "X-Api-Key: {{ user.wallets[0].adminkey }}" diff --git a/lnbits/extensions/withdraw/README.md b/lnbits/extensions/withdraw/README.md index 0e5939fd..7bf7c232 100644 --- a/lnbits/extensions/withdraw/README.md +++ b/lnbits/extensions/withdraw/README.md @@ -26,6 +26,8 @@ LNBits Quick Vouchers allows you to easily create a batch of LNURLw's QR codes t - on details you can print the vouchers\ ![printable vouchers](https://i.imgur.com/2xLHbob.jpg) - every printed LNURLw QR code is unique, it can only be used once +3. Bonus: you can use an LNbits themed voucher, or use a custom one. There's a _template.svg_ file in `static/images` folder if you want to create your own.\ + ![voucher](https://i.imgur.com/qyQoHi3.jpg) #### Advanced diff --git a/lnbits/extensions/withdraw/__init__.py b/lnbits/extensions/withdraw/__init__.py index 58ccfe7e..a0f4b606 100644 --- a/lnbits/extensions/withdraw/__init__.py +++ b/lnbits/extensions/withdraw/__init__.py @@ -9,7 +9,7 @@ db = Database("ext_withdraw") withdraw_static_files = [ { "path": "/withdraw/static", - "app": StaticFiles(directory="lnbits/extensions/withdraw/static"), + "app": StaticFiles(packages=[("lnbits", "extensions/withdraw/static")]), "name": "withdraw_static", } ] diff --git a/lnbits/extensions/withdraw/crud.py b/lnbits/extensions/withdraw/crud.py index 18a057f3..9868b057 100644 --- a/lnbits/extensions/withdraw/crud.py +++ b/lnbits/extensions/withdraw/crud.py @@ -25,9 +25,11 @@ async def create_withdraw_link( unique_hash, k1, open_time, - usescsv + usescsv, + webhook_url, + custom_url ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( link_id, @@ -42,6 +44,8 @@ async def create_withdraw_link( urlsafe_short_hash(), int(datetime.now().timestamp()) + data.wait_time, usescsv, + data.webhook_url, + data.custom_url, ), ) link = await get_withdraw_link(link_id, 0) diff --git a/lnbits/extensions/withdraw/lnurl.py b/lnbits/extensions/withdraw/lnurl.py index 801fa62f..18a99599 100644 --- a/lnbits/extensions/withdraw/lnurl.py +++ b/lnbits/extensions/withdraw/lnurl.py @@ -1,10 +1,13 @@ import json +import traceback from datetime import datetime from http import HTTPStatus +import httpx import shortuuid # type: ignore from fastapi import HTTPException from fastapi.param_functions import Query +from loguru import logger from starlette.requests import Request from starlette.responses import HTMLResponse # type: ignore @@ -30,7 +33,9 @@ async def api_lnurl_response(request: Request, unique_hash): ) if link.is_spent: - raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Withdraw is spent.") + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Withdraw is spent." + ) url = request.url_for("withdraw.api_lnurl_callback", unique_hash=link.unique_hash) withdrawResponse = { "tag": "withdrawRequest", @@ -48,7 +53,11 @@ async def api_lnurl_response(request: Request, unique_hash): @withdraw_ext.get("/api/v1/lnurl/cb/{unique_hash}", name="withdraw.api_lnurl_callback") async def api_lnurl_callback( - unique_hash, request: Request, k1: str = Query(...), pr: str = Query(...), id_unique_hash=None + unique_hash, + request: Request, + k1: str = Query(...), + pr: str = Query(...), + id_unique_hash=None, ): link = await get_withdraw_link_by_hash(unique_hash) now = int(datetime.now().timestamp()) @@ -58,7 +67,9 @@ async def api_lnurl_callback( ) if link.is_spent: - raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Withdraw is spent.") + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Withdraw is spent." + ) if link.k1 != k1: raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Bad request.") @@ -81,7 +92,7 @@ async def api_lnurl_callback( if id_unique_hash == shortuuid.uuid(name=tohash): found = True useslist.pop(ind) - usescsv = ','.join(useslist) + usescsv = ",".join(useslist) if not found: raise HTTPException( status_code=HTTPStatus.NOT_FOUND, detail="LNURL-withdraw not found." @@ -104,16 +115,34 @@ async def api_lnurl_callback( payment_request = pr - await pay_invoice( + payment_hash = await pay_invoice( wallet_id=link.wallet, payment_request=payment_request, max_sat=link.max_withdrawable, extra={"tag": "withdraw"}, ) + + if link.webhook_url: + async with httpx.AsyncClient() as client: + try: + r = await client.post( + link.webhook_url, + json={ + "payment_hash": payment_hash, + "payment_request": payment_request, + "lnurlw": link.id, + }, + timeout=40, + ) + except Exception as exc: + # webhook fails shouldn't cause the lnurlw to fail since invoice is already paid + logger.error("Caught exception when dispatching webhook url:", exc) + return {"status": "OK"} except Exception as e: await update_withdraw_link(link.id, **changesback) + logger.error(traceback.format_exc()) return {"status": "ERROR", "reason": "Link not working"} @@ -134,7 +163,9 @@ async def api_lnurl_multi_response(request: Request, unique_hash, id_unique_hash ) if link.is_spent: - raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Withdraw is spent.") + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Withdraw is spent." + ) useslist = link.usescsv.split(",") found = False diff --git a/lnbits/extensions/withdraw/migrations.py b/lnbits/extensions/withdraw/migrations.py index 1a13aa6d..5484277a 100644 --- a/lnbits/extensions/withdraw/migrations.py +++ b/lnbits/extensions/withdraw/migrations.py @@ -108,3 +108,17 @@ async def m003_make_hash_check(db): ); """ ) + + +async def m004_webhook_url(db): + """ + Adds webhook_url + """ + await db.execute("ALTER TABLE withdraw.withdraw_link ADD COLUMN webhook_url TEXT;") + + +async def m005_add_custom_print_design(db): + """ + Adds custom print design + """ + await db.execute("ALTER TABLE withdraw.withdraw_link ADD COLUMN custom_url TEXT;") diff --git a/lnbits/extensions/withdraw/models.py b/lnbits/extensions/withdraw/models.py index a03c7db8..2672537f 100644 --- a/lnbits/extensions/withdraw/models.py +++ b/lnbits/extensions/withdraw/models.py @@ -15,6 +15,8 @@ class CreateWithdrawData(BaseModel): uses: int = Query(..., ge=1) wait_time: int = Query(..., ge=1) is_unique: bool + webhook_url: str = Query(None) + custom_url: str = Query(None) class WithdrawLink(BaseModel): @@ -32,6 +34,8 @@ class WithdrawLink(BaseModel): used: int = Query(0) usescsv: str = Query(None) number: int = Query(0) + webhook_url: str = Query(None) + custom_url: str = Query(None) @property def is_spent(self) -> bool: diff --git a/lnbits/extensions/withdraw/static/js/index.js b/lnbits/extensions/withdraw/static/js/index.js index 91ff6446..1982d684 100644 --- a/lnbits/extensions/withdraw/static/js/index.js +++ b/lnbits/extensions/withdraw/static/js/index.js @@ -20,9 +20,12 @@ var mapWithdrawLink = function (obj) { obj.uses_left = obj.uses - obj.used obj.print_url = [locationPath, 'print/', obj.id].join('') obj.withdraw_url = [locationPath, obj.id].join('') + obj._data.use_custom = Boolean(obj.custom_url) return obj } +const CUSTOM_URL = '/static/images/default_voucher.png' + new Vue({ el: '#vue', mixins: [windowMixin], @@ -53,18 +56,21 @@ new Vue({ rowsPerPage: 10 } }, + nfcTagWriting: false, formDialog: { show: false, secondMultiplier: 'seconds', secondMultiplierOptions: ['seconds', 'minutes', 'hours'], data: { - is_unique: false + is_unique: false, + use_custom: false } }, simpleformDialog: { show: false, data: { is_unique: true, + use_custom: true, title: 'Vouchers', min_withdrawable: 0, wait_time: 1 @@ -105,12 +111,14 @@ new Vue({ }, closeFormDialog: function () { this.formDialog.data = { - is_unique: false + is_unique: false, + use_custom: false } }, simplecloseFormDialog: function () { this.simpleformDialog.data = { - is_unique: false + is_unique: false, + use_custom: false } }, openQrCodeDialog: function (linkId) { @@ -132,6 +140,9 @@ new Vue({ id: this.formDialog.data.wallet }) var data = _.omit(this.formDialog.data, 'wallet') + if (data.use_custom && !data?.custom_url) { + data.custom_url = CUSTOM_URL + } data.wait_time = data.wait_time * @@ -140,7 +151,6 @@ new Vue({ minutes: 60, hours: 3600 }[this.formDialog.secondMultiplier] - if (data.id) { this.updateWithdrawLink(wallet, data) } else { @@ -158,6 +168,10 @@ new Vue({ data.title = 'vouchers' data.is_unique = true + if (data.use_custom && !data?.custom_url) { + data.custom_url = '/static/images/default_voucher.png' + } + if (data.id) { this.updateWithdrawLink(wallet, data) } else { @@ -179,7 +193,9 @@ new Vue({ 'max_withdrawable', 'uses', 'wait_time', - 'is_unique' + 'is_unique', + 'webhook_url', + 'custom_url' ) ) .then(function (response) { @@ -230,6 +246,42 @@ new Vue({ }) }) }, + writeNfcTag: async function (lnurl) { + try { + if (typeof NDEFReader == 'undefined') { + throw { + toString: function () { + return 'NFC not supported on this device or browser.' + } + } + } + + const ndef = new NDEFReader() + + this.nfcTagWriting = true + this.$q.notify({ + message: 'Tap your NFC tag to write the LNURL-withdraw link to it.' + }) + + await ndef.write({ + records: [{recordType: 'url', data: 'lightning:' + lnurl, lang: 'en'}] + }) + + this.nfcTagWriting = false + this.$q.notify({ + type: 'positive', + message: 'NFC tag written successfully.' + }) + } catch (error) { + this.nfcTagWriting = false + this.$q.notify({ + type: 'negative', + message: error + ? error.toString() + : 'An unexpected error has occurred.' + }) + } + }, exportCSV: function () { LNbits.utils.exportCSV(this.paywallsTable.columns, this.paywalls) } diff --git a/lnbits/extensions/withdraw/templates/withdraw/_api_docs.html b/lnbits/extensions/withdraw/templates/withdraw/_api_docs.html index c1172bcd..ff88189d 100644 --- a/lnbits/extensions/withdraw/templates/withdraw/_api_docs.html +++ b/lnbits/extensions/withdraw/templates/withdraw/_api_docs.html @@ -4,6 +4,7 @@ label="API info" :content-inset-level="0.5" > + [<withdraw_link_object>, ...]
Curl example
curl -X GET {{ request.base_url }}withdraw/api/v1/links -H "X-Api-Key: {{ - user.wallets[0].inkey }}" + >curl -X GET {{ request.base_url }}withdraw/api/v1/links -H + "X-Api-Key: {{ user.wallets[0].inkey }}" @@ -49,8 +50,9 @@ {"lnurl": <string>}
Curl example
curl -X GET {{ request.base_url }}withdraw/api/v1/links/<withdraw_id> -H - "X-Api-Key: {{ user.wallets[0].inkey }}" + >curl -X GET {{ request.base_url + }}withdraw/api/v1/links/<withdraw_id> -H "X-Api-Key: {{ + user.wallets[0].inkey }}" @@ -70,7 +72,8 @@ {"title": <string>, "min_withdrawable": <integer>, "max_withdrawable": <integer>, "uses": <integer>, - "wait_time": <integer>, "is_unique": <boolean>}
Returns 201 CREATED (application/json) @@ -78,12 +81,12 @@ {"lnurl": <string>}
Curl example
curl -X POST {{ request.base_url }}withdraw/api/v1/links -d '{"title": - <string>, "min_withdrawable": <integer>, + >curl -X POST {{ request.base_url }}withdraw/api/v1/links -d + '{"title": <string>, "min_withdrawable": <integer>, "max_withdrawable": <integer>, "uses": <integer>, - "wait_time": <integer>, "is_unique": <boolean>}' -H - "Content-type: application/json" -H "X-Api-Key: {{ - user.wallets[0].adminkey }}" + "wait_time": <integer>, "is_unique": <boolean>, + "webhook_url": <string>}' -H "Content-type: application/json" -H + "X-Api-Key: {{ user.wallets[0].adminkey }}" @@ -114,8 +117,9 @@ {"lnurl": <string>}
Curl example
curl -X PUT {{ request.base_url }}withdraw/api/v1/links/<withdraw_id> -d - '{"title": <string>, "min_withdrawable": <integer>, + >curl -X PUT {{ request.base_url + }}withdraw/api/v1/links/<withdraw_id> -d '{"title": + <string>, "min_withdrawable": <integer>, "max_withdrawable": <integer>, "uses": <integer>, "wait_time": <integer>, "is_unique": <boolean>}' -H "Content-type: application/json" -H "X-Api-Key: {{ @@ -142,8 +146,9 @@
Curl example
curl -X DELETE {{ request.base_url }}withdraw/api/v1/links/<withdraw_id> - -H "X-Api-Key: {{ user.wallets[0].adminkey }}" + >curl -X DELETE {{ request.base_url + }}withdraw/api/v1/links/<withdraw_id> -H "X-Api-Key: {{ + user.wallets[0].adminkey }}" @@ -170,8 +175,8 @@
Curl example
curl -X GET {{ request.base_url - }}withdraw/api/v1/links/<the_hash>/<lnurl_id> -H "X-Api-Key: {{ - user.wallets[0].inkey }}" + }}withdraw/api/v1/links/<the_hash>/<lnurl_id> -H + "X-Api-Key: {{ user.wallets[0].inkey }}" diff --git a/lnbits/extensions/withdraw/templates/withdraw/csv.html b/lnbits/extensions/withdraw/templates/withdraw/csv.html index d8f8c4d0..62902905 100644 --- a/lnbits/extensions/withdraw/templates/withdraw/csv.html +++ b/lnbits/extensions/withdraw/templates/withdraw/csv.html @@ -1,10 +1,12 @@ -{% extends "print.html" %} {% block page %} {% for page in link %} {% for threes in page %} {% for one in threes %} {{one}}, {% endfor %} {% endfor %} {% endfor %} {% endblock %} {% block scripts %} +{% extends "print.html" %} {% block page %} {% for page in link %} {% for threes +in page %} {% for one in threes %} {{one}}, {% endfor %} {% endfor %} {% endfor +%} {% endblock %} {% block scripts %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/lnbits/extensions/withdraw/templates/withdraw/display.html b/lnbits/extensions/withdraw/templates/withdraw/display.html index 5552c77f..1e632741 100644 --- a/lnbits/extensions/withdraw/templates/withdraw/display.html +++ b/lnbits/extensions/withdraw/templates/withdraw/display.html @@ -13,14 +13,22 @@ :value="this.here + '/?lightning={{lnurl }}'" :options="{width: 800}" class="rounded-borders" - > + > +
-
+
Copy LNURL +
@@ -51,7 +59,8 @@ mixins: [windowMixin], data: function () { return { - here: location.protocol + '//' + location.host + here: location.protocol + '//' + location.host, + nfcTagWriting: false } } }) diff --git a/lnbits/extensions/withdraw/templates/withdraw/index.html b/lnbits/extensions/withdraw/templates/withdraw/index.html index 0ce8507b..9ff428a1 100644 --- a/lnbits/extensions/withdraw/templates/withdraw/index.html +++ b/lnbits/extensions/withdraw/templates/withdraw/index.html @@ -1,37 +1,50 @@ -{% extends "base.html" %} {% from "macros.jinja" import window_vars with context %} {% block scripts %} {{ window_vars(user) }} +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block scripts %} {{ window_vars(user) }} {% endblock %} {% block page %}
-
- - - Quick vouchers - Advanced withdraw link(s) - - +
+ + + Quick vouchers + Advanced withdraw link(s) + + - - -
-
-
Withdraw links
-
-
- Export to CSV -
-
- - {% raw %} -