diff --git a/.env.example b/.env.example index 38add8fd..4849fd06 100644 --- a/.env.example +++ b/.env.example @@ -5,16 +5,38 @@ QUART_DEBUG=true HOST=127.0.0.1 PORT=5000 -LNBITS_SITE_TITLE=LNbits LNBITS_ALLOWED_USERS="" +LNBITS_ADMIN_USERS="" +# Extensions only admin can access +LNBITS_ADMIN_EXTENSIONS="ngrok" LNBITS_DEFAULT_WALLET_NAME="LNbits wallet" -LNBITS_DATA_FOLDER="./data" + +LNBITS_AD_SPACE="" # csv ad image filepaths or urls, extensions can choose to honor +LNBITS_HIDE_API=false # Hides wallet api, extensions can choose to honor + +# Disable extensions for all users, use "all" to disable all extensions LNBITS_DISABLED_EXTENSIONS="amilk" + +# Database: to use SQLite, specify LNBITS_DATA_FOLDER +# to use PostgreSQL, specify LNBITS_DATABASE_URL=postgres://... +# to use CockroachDB, specify LNBITS_DATABASE_URL=cockroachdb://... +# for both PostgreSQL and CockroachDB, you'll need to install +# psycopg2 as an additional dependency +LNBITS_DATA_FOLDER="./data" +# LNBITS_DATABASE_URL="postgres://user:password@host:port/databasename" + LNBITS_FORCE_HTTPS=true LNBITS_SERVICE_FEE="0.0" -# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, LndWallet (gRPC), -# LndRestWallet, CLightningWallet, LNbitsWallet, SparkWallet, EclairWallet +# Change theme +LNBITS_SITE_TITLE="LNbits" +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" + +# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, +# LndRestWallet, CLightningWallet, LNbitsWallet, SparkWallet, FakeWallet LNBITS_BACKEND_WALLET_CLASS=VoidWallet # VoidWallet is just a fallback that works without any actual Lightning capabilities, # just so you can see the UI before dealing with this file. @@ -28,19 +50,15 @@ SPARK_TOKEN=myaccesstoken CLIGHTNING_RPC="/home/bob/.lightning/bitcoin/lightning-rpc" # LnbitsWallet -LNBITS_ENDPOINT=http://127.0.0.1:5000 +LNBITS_ENDPOINT=https://legend.lnbits.com LNBITS_KEY=LNBITS_ADMIN_KEY -# LndWallet -LND_GRPC_ENDPOINT=127.0.0.1 -LND_GRPC_PORT=11009 -LND_GRPC_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert" -LND_GRPC_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/admin.macaroon" - # LndRestWallet LND_REST_ENDPOINT=https://127.0.0.1:8080/ LND_REST_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert" -LND_REST_MACAROON="HEXSTRING" +LND_REST_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/admin.macaroon or HEXSTRING" +# To use an AES-encrypted macaroon, set +# LND_REST_MACAROON_ENCRYPTED="eNcRyPtEdMaCaRoOn" # LNPayWallet LNPAY_API_ENDPOINT=https://api.lnpay.co/v1/ @@ -50,13 +68,13 @@ LNPAY_API_KEY=LNPAY_API_KEY LNPAY_WALLET_KEY=LNPAY_ADMIN_KEY # LntxbotWallet -LNTXBOT_API_ENDPOINT=https://lntxbot.bigsun.xyz/ +LNTXBOT_API_ENDPOINT=https://lntxbot.com/ LNTXBOT_KEY=LNTXBOT_ADMIN_KEY # OpenNodeWallet OPENNODE_API_ENDPOINT=https://api.opennode.com/ OPENNODE_KEY=OPENNODE_ADMIN_KEY -# EclairWallet -ECLAIR_URL=http://127.0.0.1:8080 -ECLAIR_PASS=eclair_password +# FakeWallet +FAKE_WALLET_SECRET="ToTheMoon1" +LNBITS_DENOMINATION=sats \ No newline at end of file diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 77d340c1..bf90a8e3 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -9,4 +9,5 @@ jobs: - uses: actions/checkout@v1 - uses: jpetrucciani/mypy-check@master with: + mypy_flags: '--install-types --non-interactive' path: lnbits diff --git a/.github/workflows/on-push.yml b/.github/workflows/on-push.yml deleted file mode 100644 index 4d008dec..00000000 --- a/.github/workflows/on-push.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: Docker build on push - -env: - DOCKER_CLI_EXPERIMENTAL: enabled - -on: - push: - branches: - - master - -jobs: - build: - runs-on: ubuntu-20.04 - name: Build and push lnbits image - steps: - - name: Login to Docker Hub - run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin - - - name: Checkout project - uses: actions/checkout@v2 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - id: qemu - - - name: Setup Docker buildx action - uses: docker/setup-buildx-action@v1 - id: buildx - - - name: Show available Docker buildx platforms - run: echo ${{ steps.buildx.outputs.platforms }} - - - name: Cache Docker layers - uses: actions/cache@v2 - id: cache - with: - path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-buildx- - - - name: Run Docker buildx against commit hash - run: | - docker buildx build \ - --cache-from "type=local,src=/tmp/.buildx-cache" \ - --cache-to "type=local,dest=/tmp/.buildx-cache" \ - --platform linux/amd64,linux/arm64,linux/arm/v7 \ - --tag ${{ secrets.DOCKER_USERNAME }}/lnbits:${GITHUB_SHA:0:7} \ - --output "type=registry" ./ - - - name: Run Docker buildx against latest - run: | - docker buildx build \ - --cache-from "type=local,src=/tmp/.buildx-cache" \ - --cache-to "type=local,dest=/tmp/.buildx-cache" \ - --platform linux/amd64,linux/arm64,linux/arm/v7 \ - --tag ${{ secrets.DOCKER_USERNAME }}/lnbits:latest \ - --output "type=registry" ./ \ No newline at end of file diff --git a/.github/workflows/on-tag.yml b/.github/workflows/on-tag.yml new file mode 100644 index 00000000..f6fa53e9 --- /dev/null +++ b/.github/workflows/on-tag.yml @@ -0,0 +1,68 @@ +name: Build and push Docker image on tag + +env: + DOCKER_CLI_EXPERIMENTAL: enabled + +on: + push: + tags: + - "[0-9]+.[0-9]+.[0-9]+" + - "[0-9]+.[0-9]+.[0-9]+-*" + +jobs: + build: + runs-on: ubuntu-20.04 + name: Build and push lnbits image + steps: + - name: Login to Docker Hub + run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin + + - name: Checkout project + uses: actions/checkout@v2 + + - name: Import environment variables + id: import-env + shell: bash + run: echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV + + - name: Show set environment variables + run: | + printf " TAG: %s\n" "$TAG" + + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + id: qemu + + - name: Setup Docker buildx action + uses: docker/setup-buildx-action@v1 + id: buildx + + - name: Show available Docker buildx platforms + run: echo ${{ steps.buildx.outputs.platforms }} + + - name: Cache Docker layers + uses: actions/cache@v2 + id: cache + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Run Docker buildx against tag + run: | + docker buildx build \ + --cache-from "type=local,src=/tmp/.buildx-cache" \ + --cache-to "type=local,dest=/tmp/.buildx-cache" \ + --platform linux/amd64,linux/arm64 \ + --tag ${{ secrets.DOCKER_USERNAME }}/lnbits-legend:${TAG} \ + --output "type=registry" ./ + + - name: Run Docker buildx against latest + run: | + docker buildx build \ + --cache-from "type=local,src=/tmp/.buildx-cache" \ + --cache-to "type=local,dest=/tmp/.buildx-cache" \ + --platform linux/amd64,linux/arm64 \ + --tag ${{ secrets.DOCKER_USERNAME }}/lnbits-legend:latest \ + --output "type=registry" ./ \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9f114467..1d2826c9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,11 +3,11 @@ name: tests on: [push, pull_request] jobs: - build: + unit: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.8] + python-version: [3.8] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} @@ -15,22 +15,44 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Install dependencies + env: + VIRTUAL_ENV: ./venv + PATH: ${{ env.VIRTUAL_ENV }}/bin:${{ env.PATH }} 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 + 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 + - 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 diff --git a/.gitignore b/.gitignore index ca3fcd00..c5f1498c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ __pycache__ *$py.class .mypy_cache .vscode +*-lock.json *.egg *.egg-info @@ -14,6 +15,7 @@ __pycache__ .webassets-cache htmlcov test-reports +tests/data *.swo *.swp diff --git a/Dockerfile b/Dockerfile index 960fbf75..7b8e523d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,9 @@ ENV PATH="$VIRTUAL_ENV/bin:$PATH" # Install build deps RUN apt-get update -RUN apt-get install -y --no-install-recommends build-essential +RUN apt-get install -y --no-install-recommends build-essential pkg-config +RUN python -m pip install --upgrade pip +RUN pip install wheel # Install runtime deps COPY requirements.txt /tmp/requirements.txt @@ -18,7 +20,7 @@ RUN pip install -r /tmp/requirements.txt RUN pip install pylightning # Install LND specific deps -RUN pip install lndgrpc purerpc +RUN pip install lndgrpc # Production image FROM python:3.7-slim as lnbits @@ -31,18 +33,13 @@ ENV VIRTUAL_ENV="/opt/venv" COPY --from=builder --chown=1000:1000 $VIRTUAL_ENV $VIRTUAL_ENV ENV PATH="$VIRTUAL_ENV/bin:$PATH" -# Setup Quart -ENV QUART_APP="lnbits.app:create_app()" -ENV QUART_ENV="development" -ENV QUART_DEBUG="true" - -# App -ENV LNBITS_BIND="0.0.0.0:5000" - # Copy in app source WORKDIR /app COPY --chown=1000:1000 lnbits /app/lnbits +ENV LNBITS_PORT="5000" +ENV LNBITS_HOST="0.0.0.0" + EXPOSE 5000 -CMD quart assets && quart migrate && hypercorn -k trio --bind $LNBITS_BIND 'lnbits.app:create_app()' +CMD ["sh", "-c", "uvicorn lnbits.__main__:app --port $LNBITS_PORT --host $LNBITS_HOST"] diff --git a/LICENSE b/LICENSE index c25c6cbc..4f1b389a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 Arc +Copyright (c) 2022 Arc Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile index 89fa12fb..300b81aa 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,5 @@ +.PHONY: test + all: format check requirements.txt format: prettier black @@ -26,3 +28,10 @@ Pipfile.lock: Pipfile 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_DATA_FOLDER="./tests/data" \ + PYTHONUNBUFFERED=1 \ + ./venv/bin/pytest -s diff --git a/Pipfile b/Pipfile index addd84b6..6e738367 100644 --- a/Pipfile +++ b/Pipfile @@ -14,25 +14,26 @@ environs = "*" lnurl = "==0.3.6" pyscss = "*" shortuuid = "*" -quart = "*" -quart-cors = "*" -quart-compress = "*" -secure = "*" typing-extensions = "*" httpx = "*" -quart-trio = "*" -trio = "==0.16.0" -hypercorn = {extras = ["trio"], version = "*"} sqlalchemy-aio = "*" embit = "*" pyqrcode = "*" pypng = "*" sqlalchemy = "==1.3.23" +psycopg2-binary = "*" +aiofiles = "*" +asyncio = "*" +fastapi = "*" +uvicorn = {extras = ["standard"], version = "*"} +sse-starlette = "*" +jinja2 = "3.0.1" +pyngrok = "*" +secp256k1 = "*" +pycryptodomex = "*" [dev-packages] black = "==20.8b1" pytest = "*" pytest-cov = "*" mypy = "latest" -pytest-trio = "*" -trio-typing = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 81b4d689..e77de500 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "8c4056a80c682fac834266c11892573ce53807226c0810e4564976656ea5ff45" + "sha256": "3e19364434fd2db3748162ccc1f3b6bddcf7a382473069d15cee6eda5e07eef1" }, "pipfile-spec": 6, "requires": { @@ -18,35 +18,45 @@ "default": { "aiofiles": { "hashes": [ - "sha256:a1c4fc9b2ff81568c83e21392a82f344ea9d23da906e4f6a52662764545e19d4", - "sha256:c67a6823b5f23fcab0a2595a289cec7d8c863ffcb4322fb8cd6b90400aedfdbc" + "sha256:7a973fc22b29e9962d0897805ace5856e6a566ab1f0c8e5c91ff6c866519c937", + "sha256:8334f23235248a3b2e83b2c3a78a22674f39969b96397126cc93664d9a901e59" ], - "markers": "python_version >= '3.6' and python_version < '4.0'", - "version": "==0.7.0" + "index": "pypi", + "version": "==0.8.0" }, "anyio": { "hashes": [ - "sha256:41c4be842c284222b197a625d76a7ab85adf9d52788f563172fe180c2744b6c1", - "sha256:89e19b1498c8a6f12277e0bd2949597e445aa1b14361fbab2c36943639ef5190" + "sha256:a0aeffe2fb1fdf374a8e4b471444f0f3ac4fb9f5a5b542b48824475e0042a5a6", + "sha256:b5fa16c5ff93fa1046f2eeb5bbff2dad4d3514d6cda61d02816dba34fa8c3c2e" ], "markers": "python_full_version >= '3.6.2'", - "version": "==3.2.0" + "version": "==3.5.0" }, - "async-generator": { + "asgiref": { "hashes": [ - "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b", - "sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144" + "sha256:2f8abc20f7248433085eda803936d98992f1343ddb022065779f37c5da0181d0", + "sha256:88d59c13d634dcffe0510be048210188edd79aeccb6a6c9028cdad6f31d730a9" ], - "markers": "python_version >= '3.5'", - "version": "==1.10" + "markers": "python_version >= '3.7'", + "version": "==3.5.0" + }, + "asyncio": { + "hashes": [ + "sha256:83360ff8bc97980e4ff25c964c7bd3923d333d177aa4f7fb736b019f26c7cb41", + "sha256:b62c9157d36187eca799c378e572c969f0da87cd5fc42ca372d92cdb06e7e1de", + "sha256:c46a87b48213d7464f22d9a497b9eef8c1928b68320a2fa94240f969f6fec08c", + "sha256:c4d18b22701821de07bd6aea8b53d21449ec0ec5680645e5317062ea21817d2d" + ], + "index": "pypi", + "version": "==3.4.3" }, "attrs": { "hashes": [ - "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", - "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" + "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.2.0" + "version": "==21.4.0" }, "bech32": { "hashes": [ @@ -58,53 +68,12 @@ }, "bitstring": { "hashes": [ - "sha256:fdf3eb72b229d2864fb507f8f42b1b2c57af7ce5fec035972f9566de440a864a" + "sha256:0de167daa6a00c9386255a7cac931b45e6e24e0ad7ea64f1f92a64ac23ad4578", + "sha256:a5848a3f63111785224dca8bb4c0a75b62ecdef56a042c8d6be74b16f7e860e7", + "sha256:e3e340e58900a948787a05e8c08772f1ccbe133f6f41fe3f0fa19a18a22bbf4f" ], "index": "pypi", - "version": "==3.1.7" - }, - "blinker": { - "hashes": [ - "sha256:471aee25f3992bd325afa3772f1063dbdbbca947a041b8b89466dc00d606f8b6" - ], - "version": "==1.4" - }, - "brotli": { - "hashes": [ - "sha256:160c78292e98d21e73a4cc7f76a234390e516afcd982fa17e1422f7c6a9ce9c8", - "sha256:16d528a45c2e1909c2798f27f7bf0a3feec1dc9e50948e738b961618e38b6a7b", - "sha256:1c48472a6ba3b113452355b9af0a60da5c2ae60477f8feda8346f8fd48e3e87c", - "sha256:268fe94547ba25b58ebc724680609c8ee3e5a843202e9a381f6f9c5e8bdb5c70", - "sha256:269a5743a393c65db46a7bb982644c67ecba4b8d91b392403ad8a861ba6f495f", - "sha256:35a3edbe18e876e596553c4007a087f8bcfd538f19bc116917b3c7522fca0429", - "sha256:3b78a24b5fd13c03ee2b7b86290ed20efdc95da75a3557cc06811764d5ad1126", - "sha256:40d15c79f42e0a2c72892bf407979febd9cf91f36f495ffb333d1d04cebb34e4", - "sha256:4d1b810aa0ed773f81dceda2cc7b403d01057458730e309856356d4ef4188438", - "sha256:503fa6af7da9f4b5780bb7e4cbe0c639b010f12be85d02c99452825dd0feef3f", - "sha256:56d027eace784738457437df7331965473f2c0da2c70e1a1f6fdbae5402e0389", - "sha256:5913a1177fc36e30fcf6dc868ce23b0453952c78c04c266d3149b3d39e1410d6", - "sha256:5b6ef7d9f9c38292df3690fe3e302b5b530999fa90014853dcd0d6902fb59f26", - "sha256:5cb1e18167792d7d21e21365d7650b72d5081ed476123ff7b8cac7f45189c0c7", - "sha256:61a7ee1f13ab913897dac7da44a73c6d44d48a4adff42a5701e3239791c96e14", - "sha256:68715970f16b6e92c574c30747c95cf8cf62804569647386ff032195dc89a430", - "sha256:6b2ae9f5f67f89aade1fab0f7fd8f2832501311c363a21579d02defa844d9296", - "sha256:6c772d6c0a79ac0f414a9f8947cc407e119b8598de7621f39cacadae3cf57d12", - "sha256:7cb81373984cc0e4682f31bc3d6be9026006d96eecd07ea49aafb06897746452", - "sha256:854c33dad5ba0fbd6ab69185fec8dab89e13cda6b7d191ba111987df74f38761", - "sha256:88c63a1b55f352b02c6ffd24b15ead9fc0e8bf781dbe070213039324922a2eea", - "sha256:93130612b837103e15ac3f9cbacb4613f9e348b58b3aad53721d92e57f96d46a", - "sha256:97f715cf371b16ac88b8c19da00029804e20e25f30d80203417255d239f228b5", - "sha256:9d12cf2851759b8de8ca5fde36a59c08210a97ffca0eb94c532ce7b17c6a3d1d", - "sha256:afde17ae04d90fbe53afb628f7f2d4ca022797aa093e809de5c3cf276f61bbfa", - "sha256:b663f1e02de5d0573610756398e44c130add0eb9a3fc912a09665332942a2efb", - "sha256:c2415d9d082152460f2bd4e382a1e85aed233abc92db5a3880da2257dc7daf7b", - "sha256:c83aa123d56f2e060644427a882a36b3c12db93727ad7a7b9efd7d7f3e9cc2c4", - "sha256:cfc391f4429ee0a9370aa93d812a52e1fee0f37a81861f4fdd1f4fb28e8547c3", - "sha256:db844eb158a87ccab83e868a762ea8024ae27337fc7ddcbfcddd157f841fdfe7", - "sha256:defed7ea5f218a9f2336301e6fd379f55c655bea65ba2476346340a0ce6f74a1", - "sha256:f909bbbc433048b499cb9db9e713b5d8d949e8c109a2a548502fb9aa8630f0b1" - ], - "version": "==1.0.9" + "version": "==3.1.9" }, "cerberus": { "hashes": [ @@ -115,18 +84,81 @@ }, "certifi": { "hashes": [ - "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", - "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8" + "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", + "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" ], - "version": "==2021.5.30" + "version": "==2021.10.8" + }, + "cffi": { + "hashes": [ + "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3", + "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2", + "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636", + "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20", + "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728", + "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27", + "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66", + "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443", + "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0", + "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7", + "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39", + "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605", + "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a", + "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37", + "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029", + "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139", + "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc", + "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df", + "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14", + "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880", + "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2", + "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a", + "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e", + "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474", + "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024", + "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8", + "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0", + "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e", + "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a", + "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e", + "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032", + "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6", + "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e", + "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b", + "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e", + "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954", + "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962", + "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c", + "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4", + "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55", + "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962", + "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023", + "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c", + "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6", + "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8", + "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382", + "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7", + "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc", + "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997", + "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796" + ], + "version": "==1.15.0" + }, + "charset-normalizer": { + "hashes": [ + "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", + "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" + ], + "markers": "python_version >= '3.5'", + "version": "==2.0.12" }, "click": { "hashes": [ - "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a", - "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6" + "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3", + "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b" ], "markers": "python_version >= '3.6'", - "version": "==8.0.1" + "version": "==8.0.3" }, "ecdsa": { "hashes": [ @@ -138,18 +170,26 @@ }, "embit": { "hashes": [ - "sha256:d67fc0f7fbdb7588c3eb24441bf8e05770056260bc8e5537399a1b3ce5ccf12a" + "sha256:d340107dc1604581df59f844d4eb76ec34b0219c2ac2cbc1837c14938a4730ee" ], "index": "pypi", - "version": "==0.4.2" + "version": "==0.4.12" }, "environs": { "hashes": [ - "sha256:2eb671afd37e6e9820131b918bbbcaa6658d0fb420ebf35bdfb750ae39c51a66", - "sha256:6bef733b88cc901e787cf24fb2eaa72621b0656226ea4e332ab24ed0cba36fcf" + "sha256:1e549569a3de49c05f856f40bce86979e7d5ffbbc4398e7f338574c220189124", + "sha256:a76307b36fbe856bdca7ee9161e6c466fd7fcffc297109a118c59b54e27e30c9" ], "index": "pypi", - "version": "==9.3.2" + "version": "==9.5.0" + }, + "fastapi": { + "hashes": [ + "sha256:dcfee92a7f9a72b5d4b7ca364bd2b009f8fc10d95ed5769be20e94f39f7e5a15", + "sha256:f0a618aff5f6942862f2d3f20f39b1c037e33314d1b8207fd1c3a2cca76dfd8c" + ], + "index": "pypi", + "version": "==0.73.0" }, "h11": { "hashes": [ @@ -159,79 +199,65 @@ "markers": "python_version >= '3.6'", "version": "==0.12.0" }, - "h2": { - "hashes": [ - "sha256:ac9e293a1990b339d5d71b19c5fe630e3dd4d768c620d1730d355485323f1b25", - "sha256:bb7ac7099dd67a857ed52c815a6192b6b1f5ba6b516237fc24a085341340593d" - ], - "markers": "python_full_version >= '3.6.1'", - "version": "==4.0.0" - }, - "hpack": { - "hashes": [ - "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c", - "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095" - ], - "markers": "python_full_version >= '3.6.1'", - "version": "==4.0.0" - }, "httpcore": { "hashes": [ - "sha256:b0d16f0012ec88d8cc848f5a55f8a03158405f4bca02ee49bc4ca2c1fda49f3e", - "sha256:db4c0dcb8323494d01b8c6d812d80091a31e520033e7b0120883d6f52da649ff" + "sha256:47d772f754359e56dd9d892d9593b6f9870a37aeb8ba51e9a88b09b3d68cfade", + "sha256:7503ec1c0f559066e7e39bc4003fd2ce023d01cf51793e3c173b864eb456ead1" ], "markers": "python_version >= '3.6'", - "version": "==0.13.6" + "version": "==0.14.7" + }, + "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" + ], + "version": "==0.3.0" }, "httpx": { "hashes": [ - "sha256:979afafecb7d22a1d10340bafb403cf2cb75aff214426ff206521fc79d26408c", - "sha256:9f99c15d33642d38bce8405df088c1c4cfd940284b4290cacbfb02e64f4877c6" + "sha256:d8e778f76d9bbd46af49e7f062467e3157a5a3d2ae4876a4bbfd8a51ed9c9cb4", + "sha256:e35e83d1d2b9b2a609ef367cc4c1e66fd80b750348b20cc9e19d1952fc2ca3f6" ], "index": "pypi", - "version": "==0.18.2" - }, - "hypercorn": { - "extras": [ - "trio" - ], - "hashes": [ - "sha256:5ba1e719c521080abd698ff5781a2331e34ef50fc1c89a50960538115a896a9a", - "sha256:8007c10f81566920f8ae12c0e26e146f94ca70506da964b5a727ad610aa1d821" - ], - "index": "pypi", - "version": "==0.11.2" - }, - "hyperframe": { - "hashes": [ - "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15", - "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914" - ], - "markers": "python_full_version >= '3.6.1'", - "version": "==6.0.1" + "version": "==0.22.0" }, "idna": { "hashes": [ - "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", - "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3" + "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", + "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" ], - "version": "==3.2" - }, - "itsdangerous": { - "hashes": [ - "sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c", - "sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0" - ], - "markers": "python_version >= '3.6'", - "version": "==2.0.1" + "version": "==3.3" }, "jinja2": { "hashes": [ - "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4", - "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4" + "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8", + "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7" ], - "markers": "python_version >= '3.6'", - "version": "==3.0.1" + "index": "pypi", + "version": "==3.0.3" }, "lnurl": { "hashes": [ @@ -246,32 +272,67 @@ "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", @@ -283,47 +344,11 @@ }, "marshmallow": { "hashes": [ - "sha256:8050475b70470cc58f4441ee92375db611792ba39ca1ad41d39cad193ea9e040", - "sha256:b45cde981d1835145257b4a3c5cb7b80786dcf5f50dd2990749a50c16cb48e01" + "sha256:04438610bc6dadbdddb22a4a55bcc7f6f8099e69580b2e67f5a681933a1f4400", + "sha256:4c05c1684e0e97fe779c62b91878f173b937fe097b356cd82f793464f5bc6138" ], - "markers": "python_version >= '3.5'", - "version": "==3.12.1" - }, - "mypy": { - "hashes": [ - "sha256:0190fb77e93ce971954c9e54ea61de2802065174e5e990c9d4c1d0f54fbeeca2", - "sha256:0756529da2dd4d53d26096b7969ce0a47997123261a5432b48cc6848a2cb0bd4", - "sha256:2f9fedc1f186697fda191e634ac1d02f03d4c260212ccb018fabbb6d4b03eee8", - "sha256:353aac2ce41ddeaf7599f1c73fed2b75750bef3b44b6ad12985a991bc002a0da", - "sha256:3f12705eabdd274b98f676e3e5a89f247ea86dc1af48a2d5a2b080abac4e1243", - "sha256:4efc67b9b3e2fddbe395700f91d5b8deb5980bfaaccb77b306310bd0b9e002eb", - "sha256:517e7528d1be7e187a5db7f0a3e479747307c1b897d9706b1c662014faba3116", - "sha256:68a098c104ae2b75e946b107ef69dd8398d54cb52ad57580dfb9fc78f7f997f0", - "sha256:746e0b0101b8efec34902810047f26a8c80e1efbb4fc554956d848c05ef85d76", - "sha256:8be7bbd091886bde9fcafed8dd089a766fa76eb223135fe5c9e9798f78023a20", - "sha256:9236c21194fde5df1b4d8ebc2ef2c1f2a5dc7f18bcbea54274937cae2e20a01c", - "sha256:9ef5355eaaf7a23ab157c21a44c614365238a7bdb3552ec3b80c393697d974e1", - "sha256:9f1d74eeb3f58c7bd3f3f92b8f63cb1678466a55e2c4612bf36909105d0724ab", - "sha256:a26d0e53e90815c765f91966442775cf03b8a7514a4e960de7b5320208b07269", - "sha256:ae94c31bb556ddb2310e4f913b706696ccbd43c62d3331cd3511caef466871d2", - "sha256:b5ba1f0d5f9087e03bf5958c28d421a03a4c1ad260bf81556195dffeccd979c4", - "sha256:b5dfcd22c6bab08dfeded8d5b44bdcb68c6f1ab261861e35c470b89074f78a70", - "sha256:cd01c599cf9f897b6b6c6b5d8b182557fb7d99326bcdf5d449a0fbbb4ccee4b9", - "sha256:e89880168c67cf4fde4506b80ee42f1537ad66ad366c101d388b3fd7d7ce2afd", - "sha256:ebe2bc9cb638475f5d39068d2dbe8ae1d605bb8d8d3ff281c695df1670ab3987", - "sha256:f89bfda7f0f66b789792ab64ce0978e4a991a0e4dd6197349d0767b0f1095b21", - "sha256:fc4d63da57ef0e8cd4ab45131f3fe5c286ce7dd7f032650d0fbc239c6190e167", - "sha256:fd634bc17b1e2d6ce716f0e43446d0d61cdadb1efcad5c56ca211c22b246ebc8" - ], - "markers": "implementation_name == 'cpython'", - "version": "==0.902" - }, - "mypy-extensions": { - "hashes": [ - "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", - "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" - ], - "version": "==0.4.3" + "markers": "python_version >= '3.6'", + "version": "==3.14.1" }, "outcome": { "hashes": [ @@ -333,47 +358,162 @@ "markers": "python_version >= '3.6'", "version": "==1.1.0" }, - "priority": { + "psycopg2-binary": { "hashes": [ - "sha256:6bc1961a6d7fcacbfc337769f1a382c8e746566aaa365e78047abe9f66b2ffbe", - "sha256:be4fcb94b5e37cdeb40af5533afe6dd603bd665fe9c8b3052610fc1001d5d1eb" + "sha256:01310cf4cf26db9aea5158c217caa92d291f0500051a6469ac52166e1a16f5b7", + "sha256:083a55275f09a62b8ca4902dd11f4b33075b743cf0d360419e2051a8a5d5ff76", + "sha256:090f3348c0ab2cceb6dfbe6bf721ef61262ddf518cd6cc6ecc7d334996d64efa", + "sha256:0a29729145aaaf1ad8bafe663131890e2111f13416b60e460dae0a96af5905c9", + "sha256:0c9d5450c566c80c396b7402895c4369a410cab5a82707b11aee1e624da7d004", + "sha256:10bb90fb4d523a2aa67773d4ff2b833ec00857f5912bafcfd5f5414e45280fb1", + "sha256:12b11322ea00ad8db8c46f18b7dfc47ae215e4df55b46c67a94b4effbaec7094", + "sha256:152f09f57417b831418304c7f30d727dc83a12761627bb826951692cc6491e57", + "sha256:15803fa813ea05bef089fa78835118b5434204f3a17cb9f1e5dbfd0b9deea5af", + "sha256:15c4e4cfa45f5a60599d9cec5f46cd7b1b29d86a6390ec23e8eebaae84e64554", + "sha256:183a517a3a63503f70f808b58bfbf962f23d73b6dccddae5aa56152ef2bcb232", + "sha256:1f14c8b0942714eb3c74e1e71700cbbcb415acbc311c730370e70c578a44a25c", + "sha256:1f6b813106a3abdf7b03640d36e24669234120c72e91d5cbaeb87c5f7c36c65b", + "sha256:280b0bb5cbfe8039205c7981cceb006156a675362a00fe29b16fbc264e242834", + "sha256:2d872e3c9d5d075a2e104540965a1cf898b52274a5923936e5bfddb58c59c7c2", + "sha256:2f9ffd643bc7349eeb664eba8864d9e01f057880f510e4681ba40a6532f93c71", + "sha256:3303f8807f342641851578ee7ed1f3efc9802d00a6f83c101d21c608cb864460", + "sha256:35168209c9d51b145e459e05c31a9eaeffa9a6b0fd61689b48e07464ffd1a83e", + "sha256:3a79d622f5206d695d7824cbf609a4f5b88ea6d6dab5f7c147fc6d333a8787e4", + "sha256:404224e5fef3b193f892abdbf8961ce20e0b6642886cfe1fe1923f41aaa75c9d", + "sha256:46f0e0a6b5fa5851bbd9ab1bc805eef362d3a230fbdfbc209f4a236d0a7a990d", + "sha256:47133f3f872faf28c1e87d4357220e809dfd3fa7c64295a4a148bcd1e6e34ec9", + "sha256:526ea0378246d9b080148f2d6681229f4b5964543c170dd10bf4faaab6e0d27f", + "sha256:53293533fcbb94c202b7c800a12c873cfe24599656b341f56e71dd2b557be063", + "sha256:539b28661b71da7c0e428692438efbcd048ca21ea81af618d845e06ebfd29478", + "sha256:57804fc02ca3ce0dbfbef35c4b3a4a774da66d66ea20f4bda601294ad2ea6092", + "sha256:63638d875be8c2784cfc952c9ac34e2b50e43f9f0a0660b65e2a87d656b3116c", + "sha256:6472a178e291b59e7f16ab49ec8b4f3bdada0a879c68d3817ff0963e722a82ce", + "sha256:68641a34023d306be959101b345732360fc2ea4938982309b786f7be1b43a4a1", + "sha256:6e82d38390a03da28c7985b394ec3f56873174e2c88130e6966cb1c946508e65", + "sha256:761df5313dc15da1502b21453642d7599d26be88bff659382f8f9747c7ebea4e", + "sha256:7af0dd86ddb2f8af5da57a976d27cd2cd15510518d582b478fbb2292428710b4", + "sha256:7b1e9b80afca7b7a386ef087db614faebbf8839b7f4db5eb107d0f1a53225029", + "sha256:874a52ecab70af13e899f7847b3e074eeb16ebac5615665db33bce8a1009cf33", + "sha256:887dd9aac71765ac0d0bac1d0d4b4f2c99d5f5c1382d8b770404f0f3d0ce8a39", + "sha256:8b344adbb9a862de0c635f4f0425b7958bf5a4b927c8594e6e8d261775796d53", + "sha256:8fc53f9af09426a61db9ba357865c77f26076d48669f2e1bb24d85a22fb52307", + "sha256:91920527dea30175cc02a1099f331aa8c1ba39bf8b7762b7b56cbf54bc5cce42", + "sha256:93cd1967a18aa0edd4b95b1dfd554cf15af657cb606280996d393dadc88c3c35", + "sha256:99485cab9ba0fa9b84f1f9e1fef106f44a46ef6afdeec8885e0b88d0772b49e8", + "sha256:9d29409b625a143649d03d0fd7b57e4b92e0ecad9726ba682244b73be91d2fdb", + "sha256:a29b3ca4ec9defec6d42bf5feb36bb5817ba3c0230dd83b4edf4bf02684cd0ae", + "sha256:a9e1f75f96ea388fbcef36c70640c4efbe4650658f3d6a2967b4cc70e907352e", + "sha256:accfe7e982411da3178ec690baaceaad3c278652998b2c45828aaac66cd8285f", + "sha256:adf20d9a67e0b6393eac162eb81fb10bc9130a80540f4df7e7355c2dd4af9fba", + "sha256:af9813db73395fb1fc211bac696faea4ca9ef53f32dc0cfa27e4e7cf766dcf24", + "sha256:b1c8068513f5b158cf7e29c43a77eb34b407db29aca749d3eb9293ee0d3103ca", + "sha256:bda845b664bb6c91446ca9609fc69f7db6c334ec5e4adc87571c34e4f47b7ddb", + "sha256:c381bda330ddf2fccbafab789d83ebc6c53db126e4383e73794c74eedce855ef", + "sha256:c3ae8e75eb7160851e59adc77b3a19a976e50622e44fd4fd47b8b18208189d42", + "sha256:d1c1b569ecafe3a69380a94e6ae09a4789bbb23666f3d3a08d06bbd2451f5ef1", + "sha256:def68d7c21984b0f8218e8a15d514f714d96904265164f75f8d3a70f9c295667", + "sha256:dffc08ca91c9ac09008870c9eb77b00a46b3378719584059c034b8945e26b272", + "sha256:e3699852e22aa68c10de06524a3721ade969abf382da95884e6a10ff798f9281", + "sha256:e847774f8ffd5b398a75bc1c18fbb56564cda3d629fe68fd81971fece2d3c67e", + "sha256:ffb7a888a047696e7f8240d649b43fb3644f14f0ee229077e7f6b9f9081635bd" ], - "version": "==1.3.0" + "index": "pypi", + "version": "==2.9.3" + }, + "pycparser": { + "hashes": [ + "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", + "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" + ], + "version": "==2.21" + }, + "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" + ], + "index": "pypi", + "version": "==3.14.1" }, "pydantic": { "hashes": [ - "sha256:021ea0e4133e8c824775a0cfe098677acf6fa5a3cbf9206a376eed3fc09302cd", - "sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739", - "sha256:05ef5246a7ffd2ce12a619cbb29f3307b7c4509307b1b49f456657b43529dc6f", - "sha256:10e5622224245941efc193ad1d159887872776df7a8fd592ed746aa25d071840", - "sha256:18b5ea242dd3e62dbf89b2b0ec9ba6c7b5abaf6af85b95a97b00279f65845a23", - "sha256:234a6c19f1c14e25e362cb05c68afb7f183eb931dd3cd4605eafff055ebbf287", - "sha256:244ad78eeb388a43b0c927e74d3af78008e944074b7d0f4f696ddd5b2af43c62", - "sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b", - "sha256:41b542c0b3c42dc17da70554bc6f38cbc30d7066d2c2815a94499b5684582ecb", - "sha256:4a03cbbe743e9c7247ceae6f0d8898f7a64bb65800a45cbdc52d65e370570820", - "sha256:4be75bebf676a5f0f87937c6ddb061fa39cbea067240d98e298508c1bda6f3f3", - "sha256:54cd5121383f4a461ff7644c7ca20c0419d58052db70d8791eacbbe31528916b", - "sha256:589eb6cd6361e8ac341db97602eb7f354551482368a37f4fd086c0733548308e", - "sha256:8621559dcf5afacf0069ed194278f35c255dc1a1385c28b32dd6c110fd6531b3", - "sha256:8b223557f9510cf0bfd8b01316bf6dd281cf41826607eada99662f5e4963f316", - "sha256:99a9fc39470010c45c161a1dc584997f1feb13f689ecf645f59bb4ba623e586b", - "sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4", - "sha256:a83db7205f60c6a86f2c44a61791d993dff4b73135df1973ecd9eed5ea0bda20", - "sha256:ac8eed4ca3bd3aadc58a13c2aa93cd8a884bcf21cb019f8cfecaae3b6ce3746e", - "sha256:e710876437bc07bd414ff453ac8ec63d219e7690128d925c6e82889d674bb505", - "sha256:ea5cb40a3b23b3265f6325727ddfc45141b08ed665458be8c6285e7b85bd73a1", - "sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833" + "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" ], "markers": "python_full_version >= '3.6.1'", - "version": "==1.8.2" + "version": "==1.9.0" + }, + "pyngrok": { + "hashes": [ + "sha256:4d03f44a69c3cbc168b17377956a9edcf723e77dbc864eba34c272db15da443c" + ], + "index": "pypi", + "version": "==5.1.0" }, "pypng": { "hashes": [ - "sha256:1032833440c91bafee38a42c38c02d00431b24c42927feb3e63b104d8550170b" + "sha256:76f8a1539ec56451da7ab7121f12a361969fe0f2d48d703d198ce2a99d6c5afd" ], "index": "pypi", - "version": "==0.0.20" + "version": "==0.0.21" }, "pyqrcode": { "hashes": [ @@ -392,42 +532,49 @@ }, "python-dotenv": { "hashes": [ - "sha256:dd8fe852847f4fbfadabf6183ddd4c824a9651f02d51714fa075c95561959c7d", - "sha256:effaac3c1e58d89b3ccb4d04a40dc7ad6e0275fda25fd75ae9d323e2465e202d" + "sha256:32b2bdc1873fd3a3c346da1c6db83d0053c3c62f28f1f38516070c4c8971b1d3", + "sha256:a5de49a31e953b45ff2d2fd434bbc2670e8db5273606c1e737cc6b93eff3655f" ], - "version": "==0.18.0" + "markers": "python_full_version >= '3.5.0'", + "version": "==0.19.2" }, - "quart": { + "pyyaml": { "hashes": [ - "sha256:f35134fb1d81af61624e6d89bca33cd611dcedce2dc4e291f527ab04395f4e1a", - "sha256:f80c91d1e0588662483e22dd9c368a5778886b62e128c5399d2cc1b1898482cf" + "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", + "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", + "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", + "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", + "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", + "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", + "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", + "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", + "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", + "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", + "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", + "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", + "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", + "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", + "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", + "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", + "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", + "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", + "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", + "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", + "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", + "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", + "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", + "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", + "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", + "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", + "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", + "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", + "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", + "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", + "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", + "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", + "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" ], - "index": "pypi", - "version": "==0.15.1" - }, - "quart-compress": { - "hashes": [ - "sha256:41cd0cc8d26905a45025ddda7022461a71b9d1d950b21b006dc106a1c41c75ef", - "sha256:63af5e6370aa7850fb219d22e1db89965aeb13b8f27bc83e7f9a44118faa3c54" - ], - "index": "pypi", - "version": "==0.2.1" - }, - "quart-cors": { - "hashes": [ - "sha256:c2be932f20413a56b176527090229afe8f725a3ee029d45ea08a174cdc319823", - "sha256:ea08d26aef918d59194fbf065cde9b6cae90dc5f21120dcd254d7d46190cd293" - ], - "index": "pypi", - "version": "==0.5.0" - }, - "quart-trio": { - "hashes": [ - "sha256:27617f0c9fa8759d3056e9ddcdc038d44093af45eb5f84f8d5714872aaaa8c7d", - "sha256:30dfab5e382f06c605d4a5960e8188e8e05d10198f02097f0a16c1dca41b3574" - ], - "index": "pypi", - "version": "==0.8.0" + "version": "==6.0" }, "represent": { "hashes": [ @@ -447,21 +594,42 @@ ], "version": "==1.5.0" }, - "secure": { + "secp256k1": { "hashes": [ - "sha256:6e30939d8f95bf3b8effb8a36ebb5ed57f265daeeae905e3aa9677ea538ab64e", - "sha256:a93b720c7614809c131ca80e477263140107c6c212829d0a6e1f7bc8d859c608" + "sha256:130f119b06142e597c10eb4470b5a38eae865362d01aaef06b113478d77f728d", + "sha256:373dc8bca735f3c2d73259aa2711a9ecea2f3c7edbb663555fe3422e3dd76102", + "sha256:3aedcfe6eb1c5fa7c6be25b7cc91c76d8eb984271920ba0f7a934ae41ed56f51", + "sha256:4b1bf09953cde181132cf5e9033065615e5c2694e803165e2db763efa47695e5", + "sha256:63eb148196b8f646922d4be6739b17fbbf50ebb3a020078c823e2445d88b7a81", + "sha256:6af07be5f8612628c3638dc7b208f6cc78d0abae3e25797eadb13890c7d5da81", + "sha256:72735da6cb28273e924431cd40aa607e7f80ef09608c8c9300be2e0e1d2417b4", + "sha256:7a27c479ab60571502516a1506a562d0a9df062de8ad645313fabfcc97252816", + "sha256:82c06712d69ef945220c8b53c1a0d424c2ff6a1f64aee609030df79ad8383397", + "sha256:87f4ad42a370f768910585989a301d1d65de17dcd86f6e8def9b021364b34d5c", + "sha256:97a30c8dae633cb18135c76b6517ae99dc59106818e8985be70dbc05dcc06c0d", + "sha256:a8dbd75a9fb6f42de307f3c5e24573fe59c3374637cbf39136edc66c200a4029", + "sha256:adc23a4c5d24c95191638eb2ca313097827f07db102e77b59faed15d50c98cae", + "sha256:bc761894b3634021686714278fc62b73395fa3eded33453eadfd8a00a6c44ef3", + "sha256:c91dd3154f6c46ac798d9a41166120e1751222587f54516cc3f378f56ce4ac82", + "sha256:c9e7c024ff17e9b9d7c392bb2a917da231d6cb40ab119389ff1f51dca10339a4", + "sha256:ce0314788d3248b275426501228969fd32f6501c9d1837902ee0e7bd8264a36f", + "sha256:f4062d8c101aa63b9ecb3709f1f075ad9c01b6672869bbaa1bd77271816936a7", + "sha256:f4b9306bff6dde020444dfee9ca9b9f5b20ca53a2c0b04898361a3f43d5daf2e", + "sha256:f666c67dcf1dc69e1448b2ede5e12aaf382b600204a61dbc65e4f82cea444405", + "sha256:fcabb3c3497a902fb61eec72d1b69bf72747d7bcc2a732d56d9319a1e8322262", + "sha256:fe3f503c9dfdf663b500d3e0688ad842e116c2907ad3f1e1d685812df3f56290", + "sha256:fec790cb6d0d37129ca0ce5b3f8e85692d5fb618d1c440f189453d18694035df" ], "index": "pypi", - "version": "==0.3.0" + "version": "==0.14.0" }, "shortuuid": { "hashes": [ - "sha256:3c11d2007b915c43bee3e10625f068d8a349e04f0d81f08f5fa08507427ebf1f", - "sha256:492c7402ff91beb1342a5898bd61ea953985bf24a41cd9f247409aa2e03c8f77" + "sha256:44a7a86bcf24dbaba2e626cf80c779926b7c3a0d31a3a013e0d3cd1077707d23", + "sha256:9435e87e5a64f3b92f7110c81f989a3b7bdb9358e22d2359829167da476cfc23" ], "index": "pypi", - "version": "==1.0.1" + "version": "==1.0.8" }, "six": { "hashes": [ @@ -479,13 +647,6 @@ "markers": "python_version >= '3.5'", "version": "==1.2.0" }, - "sortedcontainers": { - "hashes": [ - "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", - "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0" - ], - "version": "==2.4.0" - }, "sqlalchemy": { "hashes": [ "sha256:040bdfc1d76a9074717a3f43455685f781c581f94472b010cd6c4754754e1862", @@ -538,54 +699,121 @@ "index": "pypi", "version": "==0.16.0" }, - "toml": { + "sse-starlette": { "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" - }, - "trio": { - "hashes": [ - "sha256:451ddb27b4e5215e00646fcbb8028d341fccf284e053dc376506a14bb133dbcf", - "sha256:df067dd0560c321af39d412cd81fc3a7d13f55af9150527daab980683e9fcf3c" + "sha256:840607fed361360cc76f8408a25f0eca887e7cab3c3ee44f9762f179428e2bd4", + "sha256:ca2de945af80b83f1efda6144df9e13db83880b3b87c660044b64f199395e8b7" ], "index": "pypi", - "version": "==0.16.0" + "version": "==0.10.3" }, - "trio-typing": { + "starlette": { "hashes": [ - "sha256:35f1bec8df2150feab6c8b073b54135321722c9d9289bbffa78a9a091ea83b72", - "sha256:f2007df617a6c26a2294db0dd63645b5451149757e1bde4cb8dbf3e1369174fb" + "sha256:26a18cbda5e6b651c964c12c88b36d9898481cd428ed6e063f5f29c418f73050", + "sha256:57eab3cc975a28af62f6faec94d355a410634940f10b30d68d31cb5ec1b44ae8" ], - "index": "pypi", - "version": "==0.5.0" + "markers": "python_version >= '3.6'", + "version": "==0.17.1" }, "typing-extensions": { "hashes": [ - "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", - "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", - "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" + "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42", + "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2" ], "index": "pypi", - "version": "==3.10.0.0" + "version": "==4.1.1" }, - "werkzeug": { - "hashes": [ - "sha256:1de1db30d010ff1af14a009224ec49ab2329ad2cde454c8a708130642d579c42", - "sha256:6c1ec500dcdba0baa27600f6a22f6333d8b662d22027ff9f6202e3367413caa8" + "uvicorn": { + "extras": [ + "standard" ], - "markers": "python_version >= '3.6'", - "version": "==2.0.1" + "hashes": [ + "sha256:25850bbc86195a71a6477b3e4b3b7b4c861fb687fb96912972ce5324472b1011", + "sha256:e85872d84fb651cccc4c5d2a71cf7ead055b8fb4d8f1e78e36092282c0cf2aec" + ], + "index": "pypi", + "version": "==0.17.4" }, - "wsproto": { + "uvloop": { "hashes": [ - "sha256:868776f8456997ad0d9720f7322b746bbe9193751b5b290b7f924659377c8c38", - "sha256:d8345d1808dd599b5ffb352c25a367adb6157e664e140dbecba3f9bc007edb9f" + "sha256:04ff57aa137230d8cc968f03481176041ae789308b4d5079118331ab01112450", + "sha256:089b4834fd299d82d83a25e3335372f12117a7d38525217c2258e9b9f4578897", + "sha256:1e5f2e2ff51aefe6c19ee98af12b4ae61f5be456cd24396953244a30880ad861", + "sha256:30ba9dcbd0965f5c812b7c2112a1ddf60cf904c1c160f398e7eed3a6b82dcd9c", + "sha256:3a19828c4f15687675ea912cc28bbcb48e9bb907c801873bd1519b96b04fb805", + "sha256:6224f1401025b748ffecb7a6e2652b17768f30b1a6a3f7b44660e5b5b690b12d", + "sha256:647e481940379eebd314c00440314c81ea547aa636056f554d491e40503c8464", + "sha256:6ccd57ae8db17d677e9e06192e9c9ec4bd2066b77790f9aa7dede2cc4008ee8f", + "sha256:772206116b9b57cd625c8a88f2413df2fcfd0b496eb188b82a43bed7af2c2ec9", + "sha256:8e0d26fa5875d43ddbb0d9d79a447d2ace4180d9e3239788208527c4784f7cab", + "sha256:98d117332cc9e5ea8dfdc2b28b0a23f60370d02e1395f88f40d1effd2cb86c4f", + "sha256:b572256409f194521a9895aef274cea88731d14732343da3ecdb175228881638", + "sha256:bd53f7f5db562f37cd64a3af5012df8cac2c464c97e732ed556800129505bd64", + "sha256:bd8f42ea1ea8f4e84d265769089964ddda95eb2bb38b5cbe26712b0616c3edee", + "sha256:e814ac2c6f9daf4c36eb8e85266859f42174a4ff0d71b99405ed559257750382", + "sha256:f74bc20c7b67d1c27c72601c78cf95be99d5c2cdd4514502b4f3eb0933ff1228" ], - "markers": "python_full_version >= '3.6.1'", - "version": "==1.0.0" + "version": "==0.16.0" + }, + "watchgod": { + "hashes": [ + "sha256:48140d62b0ebe9dd9cf8381337f06351e1f2e70b2203fa9c6eff4e572ca84f29", + "sha256:d6c1ea21df37847ac0537ca0d6c2f4cdf513562e95f77bb93abbcf05573407b7" + ], + "version": "==0.7" + }, + "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" + ], + "version": "==10.1" } }, "develop": { @@ -596,21 +824,13 @@ ], "version": "==1.4.4" }, - "async-generator": { - "hashes": [ - "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b", - "sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144" - ], - "markers": "python_version >= '3.5'", - "version": "==1.10" - }, "attrs": { "hashes": [ - "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", - "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" + "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.2.0" + "version": "==21.4.0" }, "black": { "hashes": [ @@ -621,76 +841,61 @@ }, "click": { "hashes": [ - "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a", - "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6" + "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3", + "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b" ], "markers": "python_version >= '3.6'", - "version": "==8.0.1" + "version": "==8.0.3" }, "coverage": { - "hashes": [ - "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c", - "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6", - "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45", - "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a", - "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03", - "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529", - "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a", - "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a", - "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2", - "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6", - "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759", - "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53", - "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a", - "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4", - "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff", - "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502", - "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793", - "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb", - "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905", - "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821", - "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b", - "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81", - "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0", - "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b", - "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3", - "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184", - "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701", - "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a", - "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82", - "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638", - "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5", - "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083", - "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6", - "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90", - "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465", - "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a", - "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3", - "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e", - "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066", - "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf", - "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b", - "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae", - "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669", - "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873", - "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b", - "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6", - "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb", - "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160", - "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c", - "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079", - "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d", - "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6" + "extras": [ + "toml" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==5.5" - }, - "idna": { "hashes": [ - "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", - "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3" + "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" ], - "version": "==3.2" + "markers": "python_version >= '3.7'", + "version": "==6.3.1" }, "iniconfig": { "hashes": [ @@ -701,32 +906,29 @@ }, "mypy": { "hashes": [ - "sha256:0190fb77e93ce971954c9e54ea61de2802065174e5e990c9d4c1d0f54fbeeca2", - "sha256:0756529da2dd4d53d26096b7969ce0a47997123261a5432b48cc6848a2cb0bd4", - "sha256:2f9fedc1f186697fda191e634ac1d02f03d4c260212ccb018fabbb6d4b03eee8", - "sha256:353aac2ce41ddeaf7599f1c73fed2b75750bef3b44b6ad12985a991bc002a0da", - "sha256:3f12705eabdd274b98f676e3e5a89f247ea86dc1af48a2d5a2b080abac4e1243", - "sha256:4efc67b9b3e2fddbe395700f91d5b8deb5980bfaaccb77b306310bd0b9e002eb", - "sha256:517e7528d1be7e187a5db7f0a3e479747307c1b897d9706b1c662014faba3116", - "sha256:68a098c104ae2b75e946b107ef69dd8398d54cb52ad57580dfb9fc78f7f997f0", - "sha256:746e0b0101b8efec34902810047f26a8c80e1efbb4fc554956d848c05ef85d76", - "sha256:8be7bbd091886bde9fcafed8dd089a766fa76eb223135fe5c9e9798f78023a20", - "sha256:9236c21194fde5df1b4d8ebc2ef2c1f2a5dc7f18bcbea54274937cae2e20a01c", - "sha256:9ef5355eaaf7a23ab157c21a44c614365238a7bdb3552ec3b80c393697d974e1", - "sha256:9f1d74eeb3f58c7bd3f3f92b8f63cb1678466a55e2c4612bf36909105d0724ab", - "sha256:a26d0e53e90815c765f91966442775cf03b8a7514a4e960de7b5320208b07269", - "sha256:ae94c31bb556ddb2310e4f913b706696ccbd43c62d3331cd3511caef466871d2", - "sha256:b5ba1f0d5f9087e03bf5958c28d421a03a4c1ad260bf81556195dffeccd979c4", - "sha256:b5dfcd22c6bab08dfeded8d5b44bdcb68c6f1ab261861e35c470b89074f78a70", - "sha256:cd01c599cf9f897b6b6c6b5d8b182557fb7d99326bcdf5d449a0fbbb4ccee4b9", - "sha256:e89880168c67cf4fde4506b80ee42f1537ad66ad366c101d388b3fd7d7ce2afd", - "sha256:ebe2bc9cb638475f5d39068d2dbe8ae1d605bb8d8d3ff281c695df1670ab3987", - "sha256:f89bfda7f0f66b789792ab64ce0978e4a991a0e4dd6197349d0767b0f1095b21", - "sha256:fc4d63da57ef0e8cd4ab45131f3fe5c286ce7dd7f032650d0fbc239c6190e167", - "sha256:fd634bc17b1e2d6ce716f0e43446d0d61cdadb1efcad5c56ca211c22b246ebc8" + "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" ], - "markers": "implementation_name == 'cpython'", - "version": "==0.902" + "index": "pypi", + "version": "==0.931" }, "mypy-extensions": { "hashes": [ @@ -735,136 +937,139 @@ ], "version": "==0.4.3" }, - "outcome": { - "hashes": [ - "sha256:c7dd9375cfd3c12db9801d080a3b63d4b0a261aa996c4c13152380587288d958", - "sha256:e862f01d4e626e63e8f92c38d1f8d5546d3f9cce989263c521b2e7990d186967" - ], - "markers": "python_version >= '3.6'", - "version": "==1.1.0" - }, "packaging": { "hashes": [ - "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", - "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" + "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", + "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.9" + "markers": "python_version >= '3.6'", + "version": "==21.3" }, "pathspec": { "hashes": [ - "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd", - "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d" + "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a", + "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1" ], - "version": "==0.8.1" + "version": "==0.9.0" }, "pluggy": { "hashes": [ - "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", - "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" + "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", + "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.13.1" + "markers": "python_version >= '3.6'", + "version": "==1.0.0" }, "py": { "hashes": [ - "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3", - "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a" + "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", + "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.10.0" + "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:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", - "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" + "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea", + "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.4.7" + "markers": "python_version >= '3.6'", + "version": "==3.0.7" }, "pytest": { "hashes": [ - "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b", - "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890" + "sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db", + "sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171" ], "index": "pypi", - "version": "==6.2.4" + "version": "==7.0.1" }, "pytest-cov": { "hashes": [ - "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a", - "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7" + "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6", + "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470" ], "index": "pypi", - "version": "==2.12.1" - }, - "pytest-trio": { - "hashes": [ - "sha256:c01b741819aec2c419555f28944e132d3c711dae1e673d63260809bf92c30c31" - ], - "index": "pypi", - "version": "==0.7.0" + "version": "==3.0.0" }, "regex": { "hashes": [ - "sha256:01afaf2ec48e196ba91b37451aa353cb7eda77efe518e481707e0515025f0cd5", - "sha256:11d773d75fa650cd36f68d7ca936e3c7afaae41b863b8c387a22aaa78d3c5c79", - "sha256:18c071c3eb09c30a264879f0d310d37fe5d3a3111662438889ae2eb6fc570c31", - "sha256:1e1c20e29358165242928c2de1482fb2cf4ea54a6a6dea2bd7a0e0d8ee321500", - "sha256:281d2fd05555079448537fe108d79eb031b403dac622621c78944c235f3fcf11", - "sha256:314d66636c494ed9c148a42731b3834496cc9a2c4251b1661e40936814542b14", - "sha256:32e65442138b7b76dd8173ffa2cf67356b7bc1768851dded39a7a13bf9223da3", - "sha256:339456e7d8c06dd36a22e451d58ef72cef293112b559010db3d054d5560ef439", - "sha256:3916d08be28a1149fb97f7728fca1f7c15d309a9f9682d89d79db75d5e52091c", - "sha256:3a9cd17e6e5c7eb328517969e0cb0c3d31fd329298dd0c04af99ebf42e904f82", - "sha256:47bf5bf60cf04d72bf6055ae5927a0bd9016096bf3d742fa50d9bf9f45aa0711", - "sha256:4c46e22a0933dd783467cf32b3516299fb98cfebd895817d685130cc50cd1093", - "sha256:4c557a7b470908b1712fe27fb1ef20772b78079808c87d20a90d051660b1d69a", - "sha256:52ba3d3f9b942c49d7e4bc105bb28551c44065f139a65062ab7912bef10c9afb", - "sha256:563085e55b0d4fb8f746f6a335893bda5c2cef43b2f0258fe1020ab1dd874df8", - "sha256:598585c9f0af8374c28edd609eb291b5726d7cbce16be6a8b95aa074d252ee17", - "sha256:619d71c59a78b84d7f18891fe914446d07edd48dc8328c8e149cbe0929b4e000", - "sha256:67bdb9702427ceddc6ef3dc382455e90f785af4c13d495f9626861763ee13f9d", - "sha256:6d1b01031dedf2503631d0903cb563743f397ccaf6607a5e3b19a3d76fc10480", - "sha256:741a9647fcf2e45f3a1cf0e24f5e17febf3efe8d4ba1281dcc3aa0459ef424dc", - "sha256:7c2a1af393fcc09e898beba5dd59196edaa3116191cc7257f9224beaed3e1aa0", - "sha256:7d9884d86dd4dd489e981d94a65cd30d6f07203d90e98f6f657f05170f6324c9", - "sha256:90f11ff637fe8798933fb29f5ae1148c978cccb0452005bf4c69e13db951e765", - "sha256:919859aa909429fb5aa9cf8807f6045592c85ef56fdd30a9a3747e513db2536e", - "sha256:96fcd1888ab4d03adfc9303a7b3c0bd78c5412b2bfbe76db5b56d9eae004907a", - "sha256:97f29f57d5b84e73fbaf99ab3e26134e6687348e95ef6b48cfd2c06807005a07", - "sha256:980d7be47c84979d9136328d882f67ec5e50008681d94ecc8afa8a65ed1f4a6f", - "sha256:a91aa8619b23b79bcbeb37abe286f2f408d2f2d6f29a17237afda55bb54e7aac", - "sha256:ade17eb5d643b7fead300a1641e9f45401c98eee23763e9ed66a43f92f20b4a7", - "sha256:b9c3db21af35e3b3c05764461b262d6f05bbca08a71a7849fd79d47ba7bc33ed", - "sha256:bd28bc2e3a772acbb07787c6308e00d9626ff89e3bfcdebe87fa5afbfdedf968", - "sha256:bf5824bfac591ddb2c1f0a5f4ab72da28994548c708d2191e3b87dd207eb3ad7", - "sha256:c0502c0fadef0d23b128605d69b58edb2c681c25d44574fc673b0e52dce71ee2", - "sha256:c38c71df845e2aabb7fb0b920d11a1b5ac8526005e533a8920aea97efb8ec6a4", - "sha256:ce15b6d103daff8e9fee13cf7f0add05245a05d866e73926c358e871221eae87", - "sha256:d3029c340cfbb3ac0a71798100ccc13b97dddf373a4ae56b6a72cf70dfd53bc8", - "sha256:e512d8ef5ad7b898cdb2d8ee1cb09a8339e4f8be706d27eaa180c2f177248a10", - "sha256:e8e5b509d5c2ff12f8418006d5a90e9436766133b564db0abaec92fd27fcee29", - "sha256:ee54ff27bf0afaf4c3b3a62bcd016c12c3fdb4ec4f413391a90bd38bc3624605", - "sha256:fa4537fb4a98fe8fde99626e4681cc644bdcf2a795038533f9f711513a862ae6", - "sha256:fd45ff9293d9274c5008a2054ecef86a9bfe819a67c7be1afb65e69b405b3042" + "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" ], - "version": "==2021.4.4" - }, - "sniffio": { - "hashes": [ - "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663", - "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de" - ], - "markers": "python_version >= '3.5'", - "version": "==1.2.0" - }, - "sortedcontainers": { - "hashes": [ - "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", - "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0" - ], - "version": "==2.4.0" + "version": "==2022.1.18" }, "toml": { "hashes": [ @@ -874,57 +1079,50 @@ "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.10.2" }, - "trio": { + "tomli": { "hashes": [ - "sha256:451ddb27b4e5215e00646fcbb8028d341fccf284e053dc376506a14bb133dbcf", - "sha256:df067dd0560c321af39d412cd81fc3a7d13f55af9150527daab980683e9fcf3c" + "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", + "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" ], - "index": "pypi", - "version": "==0.16.0" + "version": "==2.0.1" }, "typed-ast": { "hashes": [ - "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace", - "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff", - "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266", - "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528", - "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6", - "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808", - "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4", - "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363", - "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341", - "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04", - "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41", - "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e", - "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3", - "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899", - "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805", - "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c", - "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c", - "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39", - "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a", - "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3", - "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7", - "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f", - "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075", - "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0", - "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40", - "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428", - "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927", - "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3", - "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f", - "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65" + "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" ], - "version": "==1.4.3" + "markers": "python_version >= '3.6'", + "version": "==1.5.2" }, "typing-extensions": { "hashes": [ - "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", - "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", - "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" + "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42", + "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2" ], "index": "pypi", - "version": "==3.10.0.0" + "version": "==4.1.1" } } } diff --git a/README.md b/README.md index bc700bfd..020f617c 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,6 @@ LNbits ====== -[![github-tests-badge]][github-tests] -[![github-mypy-badge]][github-mypy] -[![codecov-badge]][codecov] [![license-badge]](LICENSE) [![docs-badge]][docs] @@ -12,6 +9,10 @@ LNbits # LNbits v0.3 BETA, free and open-source lightning-network wallet/accounts system +(Join us on [https://t.me/lnbits](https://t.me/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! LNbits is a very simple Python server that sits on top of any funding source, and can be used as: @@ -32,11 +33,7 @@ LNbits is inspired by all the great work of [opennode.com](https://www.opennode. ## Running LNbits -See the [install guide](docs/guide/installation.md) for details on installation and setup. - -### Contributing to LNbits - -There's a [slightly different setup](docs/devs/installation.md) if you want to contribute to LNbits, but if your changes don't require adding or removing any package dependencies you don't have to bother with that, just follow the [normal installation](docs/guide/installation.md) steps. +See the [install guide](docs/devs/installation.md) for details on installation and setup. ## LNbits as an account system diff --git a/conv.py b/conv.py new file mode 100644 index 00000000..159c7dc0 --- /dev/null +++ b/conv.py @@ -0,0 +1,657 @@ +import psycopg2 +import sqlite3 +import os +# Python script to migrate an LNbits SQLite DB to Postgres +# All credits to @Fritz446 for the awesome work + + +# pip install psycopg2 OR psycopg2-binary + + +# Change these values as needed + +sqfolder = "data/" +pgdb = "lnbits" +pguser = "postgres" +pgpswd = "yourpassword" +pghost = "localhost" +pgport = "5432" +pgschema = "" + + +def get_sqlite_cursor(sqdb) -> sqlite3: + consq = sqlite3.connect(sqdb) + return consq.cursor() + + +def get_postgres_cursor(): + conpg = psycopg2.connect( + database=pgdb, user=pguser, password=pgpswd, host=pghost, port=pgport + ) + return conpg.cursor() + + +def check_db_versions(sqdb): + sqlite = get_sqlite_cursor(sqdb) + dblite = dict(sqlite.execute("SELECT * FROM dbversions;").fetchall()) + if "lnurlpos" in dblite: + del dblite["lnurlpos"] + sqlite.close() + + postgres = get_postgres_cursor() + postgres.execute("SELECT * FROM public.dbversions;") + dbpost = dict(postgres.fetchall()) + + for key in dblite.keys(): + if key in dblite and key in dbpost and dblite[key] != dbpost[key]: + raise Exception( + f"sqlite database version ({dblite[key]}) of {key} doesn't match postgres database version {dbpost[key]}" + ) + + connection = postgres.connection + postgres.close() + connection.close() + + print("Database versions OK, converting") + + +def fix_id(seq, values): + if not values or len(values) == 0: + return + + postgres = get_postgres_cursor() + + max_id = values[len(values) - 1][0] + postgres.execute(f"SELECT setval('{seq}', {max_id});") + + connection = postgres.connection + postgres.close() + connection.close() + + +def insert_to_pg(query, data): + if len(data) == 0: + return + + cursor = get_postgres_cursor() + connection = cursor.connection + + for d in data: + try: + cursor.execute(query, d) + except: + raise ValueError(f"Failed to insert {d}") + connection.commit() + + cursor.close() + connection.close() + + +def migrate_core(sqlite_db_file): + sq = get_sqlite_cursor(sqlite_db_file) + + # ACCOUNTS + res = sq.execute("SELECT * FROM accounts;") + q = f"INSERT INTO public.accounts (id, email, pass) VALUES (%s, %s, %s);" + insert_to_pg(q, res.fetchall()) + + # WALLETS + res = sq.execute("SELECT * FROM wallets;") + q = f'INSERT INTO public.wallets (id, name, "user", adminkey, inkey) VALUES (%s, %s, %s, %s, %s);' + insert_to_pg(q, res.fetchall()) + + # API PAYMENTS + res = sq.execute("SELECT * FROM apipayments;") + q = f""" + INSERT INTO public.apipayments( + checking_id, amount, fee, wallet, pending, memo, "time", hash, preimage, bolt11, extra, webhook, webhook_status) + VALUES (%s, %s, %s, %s, %s::boolean, %s, to_timestamp(%s), %s, %s, %s, %s, %s, %s); + """ + insert_to_pg(q, res.fetchall()) + + # BALANCE CHECK + res = sq.execute("SELECT * FROM balance_check;") + q = f"INSERT INTO public.balance_check(wallet, service, url) VALUES (%s, %s, %s);" + insert_to_pg(q, res.fetchall()) + + # BALANCE NOTIFY + res = sq.execute("SELECT * FROM balance_notify;") + q = f"INSERT INTO public.balance_notify(wallet, url) VALUES (%s, %s);" + insert_to_pg(q, res.fetchall()) + + # EXTENSIONS + res = sq.execute("SELECT * FROM extensions;") + q = f'INSERT INTO public.extensions("user", extension, active) VALUES (%s, %s, %s::boolean);' + insert_to_pg(q, res.fetchall()) + + print("Migrated: core") + + +def migrate_ext(sqlite_db_file, schema): + sq = get_sqlite_cursor(sqlite_db_file) + + if schema == "bleskomat": + # BLESKOMAT LNURLS + res = sq.execute("SELECT * FROM bleskomat_lnurls;") + q = f""" + INSERT INTO bleskomat.bleskomat_lnurls( + id, bleskomat, wallet, hash, tag, params, api_key_id, initial_uses, remaining_uses, created_time, updated_time) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s); + """ + insert_to_pg(q, res.fetchall()) + + # BLESKOMATS + res = sq.execute("SELECT * FROM bleskomats;") + q = f""" + INSERT INTO bleskomat.bleskomats( + id, wallet, api_key_id, api_key_secret, api_key_encoding, name, fiat_currency, exchange_rate_provider, fee) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s); + """ + insert_to_pg(q, res.fetchall()) + elif schema == "captcha": + # CAPTCHA + res = sq.execute("SELECT * FROM captchas;") + q = f""" + INSERT INTO captcha.captchas( + id, wallet, url, memo, description, amount, "time", remembers, extras) + VALUES (%s, %s, %s, %s, %s, %s, to_timestamp(%s), %s, %s); + """ + insert_to_pg(q, res.fetchall()) + elif schema == "copilot": + # OLD COPILOTS + res = sq.execute("SELECT * FROM copilots;") + q = f""" + INSERT INTO copilot.copilots( + id, "user", title, lnurl_toggle, wallet, animation1, animation2, animation3, animation1threshold, animation2threshold, animation3threshold, animation1webhook, animation2webhook, animation3webhook, lnurl_title, show_message, show_ack, show_price, amount_made, fullscreen_cam, iframe_url, "timestamp") + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, to_timestamp(%s)); + """ + insert_to_pg(q, res.fetchall()) + + # NEW COPILOTS + q = f""" + INSERT INTO copilot.newer_copilots( + id, "user", title, lnurl_toggle, wallet, animation1, animation2, animation3, animation1threshold, animation2threshold, animation3threshold, animation1webhook, animation2webhook, animation3webhook, lnurl_title, show_message, show_ack, show_price, amount_made, fullscreen_cam, iframe_url, "timestamp") + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, to_timestamp(%s)); + """ + insert_to_pg(q, res.fetchall()) + elif schema == "events": + # EVENTS + res = sq.execute("SELECT * FROM events;") + q = f""" + INSERT INTO events.events( + id, wallet, name, info, closing_date, event_start_date, event_end_date, amount_tickets, price_per_ticket, sold, "time") + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, to_timestamp(%s)); + """ + insert_to_pg(q, res.fetchall()) + # EVENT TICKETS + res = sq.execute("SELECT * FROM ticket;") + q = f""" + INSERT INTO events.ticket( + id, wallet, event, name, email, registered, paid, "time") + VALUES (%s, %s, %s, %s, %s, %s::boolean, %s::boolean, to_timestamp(%s)); + """ + insert_to_pg(q, res.fetchall()) + elif schema == "example": + # Example doesn't have a database at the moment + pass + elif schema == "hivemind": + # Hivemind doesn't have a database at the moment + pass + elif schema == "jukebox": + # JUKEBOXES + res = sq.execute("SELECT * FROM jukebox;") + q = f""" + INSERT INTO jukebox.jukebox( + id, "user", title, wallet, inkey, sp_user, sp_secret, sp_access_token, sp_refresh_token, sp_device, sp_playlists, price, profit) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s); + """ + insert_to_pg(q, res.fetchall()) + # JUKEBOX PAYMENTS + res = sq.execute("SELECT * FROM jukebox_payment;") + q = f""" + INSERT INTO jukebox.jukebox_payment( + payment_hash, juke_id, song_id, paid) + VALUES (%s, %s, %s, %s::boolean); + """ + insert_to_pg(q, res.fetchall()) + elif schema == "withdraw": + # WITHDRAW LINK + res = sq.execute("SELECT * FROM withdraw_link;") + q = f""" + INSERT INTO withdraw.withdraw_link ( + id, + wallet, + title, + min_withdrawable, + max_withdrawable, + uses, + wait_time, + is_unique, + unique_hash, + k1, + open_time, + used, + usescsv + ) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s); + """ + insert_to_pg(q, res.fetchall()) + # WITHDRAW HASH CHECK + res = sq.execute("SELECT * FROM hash_check;") + q = f""" + INSERT INTO withdraw.hash_check (id, lnurl_id) + VALUES (%s, %s); + """ + insert_to_pg(q, res.fetchall()) + elif schema == "watchonly": + # WALLETS + res = sq.execute("SELECT * FROM wallets;") + q = f""" + INSERT INTO watchonly.wallets ( + id, + "user", + masterpub, + title, + address_no, + balance + ) + VALUES (%s, %s, %s, %s, %s, %s); + """ + insert_to_pg(q, res.fetchall()) + # ADDRESSES + res = sq.execute("SELECT * FROM addresses;") + q = f""" + INSERT INTO watchonly.addresses (id, address, wallet, amount) + VALUES (%s, %s, %s, %s); + """ + insert_to_pg(q, res.fetchall()) + # MEMPOOL + res = sq.execute("SELECT * FROM mempool;") + q = f""" + INSERT INTO watchonly.mempool ("user", endpoint) + VALUES (%s, %s); + """ + insert_to_pg(q, res.fetchall()) + elif schema == "usermanager": + # USERS + res = sq.execute("SELECT * FROM users;") + q = f""" + INSERT INTO usermanager.users (id, name, admin, email, password) + VALUES (%s, %s, %s, %s, %s); + """ + insert_to_pg(q, res.fetchall()) + # WALLETS + res = sq.execute("SELECT * FROM wallets;") + q = f""" + INSERT INTO usermanager.wallets (id, admin, name, "user", adminkey, inkey) + VALUES (%s, %s, %s, %s, %s, %s); + """ + insert_to_pg(q, res.fetchall()) + elif schema == "tpos": + # TPOSS + res = sq.execute("SELECT * FROM tposs;") + q = f""" + INSERT INTO tpos.tposs (id, wallet, name, currency) + VALUES (%s, %s, %s, %s); + """ + insert_to_pg(q, res.fetchall()) + elif schema == "tipjar": + # TIPJARS + res = sq.execute("SELECT * FROM TipJars;") + q = f""" + INSERT INTO tipjar.TipJars (id, name, wallet, onchain, webhook) + VALUES (%s, %s, %s, %s, %s); + """ + pay_links = res.fetchall() + insert_to_pg(q, pay_links) + fix_id("tipjar.tipjars_id_seq", pay_links) + # TIPS + res = sq.execute("SELECT * FROM Tips;") + q = f""" + INSERT INTO tipjar.Tips (id, wallet, name, message, sats, tipjar) + VALUES (%s, %s, %s, %s, %s, %s); + """ + insert_to_pg(q, res.fetchall()) + elif schema == "subdomains": + # DOMAIN + res = sq.execute("SELECT * FROM domain;") + q = f""" + INSERT INTO subdomains.domain ( + id, + wallet, + domain, + webhook, + cf_token, + cf_zone_id, + description, + cost, + amountmade, + allowed_record_types, + time + ) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, to_timestamp(%s)); + """ + insert_to_pg(q, res.fetchall()) + # SUBDOMAIN + res = sq.execute("SELECT * FROM subdomain;") + q = f""" + INSERT INTO subdomains.subdomain ( + id, + domain, + email, + subdomain, + ip, + wallet, + sats, + duration, + paid, + record_type, + time + ) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s::boolean, %s, to_timestamp(%s)); + """ + insert_to_pg(q, res.fetchall()) + elif schema == "streamalerts": + # SERVICES + res = sq.execute("SELECT * FROM Services;") + q = f""" + INSERT INTO streamalerts.Services ( + id, + state, + twitchuser, + client_id, + client_secret, + wallet, + onchain, + servicename, + authenticated, + token + ) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s::boolean, %s); + """ + services = res.fetchall() + insert_to_pg(q, services) + fix_id("streamalerts.services_id_seq", services) + # DONATIONS + res = sq.execute("SELECT * FROM Donations;") + q = f""" + INSERT INTO streamalerts.Donations ( + id, + wallet, + name, + message, + cur_code, + sats, + amount, + service, + posted, + ) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s::boolean); + """ + insert_to_pg(q, res.fetchall()) + elif schema == "splitpayments": + # TARGETS + res = sq.execute("SELECT * FROM targets;") + q = f""" + INSERT INTO splitpayments.targets (wallet, source, percent, alias) + VALUES (%s, %s, %s, %s); + """ + insert_to_pg(q, res.fetchall()) + elif schema == "satspay": + # CHARGES + res = sq.execute("SELECT * FROM charges;") + q = f""" + INSERT INTO satspay.charges ( + id, + "user", + description, + onchainwallet, + onchainaddress, + lnbitswallet, + payment_request, + payment_hash, + webhook, + completelink, + completelinktext, + time, + amount, + balance, + timestamp + ) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, to_timestamp(%s)); + """ + insert_to_pg(q, res.fetchall()) + elif schema == "satsdice": + # SATSDICE PAY + res = sq.execute("SELECT * FROM satsdice_pay;") + q = f""" + INSERT INTO satsdice.satsdice_pay ( + id, + wallet, + title, + min_bet, + max_bet, + amount, + served_meta, + served_pr, + multiplier, + haircut, + chance, + base_url, + open_time + ) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s); + """ + insert_to_pg(q, res.fetchall()) + # SATSDICE WITHDRAW + res = sq.execute("SELECT * FROM satsdice_withdraw;") + q = f""" + INSERT INTO satsdice.satsdice_withdraw ( + id, + satsdice_pay, + value, + unique_hash, + k1, + open_time, + used + ) + VALUES (%s, %s, %s, %s, %s, %s, %s); + """ + insert_to_pg(q, res.fetchall()) + # SATSDICE PAYMENT + res = sq.execute("SELECT * FROM satsdice_payment;") + q = f""" + INSERT INTO satsdice.satsdice_payment ( + payment_hash, + satsdice_pay, + value, + paid, + lost + ) + VALUES (%s, %s, %s, %s::boolean, %s::boolean); + """ + insert_to_pg(q, res.fetchall()) + # SATSDICE HASH CHECK + res = sq.execute("SELECT * FROM hash_checkw;") + q = f""" + INSERT INTO satsdice.hash_checkw (id, lnurl_id) + VALUES (%s, %s); + """ + insert_to_pg(q, res.fetchall()) + elif schema == "paywall": + # PAYWALLS + res = sq.execute("SELECT * FROM paywalls;") + q = f""" + INSERT INTO paywall.paywalls( + id, + wallet, + url, + memo, + amount, + time, + remembers, + extra + ) + VALUES (%s, %s, %s, %s, %s, to_timestamp(%s), %s, %s); + """ + insert_to_pg(q, res.fetchall()) + elif schema == "offlineshop": + # SHOPS + res = sq.execute("SELECT * FROM shops;") + q = f""" + INSERT INTO offlineshop.shops (id, wallet, method, wordlist) + VALUES (%s, %s, %s, %s); + """ + shops = res.fetchall() + insert_to_pg(q, shops) + fix_id("offlineshop.shops_id_seq", shops) + # ITEMS + res = sq.execute("SELECT * FROM items;") + q = f""" + INSERT INTO offlineshop.items (shop, id, name, description, image, enabled, price, unit) + VALUES (%s, %s, %s, %s, %s, %s::boolean, %s, %s); + """ + items = res.fetchall() + insert_to_pg(q, items) + fix_id("offlineshop.items_id_seq", items) + elif schema == "lnurlpos": + # LNURLPOSS + res = sq.execute("SELECT * FROM lnurlposs;") + q = f""" + INSERT INTO lnurlpos.lnurlposs (id, key, title, wallet, currency, timestamp) + VALUES (%s, %s, %s, %s, %s, to_timestamp(%s)); + """ + insert_to_pg(q, res.fetchall()) + # LNURLPOS PAYMENT + res = sq.execute("SELECT * FROM lnurlpospayment;") + q = f""" + INSERT INTO lnurlpos.lnurlpospayment (id, posid, payhash, payload, pin, sats, timestamp) + VALUES (%s, %s, %s, %s, %s, %s, to_timestamp(%s)); + """ + insert_to_pg(q, res.fetchall()) + elif schema == "lnurlp": + # PAY LINKS + res = sq.execute("SELECT * FROM pay_links;") + q = f""" + INSERT INTO lnurlp.pay_links ( + id, + wallet, + description, + min, + served_meta, + served_pr, + webhook_url, + success_text, + success_url, + currency, + comment_chars, + max + ) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s); + """ + pay_links = res.fetchall() + insert_to_pg(q, pay_links) + fix_id("lnurlp.pay_links_id_seq", pay_links) + elif schema == "lndhub": + # LndHub doesn't have a database at the moment + pass + elif schema == "lnticket": + # TICKET + res = sq.execute("SELECT * FROM ticket;") + q = f""" + INSERT INTO lnticket.ticket ( + id, + form, + email, + ltext, + name, + wallet, + sats, + paid, + time + ) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s::boolean, to_timestamp(%s)); + """ + insert_to_pg(q, res.fetchall()) + # FORM + res = sq.execute("SELECT * FROM form2;") + q = f""" + INSERT INTO lnticket.form2 ( + id, + wallet, + name, + webhook, + description, + flatrate, + amount, + amountmade, + time + ) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, to_timestamp(%s)); + """ + insert_to_pg(q, res.fetchall()) + elif schema == "livestream": + # LIVESTREAMS + res = sq.execute("SELECT * FROM livestreams;") + q = f""" + INSERT INTO livestream.livestreams ( + id, + wallet, + fee_pct, + current_track + ) + VALUES (%s, %s, %s, %s); + """ + livestreams = res.fetchall() + insert_to_pg(q, livestreams) + fix_id("livestream.livestreams_id_seq", livestreams) + # PRODUCERS + res = sq.execute("SELECT * FROM producers;") + q = f""" + INSERT INTO livestream.producers ( + livestream, + id, + "user", + wallet, + name + ) + VALUES (%s, %s, %s, %s, %s); + """ + producers = res.fetchall() + insert_to_pg(q, producers) + fix_id("livestream.producers_id_seq", producers) + # TRACKS + res = sq.execute("SELECT * FROM tracks;") + q = f""" + INSERT INTO livestream.tracks ( + livestream, + id, + download_url, + price_msat, + name, + producer + ) + VALUES (%s, %s, %s, %s, %s, %s); + """ + tracks = res.fetchall() + insert_to_pg(q, tracks) + fix_id("livestream.tracks_id_seq", tracks) + else: + print(f"Not implemented: {schema}") + sq.close() + return + + print(f"Migrated: {schema}") + sq.close() + + +check_db_versions("data/database.sqlite3") +migrate_core("data/database.sqlite3") + +files = os.listdir(sqfolder) +for file in files: + path = f"data/{file}" + if file.startswith("ext_"): + schema = file.replace("ext_", "").split(".")[0] + print(f"Migrating: {schema}") + migrate_ext(path, schema) diff --git a/docs/devs/development.md b/docs/devs/development.md index 5a8cd214..85346d16 100644 --- a/docs/devs/development.md +++ b/docs/devs/development.md @@ -10,3 +10,17 @@ For developers ============== Thanks for contributing :) + + +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 +``` + +Then to run the tests: +```bash +make test +``` diff --git a/docs/devs/installation.md b/docs/devs/installation.md index 013f7be9..cbf234cc 100644 --- a/docs/devs/installation.md +++ b/docs/devs/installation.md @@ -7,64 +7,46 @@ nav_order: 1 # Installation -Download the latest stable release https://github.com/lnbits/lnbits/releases - -## Application dependencies - -The application uses [Pipenv][pipenv] to manage Python packages. -While in development, you will need to install all dependencies: +LNbits uses [Pipenv][pipenv] to manage Python packages. ```sh -$ pipenv shell -$ pipenv install --dev -``` +git clone https://github.com/lnbits/lnbits-legend.git +cd lnbits-legend/ -If any of the modules fails to install, try checking and upgrading your setupTool module. -`pip install -U setuptools` +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 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 -```sh -$ pipenv --python 3.8 install --dev -``` +# install libffi/libpq in case "pipenv install" fails +# sudo apt-get install -y libffi-dev libpq-dev +``` +## Running the server -You will need to copy `.env.example` to `.env`, then set variables there. +Create the data folder and edit the .env file: -![Files](https://i.imgur.com/ri2zOe8.png) + 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. -## Running the server +**Notes**: -LNbits uses [Quart][quart] as an application server. -Before running the server for the first time, make sure to create the data folder: - -```sh -$ mkdir data -``` - -To then run the server, use: - -```sh -$ pipenv run python -m lnbits -``` - -**Note**: You'll need to use _https_ for some endpoints and/or extensions. You can use [ngrok](https://ngrok.com/) for that. Follow the installation instructions on the website and when it's all set you can run: - -```sh -$ ./nrok http 5000 -``` - -this will give you an _https_ tunnel for the _localhost_, use that URL for navigating to LNBits. - -## Frontend - -The frontend uses [Vue.js and Quasar][quasar]. - -[quart]: https://pgjones.gitlab.io/ -[pipenv]: https://pipenv.pypa.io/ -[polar]: https://lightningpolar.com/ -[quasar]: https://quasar.dev/start/how-to-use-vue +* 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/). +* Screen works well if you want LNbits to continue running when you close your terminal session. diff --git a/docs/guide/fastapi_transition.md b/docs/guide/fastapi_transition.md new file mode 100644 index 00000000..7a367004 --- /dev/null +++ b/docs/guide/fastapi_transition.md @@ -0,0 +1,122 @@ + +## Defining a route with path parameters +**old:** +```python +# with <> +@offlineshop_ext.route("/lnurl/", methods=["GET"]) +``` + +**new:** +```python +# with curly braces: {} +@offlineshop_ext.get("/lnurl/{item_id}") +``` + +## Check if a user exists and access user object +**old:** +```python +# decorators +@check_user_exists() +async def do_routing_stuff(): + pass +``` + +**new:** +If user doesn't exist, `Depends(check_user_exists)` will raise an exception. +If user exists, `user` will be the user object +```python +# depends calls +@core_html_routes.get("/my_route") +async def extensions(user: User = Depends(check_user_exists)): + pass +``` +## Returning data from API calls +**old:** +```python +return ( + { + "id": wallet.wallet.id, + "name": wallet.wallet.name, + "balance": wallet.wallet.balance_msat + }, + HTTPStatus.OK, +) +``` +FastAPI returns `HTTPStatus.OK` by default id no Exception is raised + +**new:** +```python +return { + "id": wallet.wallet.id, + "name": wallet.wallet.name, + "balance": wallet.wallet.balance_msat +} +``` + +To change the default HTTPStatus, add it to the path decorator +```python +@core_app.post("/api/v1/payments", status_code=HTTPStatus.CREATED) +async def payments(): + pass +``` + +## Raise exceptions +**old:** +```python +return ( + {"message": f"Failed to connect to {domain}."}, + HTTPStatus.BAD_REQUEST, +) +# or the Quart way via abort function +abort(HTTPStatus.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.") +``` + +**new:** + +Raise an exception to return a status code other than the default status code. +```python +raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=f"Failed to connect to {domain}." +) +``` + +## Extensions +**old:** +```python +from quart import Blueprint + +amilk_ext: Blueprint = Blueprint( + "amilk", __name__, static_folder="static", template_folder="templates" +) +``` + +**new:** +```python +from fastapi import APIRouter +from lnbits.jinja2_templating import Jinja2Templates +from lnbits.helpers import template_renderer +from fastapi.staticfiles import StaticFiles + +offlineshop_ext: APIRouter = APIRouter( + prefix="/Extension", + tags=["Offlineshop"] +) + +offlineshop_ext.mount( + "lnbits/extensions/offlineshop/static", + StaticFiles("lnbits/extensions/offlineshop/static") +) + +offlineshop_rndr = template_renderer([ + "lnbits/extensions/offlineshop/templates", +]) +``` + +## Possible optimizations +### Use Redis as a cache server +Instead of hitting the database over and over again, we can store a short lived object in [Redis](https://redis.io) for an arbitrary key. +Example: +* Get transactions for a wallet ID +* User data for a user id +* Wallet data for a Admin / Invoice key \ No newline at end of file diff --git a/docs/guide/installation.md b/docs/guide/installation.md index de309086..2806a4f5 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -4,57 +4,142 @@ title: Basic installation nav_order: 2 --- +# Basic installation +Install Postgres and setup a database for LNbits: -Basic installation -================== +```sh +# on debian/ubuntu 'sudo apt-get -y install postgresql' +# or follow instructions at https://www.postgresql.org/download/linux/ + +# Postgres doesn't have a default password, so we'll create one. +sudo -i -u postgres +psql +# on psql +ALTER USER postgres PASSWORD 'myPassword'; # choose whatever password you want +\q +# on postgres user +createdb lnbits +exit +``` Download this repo and install the dependencies: ```sh -git clone https://github.com/lnbits/lnbits.git -cd lnbits/ +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 -mkdir data -./venv/bin/quart assets -./venv/bin/quart migrate -./venv/bin/hypercorn -k trio --bind 0.0.0.0:5000 'lnbits.app:create_app()' +# 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 ``` -No you can visit your LNbits at http://localhost:5000/. +Now you can visit your LNbits at http://localhost:5000/. Now modify the `.env` file with any settings you prefer and add a proper [funding source](./wallets.md) by modifying the value of `LNBITS_BACKEND_WALLET_CLASS` and providing the extra information and credentials related to the chosen funding source. -Then you can run restart it and it will be using the new settings. +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. -Docker installation -=================== +## 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. + + + +# Additional guides + +### LNbits as a systemd service + +Systemd is great for taking care of your LNbits instance. It will start it on boot and restart it in case it crashes. If you want to run LNbits as a systemd service on your Debian/Ubuntu/Raspbian server, create a file at `/etc/systemd/system/lnbits.service` with the following content: + +``` +# Systemd unit for lnbits +# /etc/systemd/system/lnbits.service + +[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) + +[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 +Restart=always +TimeoutSec=120 +RestartSec=30 +Environment=PYTHONUNBUFFERED=1 # this makes sure that you receive logs in real time + +[Install] +WantedBy=multi-user.target +``` + +Save the file and run the following commands: + +```sh +sudo systemctl enable lnbits.service +sudo systemctl start lnbits.service +``` + +### 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 To install using docker you first need to build the docker image as: + ``` git clone https://github.com/lnbits/lnbits.git -cd lnbits/ # ${PWD} refered as +cd lnbits/ # ${PWD} referred as docker build -t lnbits . ``` You can launch the docker in a different directory, but make sure to copy `.env.example` from lnbits there + ``` cp /.env.example .env ``` + and change the configuration in `.env` as required. Then create the data directory for the user ID 1000, which is the user that runs the lnbits within the docker container. + ``` mkdir data sudo chown 1000:1000 ./data/ ``` Then the image can be run as: + ``` docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits ``` -Finally you can access the lnbits on your machine port 5000. + +Finally you can access your lnbits on your machine at port 5000. + +# Additional guides + +## LNbits running on Umbrel behind Tor + +If you want to run LNbits on your Umbrel but want it to be reached through clearnet, _Uxellodunum_ made an extensive [guide](https://community.getumbrel.com/t/guide-lnbits-without-tor/604) on how to do it. diff --git a/docs/guide/wallets.md b/docs/guide/wallets.md index 888fc8d6..7a3b6a27 100644 --- a/docs/guide/wallets.md +++ b/docs/guide/wallets.md @@ -17,6 +17,7 @@ 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 @@ -29,22 +30,30 @@ Using this wallet requires the installation of the `pylightning` Python package. ### LND (gRPC) -Using this wallet requires the installation of the `lndgrpc` and `purerpc` Python packages. +Using this wallet requires the installation of the `grpcio` and `protobuf` Python packages. - `LNBITS_BACKEND_WALLET_CLASS`: **LndWallet** - `LND_GRPC_ENDPOINT`: ip_address - `LND_GRPC_PORT`: port - `LND_GRPC_CERT`: /file/path/tls.cert -- `LND_GRPC_MACAROON`: /file/path/admin.macaroon +- `LND_GRPC_MACAROON`: /file/path/admin.macaroon or Bech64/Hex +You can also use an AES-encrypted macaroon (more info) instead by using + +- `LND_GRPC_MACAROON_ENCRYPTED`: eNcRyPtEdMaCaRoOn + +To encrypt your macaroon, run `./venv/bin/python lnbits/wallets/macaroon/macaroon.py`. ### LND (REST) - `LNBITS_BACKEND_WALLET_CLASS`: **LndRestWallet** -- `LND_REST_ENDPOINT`: ip_address +- `LND_REST_ENDPOINT`: http://10.147.17.230:8080/ - `LND_REST_CERT`: /file/path/tls.cert -- `LND_GRPC_MACAROON`: /file/path/admin.macaroon +- `LND_REST_MACAROON`: /file/path/admin.macaroon or Bech64/Hex +or + +- `LND_REST_MACAROON_ENCRYPTED`: eNcRyPtEdMaCaRoOn ### LNbits @@ -65,7 +74,7 @@ For the invoice listener to work you have a publicly accessible URL in your LNbi ### lntxbot - `LNBITS_BACKEND_WALLET_CLASS`: **LntxbotWallet** -- `LNTXBOT_API_ENDPOINT`: https://lntxbot.bigsun.xyz/ +- `LNTXBOT_API_ENDPOINT`: https://lntxbot.com/ - `LNTXBOT_KEY`: lntxbotAdminApiKey diff --git a/lnbits/__main__.py b/lnbits/__main__.py index 90b08642..8461eb42 100644 --- a/lnbits/__main__.py +++ b/lnbits/__main__.py @@ -1,8 +1,22 @@ -import trio +import asyncio -from .commands import migrate_databases, transpile_scss, bundle_vendored +import uvloop +from starlette.requests import Request -trio.run(migrate_databases) +from .commands import bundle_vendored, migrate_databases, transpile_scss +from .settings import ( + DEBUG, + LNBITS_COMMIT, + LNBITS_DATA_FOLDER, + LNBITS_SITE_TITLE, + PORT, + SERVICE_FEE, + WALLET, +) + +uvloop.install() + +asyncio.create_task(migrate_databases()) transpile_scss() bundle_vendored() @@ -10,15 +24,6 @@ from .app import create_app app = create_app() -from .settings import ( - LNBITS_SITE_TITLE, - SERVICE_FEE, - DEBUG, - LNBITS_DATA_FOLDER, - WALLET, - LNBITS_COMMIT, -) - print( f"""Starting LNbits with - git version: {LNBITS_COMMIT} @@ -29,5 +34,3 @@ print( - service fee: {SERVICE_FEE} """ ) - -app.run(host=app.config["HOST"], port=app.config["PORT"]) diff --git a/lnbits/app.py b/lnbits/app.py index 35852cd9..ca770167 100644 --- a/lnbits/app.py +++ b/lnbits/app.py @@ -1,156 +1,176 @@ -import sys -import warnings +import asyncio import importlib +import sys import traceback +import warnings -from quart import g -from quart_trio import QuartTrio -from quart_cors import cors # type: ignore -from quart_compress import Compress # type: ignore -from secure import SecureHeaders # type: ignore +from fastapi import FastAPI, Request +from fastapi.exceptions import RequestValidationError +from fastapi.middleware.cors import CORSMiddleware +from fastapi.middleware.gzip import GZipMiddleware +from fastapi.staticfiles import StaticFiles + +import lnbits.settings +from lnbits.core.tasks import register_task_listeners from .commands import db_migrate, handle_assets from .core import core_app +from .core.views.generic import core_html_routes from .helpers import ( - get_valid_extensions, - get_js_vendored, get_css_vendored, + get_js_vendored, + get_valid_extensions, + template_renderer, url_for_vendored, ) -from .proxy_fix import ASGIProxyFix +from .requestvars import g +from .settings import WALLET from .tasks import ( - run_deferred_async, + catch_everything_and_restart, check_pending_payments, - invoice_listener, internal_invoice_listener, + invoice_listener, + run_deferred_async, webhook_handler, ) -from .settings import WALLET - -secure_headers = SecureHeaders(hsts=False, xfo=False) -def create_app(config_object="lnbits.settings") -> QuartTrio: +def create_app(config_object="lnbits.settings") -> FastAPI: """Create application factory. :param config_object: The configuration object to use. """ - app = QuartTrio(__name__, static_folder="static") - app.config.from_object(config_object) - app.asgi_http_class = ASGIProxyFix + app = FastAPI() + app.mount("/static", StaticFiles(directory="lnbits/static"), name="static") + app.mount( + "/core/static", StaticFiles(directory="lnbits/core/static"), name="core_static" + ) - cors(app) - Compress(app) + origins = ["*"] + + app.add_middleware( + CORSMiddleware, allow_origins=origins, allow_methods=["*"], allow_headers=["*"] + ) + + g().config = lnbits.settings + g().base_url = f"http://{lnbits.settings.HOST}:{lnbits.settings.PORT}" + + @app.exception_handler(RequestValidationError) + 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."}, + ) + + # return HTMLResponse( + # status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + # content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}), + # ) + + app.add_middleware(GZipMiddleware, minimum_size=1000) + # app.add_middleware(ASGIProxyFix) check_funding_source(app) register_assets(app) - register_blueprints(app) - register_filters(app) - register_commands(app) - register_request_hooks(app) + register_routes(app) + # register_commands(app) register_async_tasks(app) register_exception_handlers(app) return app -def check_funding_source(app: QuartTrio) -> None: - @app.before_serving +def check_funding_source(app: FastAPI) -> None: + @app.on_event("startup") async def check_wallet_status(): - error_message, balance = await WALLET.status() - if error_message: + while True: + 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}'", RuntimeWarning, ) - - sys.exit(4) - else: - print( - f" ✔️ {WALLET.__class__.__name__} seems to be connected and with a balance of {balance} msat." - ) + print("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." + ) -def register_blueprints(app: QuartTrio) -> None: - """Register Flask blueprints / LNbits extensions.""" - app.register_blueprint(core_app) +def register_routes(app: FastAPI) -> None: + """Register FastAPI routes / LNbits extensions.""" + app.include_router(core_app) + app.include_router(core_html_routes) for ext in get_valid_extensions(): try: ext_module = importlib.import_module(f"lnbits.extensions.{ext.code}") - bp = getattr(ext_module, f"{ext.code}_ext") + ext_route = getattr(ext_module, f"{ext.code}_ext") - app.register_blueprint(bp, url_prefix=f"/{ext.code}") - except Exception: + if hasattr(ext_module, f"{ext.code}_start"): + ext_start_func = getattr(ext_module, f"{ext.code}_start") + ext_start_func() + + if hasattr(ext_module, f"{ext.code}_static_files"): + ext_statics = getattr(ext_module, f"{ext.code}_static_files") + for s in ext_statics: + app.mount(s["path"], s["app"], s["name"]) + + app.include_router(ext_route) + except Exception as e: + print(str(e)) raise ImportError( f"Please make sure that the extension `{ext.code}` follows conventions." ) -def register_commands(app: QuartTrio): +def register_commands(app: FastAPI): """Register Click commands.""" app.cli.add_command(db_migrate) app.cli.add_command(handle_assets) -def register_assets(app: QuartTrio): +def register_assets(app: FastAPI): """Serve each vendored asset separately or a bundle.""" - @app.before_request + @app.on_event("startup") async def vendored_assets_variable(): - if app.config["DEBUG"]: - g.VENDORED_JS = map(url_for_vendored, get_js_vendored()) - g.VENDORED_CSS = map(url_for_vendored, get_css_vendored()) + if g().config.DEBUG: + g().VENDORED_JS = map(url_for_vendored, get_js_vendored()) + g().VENDORED_CSS = map(url_for_vendored, get_css_vendored()) else: - g.VENDORED_JS = ["/static/bundle.js"] - g.VENDORED_CSS = ["/static/bundle.css"] - - -def register_filters(app: QuartTrio): - """Jinja filters.""" - app.jinja_env.globals["SITE_TITLE"] = app.config["LNBITS_SITE_TITLE"] - app.jinja_env.globals["LNBITS_VERSION"] = app.config["LNBITS_COMMIT"] - app.jinja_env.globals["EXTENSIONS"] = get_valid_extensions() - - -def register_request_hooks(app: QuartTrio): - """Open the core db for each request so everything happens in a big transaction""" - - @app.after_request - async def set_secure_headers(response): - secure_headers.quart(response) - return response + g().VENDORED_JS = ["/static/bundle.js"] + g().VENDORED_CSS = ["/static/bundle.css"] def register_async_tasks(app): - @app.route("/wallet/webhook", methods=["GET", "POST", "PUT", "PATCH", "DELETE"]) + @app.route("/wallet/webhook") async def webhook_listener(): return await webhook_handler() - @app.before_serving + @app.on_event("startup") async def listeners(): - run_deferred_async(app.nursery) - app.nursery.start_soon(check_pending_payments) - app.nursery.start_soon(invoice_listener, app.nursery) - app.nursery.start_soon(internal_invoice_listener, app.nursery) + loop = asyncio.get_event_loop() + loop.create_task(catch_everything_and_restart(check_pending_payments)) + loop.create_task(catch_everything_and_restart(invoice_listener)) + loop.create_task(catch_everything_and_restart(internal_invoice_listener)) + await register_task_listeners() + await run_deferred_async() - @app.after_serving + @app.on_event("shutdown") async def stop_listeners(): pass -def register_exception_handlers(app): - @app.errorhandler(Exception) - async def basic_error(err): - etype, value, tb = sys.exc_info() +def register_exception_handlers(app: FastAPI): + @app.exception_handler(Exception) + async def basic_error(request: Request, err): + print("handled error", traceback.format_exc()) + etype, _, tb = sys.exc_info() traceback.print_exception(etype, err, tb) exc = traceback.format_exc() - return ( - "\n\n".join( - [ - "LNbits internal error!", - exc, - "If you believe this shouldn't be an error please bring it up on https://t.me/lnbits", - ] - ), - 500, + return template_renderer().TemplateResponse( + "error.html", {"request": request, "err": err} ) diff --git a/lnbits/bolt11.py b/lnbits/bolt11.py index 6acc6db7..74f73963 100644 --- a/lnbits/bolt11.py +++ b/lnbits/bolt11.py @@ -2,10 +2,14 @@ import bitstring # type: ignore import re import hashlib from typing import List, NamedTuple, Optional -from bech32 import bech32_decode, CHARSET # type: ignore +from bech32 import bech32_encode, bech32_decode, CHARSET from ecdsa import SECP256k1, VerifyingKey # type: ignore from ecdsa.util import sigdecode_string # type: ignore from binascii import unhexlify +import time +from decimal import Decimal +import embit +import secp256k1 class Route(NamedTuple): @@ -116,6 +120,166 @@ def decode(pr: str) -> Invoice: return invoice +def encode(options): + """Convert options into LnAddr and pass it to the encoder""" + addr = LnAddr() + addr.currency = options["currency"] + addr.fallback = options["fallback"] if options["fallback"] else None + if options["amount"]: + addr.amount = options["amount"] + if options["timestamp"]: + addr.date = int(options["timestamp"]) + + addr.paymenthash = unhexlify(options["paymenthash"]) + + if options["description"]: + addr.tags.append(("d", options["description"])) + if options["description_hash"]: + addr.tags.append(("h", options["description_hash"])) + if options["expires"]: + addr.tags.append(("x", options["expires"])) + + if options["fallback"]: + addr.tags.append(("f", options["fallback"])) + if options["route"]: + for r in options["route"]: + splits = r.split("/") + route = [] + while len(splits) >= 5: + route.append( + ( + unhexlify(splits[0]), + unhexlify(splits[1]), + int(splits[2]), + int(splits[3]), + int(splits[4]), + ) + ) + splits = splits[5:] + assert len(splits) == 0 + addr.tags.append(("r", route)) + return lnencode(addr, options["privkey"]) + + +def lnencode(addr, privkey): + if addr.amount: + amount = Decimal(str(addr.amount)) + # We can only send down to millisatoshi. + if amount * 10 ** 12 % 10: + raise ValueError( + "Cannot encode {}: too many decimal places".format(addr.amount) + ) + + amount = addr.currency + shorten_amount(amount) + else: + amount = addr.currency if addr.currency else "" + + hrp = "ln" + amount + "0n" + + # Start with the timestamp + data = bitstring.pack("uint:35", addr.date) + + # Payment hash + data += tagged_bytes("p", addr.paymenthash) + tags_set = set() + + for k, v in addr.tags: + + # BOLT #11: + # + # A writer MUST NOT include more than one `d`, `h`, `n` or `x` fields, + if k in ("d", "h", "n", "x"): + if k in tags_set: + raise ValueError("Duplicate '{}' tag".format(k)) + + if k == "r": + route = bitstring.BitArray() + for step in v: + pubkey, channel, feebase, feerate, cltv = step + route.append( + bitstring.BitArray(pubkey) + + bitstring.BitArray(channel) + + bitstring.pack("intbe:32", feebase) + + bitstring.pack("intbe:32", feerate) + + bitstring.pack("intbe:16", cltv) + ) + data += tagged("r", route) + elif k == "f": + data += encode_fallback(v, addr.currency) + elif k == "d": + data += tagged_bytes("d", v.encode()) + elif k == "x": + # Get minimal length by trimming leading 5 bits at a time. + expirybits = bitstring.pack("intbe:64", v)[4:64] + while expirybits.startswith("0b00000"): + expirybits = expirybits[5:] + data += tagged("x", expirybits) + elif k == "h": + data += tagged_bytes("h", hashlib.sha256(v.encode("utf-8")).digest()) + elif k == "n": + data += tagged_bytes("n", v) + else: + # FIXME: Support unknown tags? + raise ValueError("Unknown tag {}".format(k)) + + tags_set.add(k) + + # BOLT #11: + # + # A writer MUST include either a `d` or `h` field, and MUST NOT include + # both. + if "d" in tags_set and "h" in tags_set: + raise ValueError("Cannot include both 'd' and 'h'") + if not "d" in tags_set and not "h" in tags_set: + raise ValueError("Must include either 'd' or 'h'") + + # We actually sign the hrp, then data (padded to 8 bits with zeroes). + privkey = secp256k1.PrivateKey(bytes(unhexlify(privkey))) + sig = privkey.ecdsa_sign_recoverable( + bytearray([ord(c) for c in hrp]) + data.tobytes() + ) + # This doesn't actually serialize, but returns a pair of values :( + sig, recid = privkey.ecdsa_recoverable_serialize(sig) + data += bytes(sig) + bytes([recid]) + + return bech32_encode(hrp, bitarray_to_u5(data)) + + +class LnAddr(object): + def __init__( + self, paymenthash=None, amount=None, currency="bc", tags=None, date=None + ): + self.date = int(time.time()) if not date else int(date) + self.tags = [] if not tags else tags + self.unknown_tags = [] + self.paymenthash = paymenthash + self.signature = None + self.pubkey = None + self.currency = currency + self.amount = amount + + def __str__(self): + return "LnAddr[{}, amount={}{} tags=[{}]]".format( + hexlify(self.pubkey.serialize()).decode("utf-8"), + self.amount, + self.currency, + ", ".join([k + "=" + str(v) for k, v in self.tags]), + ) + + +def shorten_amount(amount): + """Given an amount in bitcoin, shorten it""" + # Convert to pico initially + amount = int(amount * 10 ** 12) + units = ["p", "n", "u", "m", ""] + for unit in units: + if amount % 1000 == 0: + amount //= 1000 + else: + break + return str(amount) + unit + + def _unshorten_amount(amount: str) -> int: """Given a shortened amount, return millisatoshis""" # BOLT #11: @@ -125,12 +289,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: @@ -151,6 +310,34 @@ def _pull_tagged(stream): return (CHARSET[tag], stream.read(length * 5), stream) +def is_p2pkh(currency, prefix): + return prefix == base58_prefix_map[currency][0] + + +def is_p2sh(currency, prefix): + return prefix == base58_prefix_map[currency][1] + + +# Tagged field containing BitArray +def tagged(char, l): + # Tagged fields need to be zero-padded to 5 bits. + while l.len % 5 != 0: + l.append("0b0") + return ( + bitstring.pack( + "uint:5, uint:5, uint:5", + CHARSET.find(char), + (l.len / 5) / 32, + (l.len / 5) % 32, + ) + + l + ) + + +def tagged_bytes(char, l): + return tagged(char, bitstring.BitArray(l)) + + def _trim_to_bytes(barr): # Adds a byte if necessary. b = barr.tobytes() @@ -161,9 +348,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), ) @@ -172,3 +359,12 @@ def _u5_to_bitarray(arr: List[int]) -> bitstring.BitArray: for a in arr: ret += bitstring.pack("uint:5", a) return ret + + +def bitarray_to_u5(barr): + assert barr.len % 5 == 0 + ret = [] + s = bitstring.ConstBitStream(barr) + while s.pos != s.len: + ret.append(s.read(5).uint) + return ret diff --git a/lnbits/commands.py b/lnbits/commands.py index 2e9b837f..95950760 100644 --- a/lnbits/commands.py +++ b/lnbits/commands.py @@ -1,11 +1,11 @@ -import trio +import asyncio import warnings import click import importlib import re import os -from sqlalchemy.exc import OperationalError # type: ignore +from .db import SQLITE, POSTGRES, COCKROACH from .core import db as core_db, migrations as core_migrations from .helpers import ( get_valid_extensions, @@ -18,7 +18,7 @@ from .settings import LNBITS_PATH @click.command("migrate") def db_migrate(): - trio.run(migrate_databases) + asyncio.create_task(migrate_databases()) @click.command("assets") @@ -53,41 +53,61 @@ def bundle_vendored(): async def migrate_databases(): """Creates the necessary databases if they don't exist already; or migrates them.""" - async with core_db.connect() as conn: - try: - rows = await (await conn.execute("SELECT * FROM dbversions")).fetchall() - except OperationalError: - # migration 3 wasn't ran - await core_migrations.m000_create_migrations_table(conn) - rows = await (await conn.execute("SELECT * FROM dbversions")).fetchall() + async def set_migration_version(conn, db_name, version): + await conn.execute( + """ + INSERT INTO dbversions (db, version) VALUES (?, ?) + ON CONFLICT (db) DO UPDATE SET version = ? + """, + (db_name, version, version), + ) + async def run_migration(db, migrations_module): + db_name = migrations_module.__name__.split(".")[-2] + for key, migrate in migrations_module.__dict__.items(): + match = match = matcher.match(key) + if match: + version = int(match.group(1)) + if version > current_versions.get(db_name, 0): + print(f"running migration {db_name}.{version}") + await migrate(db) + + if db.schema == None: + await set_migration_version(db, db_name, version) + else: + async with core_db.connect() as conn: + await set_migration_version(conn, db_name, version) + + async with core_db.connect() as conn: + if conn.type == SQLITE: + exists = await conn.fetchone( + "SELECT * FROM sqlite_master WHERE type='table' AND name='dbversions'" + ) + elif conn.type in {POSTGRES, COCKROACH}: + exists = await conn.fetchone( + "SELECT * FROM information_schema.tables WHERE table_name = 'dbversions'" + ) + + if not exists: + await core_migrations.m000_create_migrations_table(conn) + + rows = await (await conn.execute("SELECT * FROM dbversions")).fetchall() current_versions = {row["db"]: row["version"] for row in rows} matcher = re.compile(r"^m(\d\d\d)_") - - async def run_migration(db, migrations_module): - db_name = migrations_module.__name__.split(".")[-2] - for key, migrate in migrations_module.__dict__.items(): - match = match = matcher.match(key) - if match: - version = int(match.group(1)) - if version > current_versions.get(db_name, 0): - print(f"running migration {db_name}.{version}") - await migrate(db) - await conn.execute( - "INSERT OR REPLACE INTO dbversions (db, version) VALUES (?, ?)", - (db_name, version), - ) - await run_migration(conn, core_migrations) - for ext in get_valid_extensions(): - try: - ext_migrations = importlib.import_module( - f"lnbits.extensions.{ext.code}.migrations" - ) - ext_db = importlib.import_module(f"lnbits.extensions.{ext.code}").db - await run_migration(ext_db, ext_migrations) - except ImportError: - raise ImportError( - f"Please make sure that the extension `{ext.code}` has a migrations file." - ) + for ext in get_valid_extensions(): + try: + ext_migrations = importlib.import_module( + f"lnbits.extensions.{ext.code}.migrations" + ) + ext_db = importlib.import_module(f"lnbits.extensions.{ext.code}").db + except ImportError: + raise ImportError( + f"Please make sure that the extension `{ext.code}` has a migrations file." + ) + + async with ext_db.connect() as ext_conn: + await run_migration(ext_conn, ext_migrations) + + print(" ✔️ All migrations done.") diff --git a/lnbits/core/__init__.py b/lnbits/core/__init__.py index 12dcded8..85e72d50 100644 --- a/lnbits/core/__init__.py +++ b/lnbits/core/__init__.py @@ -1,22 +1,11 @@ -from quart import Blueprint +from fastapi.routing import APIRouter + from lnbits.db import Database db = Database("database") -core_app: Blueprint = Blueprint( - "core", - __name__, - template_folder="templates", - static_folder="static", - static_url_path="/core/static", -) - +core_app: APIRouter = APIRouter() from .views.api import * # noqa from .views.generic import * # noqa from .views.public_api import * # noqa -from .tasks import register_listeners - -from lnbits.tasks import record_async - -core_app.record(record_async(register_listeners)) diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index 47623cc2..a63f52c4 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -5,13 +5,12 @@ from typing import List, Optional, Dict, Any from urllib.parse import urlparse from lnbits import bolt11 -from lnbits.db import Connection -from lnbits.settings import DEFAULT_WALLET_NAME +from lnbits.db import Connection, POSTGRES, COCKROACH +from lnbits.settings import DEFAULT_WALLET_NAME, LNBITS_ADMIN_USERS from . import db from .models import User, Wallet, Payment, BalanceCheck - # accounts # -------- @@ -43,41 +42,40 @@ async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[ if user: extensions = await (conn or db).fetchall( - "SELECT extension FROM extensions WHERE user = ? AND active = 1", (user_id,) + """SELECT extension FROM extensions WHERE "user" = ? AND active""", + (user_id,), ) wallets = await (conn or db).fetchall( """ SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) AS balance_msat FROM wallets - WHERE user = ? + WHERE "user" = ? """, (user_id,), ) + else: + return None - return ( - User( - **{ - **user, - **{ - "extensions": [e[0] for e in extensions], - "wallets": [Wallet(**w) for w in wallets], - }, - } - ) - if user - else None + return User( + id=user["id"], + email=user["email"], + extensions=[e[0] for e in extensions], + wallets=[Wallet(**w) for w in wallets], + admin=user["id"] in [x.strip() for x in LNBITS_ADMIN_USERS] + if LNBITS_ADMIN_USERS + else False, ) async def update_user_extension( - *, user_id: str, extension: str, active: int, conn: Optional[Connection] = None + *, user_id: str, extension: str, active: bool, conn: Optional[Connection] = None ) -> None: await (conn or db).execute( """ - INSERT OR REPLACE INTO extensions (user, extension, active) - VALUES (?, ?, ?) + INSERT INTO extensions ("user", extension, active) VALUES (?, ?, ?) + ON CONFLICT ("user", extension) DO UPDATE SET active = ? """, - (user_id, extension, active), + (user_id, extension, active, active), ) @@ -94,7 +92,7 @@ async def create_wallet( wallet_id = uuid4().hex await (conn or db).execute( """ - INSERT INTO wallets (id, name, user, adminkey, inkey) + INSERT INTO wallets (id, name, "user", adminkey, inkey) VALUES (?, ?, ?, ?, ?) """, ( @@ -112,6 +110,19 @@ async def create_wallet( return new_wallet +async def update_wallet( + wallet_id: str, new_name: str, conn: Optional[Connection] = None +) -> Optional[Wallet]: + await (conn or db).execute( + """ + UPDATE wallets SET + name = ? + WHERE id = ? + """, + (new_name, wallet_id), + ) + + async def delete_wallet( *, user_id: str, wallet_id: str, conn: Optional[Connection] = None ) -> None: @@ -119,10 +130,10 @@ async def delete_wallet( """ UPDATE wallets AS w SET - user = 'del:' || w.user, + "user" = 'del:' || w."user", adminkey = 'del:' || w.adminkey, inkey = 'del:' || w.inkey - WHERE id = ? AND user = ? + WHERE id = ? AND "user" = ? """, (wallet_id, user_id), ) @@ -208,6 +219,8 @@ async def get_payments( incoming: bool = False, since: Optional[int] = None, exclude_uncheckable: bool = False, + limit: Optional[int] = None, + offset: Optional[int] = None, conn: Optional[Connection] = None, ) -> List[Payment]: """ @@ -218,7 +231,12 @@ async def get_payments( clause: List[str] = [] if since != None: - clause.append("time > ?") + if db.type == POSTGRES: + clause.append("time > to_timestamp(?)") + elif db.type == COCKROACH: + clause.append("time > cast(? AS timestamp)") + else: + clause.append("time > ?") args.append(since) if wallet_id: @@ -228,9 +246,9 @@ async def get_payments( if complete and pending: pass elif complete: - clause.append("((amount > 0 AND pending = 0) OR amount < 0)") + clause.append("((amount > 0 AND pending = false) OR amount < 0)") elif pending: - clause.append("pending = 1") + clause.append("pending = true") else: pass @@ -247,6 +265,15 @@ async def get_payments( clause.append("checking_id NOT LIKE 'temp_%'") clause.append("checking_id NOT LIKE 'internal_%'") + limit_clause = f"LIMIT {limit}" if type(limit) == int and limit > 0 else "" + offset_clause = f"OFFSET {offset}" if type(offset) == int and offset > 0 else "" + # combine limit and offset clauses + limit_offset_clause = ( + f"{limit_clause} {offset_clause}" + if limit_clause and offset_clause + else limit_clause or offset_clause + ) + where = "" if clause: where = f"WHERE {' AND '.join(clause)}" @@ -257,10 +284,10 @@ async def get_payments( FROM apipayments {where} ORDER BY time DESC + {limit_offset_clause} """, tuple(args), ) - return [Payment.from_row(row) for row in rows] @@ -269,20 +296,21 @@ async def delete_expired_invoices( ) -> None: # first we delete all invoices older than one month await (conn or db).execute( - """ + f""" DELETE FROM apipayments - WHERE pending = 1 AND amount > 0 AND time < strftime('%s', 'now') - 2592000 + WHERE pending = true AND amount > 0 + AND time < {db.timestamp_now} - {db.interval_seconds(2592000)} """ ) # then we delete all expired invoices, checking one by one rows = await (conn or db).fetchall( - """ + f""" SELECT bolt11 FROM apipayments - WHERE pending = 1 + WHERE pending = true AND bolt11 IS NOT NULL - AND amount > 0 AND time < strftime('%s', 'now') - 86400 + AND amount > 0 AND time < {db.timestamp_now} - {db.interval_seconds(86400)} """ ) for (payment_request,) in rows: @@ -298,7 +326,7 @@ async def delete_expired_invoices( await (conn or db).execute( """ DELETE FROM apipayments - WHERE pending = 1 AND hash = ? + WHERE pending = true AND hash = ? """, (invoice.payment_hash,), ) @@ -337,7 +365,7 @@ async def create_payment( payment_hash, preimage, amount, - int(pending), + pending, memo, fee, json.dumps(extra) @@ -354,36 +382,27 @@ async def create_payment( async def update_payment_status( - checking_id: str, - pending: bool, - conn: Optional[Connection] = None, + checking_id: str, pending: bool, conn: Optional[Connection] = None ) -> None: await (conn or db).execute( "UPDATE apipayments SET pending = ? WHERE checking_id = ?", - ( - int(pending), - checking_id, - ), + (pending, checking_id), ) -async def delete_payment( - checking_id: str, - conn: Optional[Connection] = None, -) -> None: +async def delete_payment(checking_id: str, conn: Optional[Connection] = None) -> None: await (conn or db).execute( "DELETE FROM apipayments WHERE checking_id = ?", (checking_id,) ) async def check_internal( - payment_hash: str, - conn: Optional[Connection] = None, + payment_hash: str, conn: Optional[Connection] = None ) -> Optional[str]: row = await (conn or db).fetchone( """ SELECT checking_id FROM apipayments - WHERE hash = ? AND pending AND amount > 0 + WHERE hash = ? AND pending AND amount > 0 """, (payment_hash,), ) @@ -398,25 +417,21 @@ async def check_internal( async def save_balance_check( - wallet_id: str, - url: str, - conn: Optional[Connection] = None, + wallet_id: str, url: str, conn: Optional[Connection] = None ): domain = urlparse(url).netloc await (conn or db).execute( """ - INSERT OR REPLACE INTO balance_check (wallet, service, url) - VALUES (?, ?, ?) + INSERT INTO balance_check (wallet, service, url) VALUES (?, ?, ?) + ON CONFLICT (wallet, service) DO UPDATE SET url = ? """, - (wallet_id, domain, url), + (wallet_id, domain, url, url), ) async def get_balance_check( - wallet_id: str, - domain: str, - conn: Optional[Connection] = None, + wallet_id: str, domain: str, conn: Optional[Connection] = None ) -> Optional[BalanceCheck]: row = await (conn or db).fetchone( """ @@ -439,22 +454,19 @@ async def get_balance_checks(conn: Optional[Connection] = None) -> List[BalanceC async def save_balance_notify( - wallet_id: str, - url: str, - conn: Optional[Connection] = None, + wallet_id: str, url: str, conn: Optional[Connection] = None ): await (conn or db).execute( """ - INSERT OR REPLACE INTO balance_notify (wallet, url) - VALUES (?, ?) + INSERT INTO balance_notify (wallet, url) VALUES (?, ?) + ON CONFLICT (wallet) DO UPDATE SET url = ? """, - (wallet_id, url), + (wallet_id, url, url), ) async def get_balance_notify( - wallet_id: str, - conn: Optional[Connection] = None, + wallet_id: str, conn: Optional[Connection] = None ) -> Optional[str]: row = await (conn or db).fetchone( """ diff --git a/lnbits/core/migrations.py b/lnbits/core/migrations.py index 64de9acf..ebecb5e3 100644 --- a/lnbits/core/migrations.py +++ b/lnbits/core/migrations.py @@ -4,7 +4,7 @@ from sqlalchemy.exc import OperationalError # type: ignore async def m000_create_migrations_table(db): await db.execute( """ - CREATE TABLE dbversions ( + CREATE TABLE IF NOT EXISTS dbversions ( db TEXT PRIMARY KEY, version INT NOT NULL ) @@ -28,11 +28,11 @@ async def m001_initial(db): await db.execute( """ CREATE TABLE IF NOT EXISTS extensions ( - user TEXT NOT NULL, + "user" TEXT NOT NULL, extension TEXT NOT NULL, - active BOOLEAN DEFAULT 0, + active BOOLEAN DEFAULT false, - UNIQUE (user, extension) + UNIQUE ("user", extension) ); """ ) @@ -41,14 +41,14 @@ async def m001_initial(db): CREATE TABLE IF NOT EXISTS wallets ( id TEXT PRIMARY KEY, name TEXT NOT NULL, - user TEXT NOT NULL, + "user" TEXT NOT NULL, adminkey TEXT NOT NULL, inkey TEXT ); """ ) await db.execute( - """ + f""" CREATE TABLE IF NOT EXISTS apipayments ( payhash TEXT NOT NULL, amount INTEGER NOT NULL, @@ -56,8 +56,7 @@ async def m001_initial(db): wallet TEXT NOT NULL, pending BOOLEAN NOT NULL, memo TEXT, - time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now')), - + time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}, UNIQUE (wallet, payhash) ); """ @@ -65,18 +64,18 @@ async def m001_initial(db): await db.execute( """ - CREATE VIEW IF NOT EXISTS balances AS + CREATE VIEW balances AS SELECT wallet, COALESCE(SUM(s), 0) AS balance FROM ( SELECT wallet, SUM(amount) AS s -- incoming FROM apipayments - WHERE amount > 0 AND pending = 0 -- don't sum pending + WHERE amount > 0 AND pending = false -- don't sum pending GROUP BY wallet UNION ALL SELECT wallet, SUM(amount + fee) AS s -- outgoing, sum fees FROM apipayments WHERE amount < 0 -- do sum pending GROUP BY wallet - ) + )x GROUP BY wallet; """ ) @@ -143,21 +142,20 @@ async def m004_ensure_fees_are_always_negative(db): """ await db.execute("DROP VIEW balances") - await db.execute( """ - CREATE VIEW IF NOT EXISTS balances AS + CREATE VIEW balances AS SELECT wallet, COALESCE(SUM(s), 0) AS balance FROM ( SELECT wallet, SUM(amount) AS s -- incoming FROM apipayments - WHERE amount > 0 AND pending = 0 -- don't sum pending + WHERE amount > 0 AND pending = false -- don't sum pending GROUP BY wallet UNION ALL SELECT wallet, SUM(amount - abs(fee)) AS s -- outgoing, sum fees FROM apipayments WHERE amount < 0 -- do sum pending GROUP BY wallet - ) + )x GROUP BY wallet; """ ) @@ -170,8 +168,8 @@ async def m005_balance_check_balance_notify(db): await db.execute( """ - CREATE TABLE balance_check ( - wallet INTEGER NOT NULL REFERENCES wallets (id), + CREATE TABLE IF NOT EXISTS balance_check ( + wallet TEXT NOT NULL REFERENCES wallets (id), service TEXT NOT NULL, url TEXT NOT NULL, @@ -182,8 +180,8 @@ async def m005_balance_check_balance_notify(db): await db.execute( """ - CREATE TABLE balance_notify ( - wallet INTEGER NOT NULL REFERENCES wallets (id), + CREATE TABLE IF NOT EXISTS balance_notify ( + wallet TEXT NOT NULL REFERENCES wallets (id), url TEXT NOT NULL, UNIQUE(wallet, url) diff --git a/lnbits/core/models.py b/lnbits/core/models.py index bef29135..88963b2b 100644 --- a/lnbits/core/models.py +++ b/lnbits/core/models.py @@ -1,32 +1,16 @@ import json import hmac import hashlib -from quart import url_for +from lnbits.helpers import url_for 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 pydantic import BaseModel from lnbits.settings import WALLET -class User(NamedTuple): - id: str - email: str - extensions: List[str] = [] - wallets: List["Wallet"] = [] - password: Optional[str] = None - - @property - def wallet_ids(self) -> List[str]: - return [wallet.id for wallet in self.wallets] - - def get_wallet(self, wallet_id: str) -> Optional["Wallet"]: - w = [wallet for wallet in self.wallets if wallet.id == wallet_id] - return w[0] if w else None - - -class Wallet(NamedTuple): +class Wallet(BaseModel): id: str name: str user: str @@ -46,12 +30,8 @@ class Wallet(NamedTuple): @property def lnurlwithdraw_full(self) -> str: - url = url_for( - "core.lnurl_full_withdraw", - usr=self.user, - wal=self.id, - _external=True, - ) + + url = url_for("/withdraw", external=True, usr=self.user, wal=self.id) try: return lnurl_encode(url) except: @@ -62,51 +42,46 @@ class Wallet(NamedTuple): linking_key = hmac.digest(hashing_key, domain.encode("utf-8"), "sha256") return SigningKey.from_string( - linking_key, - curve=SECP256k1, - hashfunc=hashlib.sha256, + linking_key, curve=SECP256k1, hashfunc=hashlib.sha256 ) async def get_payment(self, payment_hash: str) -> Optional["Payment"]: - from .crud import get_wallet_payment + from .crud import get_standalone_payment - return await get_wallet_payment(self.id, payment_hash) - - async def get_payments( - self, - *, - complete: bool = True, - pending: bool = False, - outgoing: bool = True, - incoming: bool = True, - exclude_uncheckable: bool = False, - ) -> List["Payment"]: - from .crud import get_payments - - return await get_payments( - wallet_id=self.id, - complete=complete, - pending=pending, - outgoing=outgoing, - incoming=incoming, - exclude_uncheckable=exclude_uncheckable, - ) + return await get_standalone_payment(payment_hash) -class Payment(NamedTuple): +class User(BaseModel): + id: str + email: Optional[str] = None + extensions: List[str] = [] + wallets: List[Wallet] = [] + password: Optional[str] = None + admin: bool = False + + @property + def wallet_ids(self) -> List[str]: + return [wallet.id for wallet in self.wallets] + + def get_wallet(self, wallet_id: str) -> Optional["Wallet"]: + w = [wallet for wallet in self.wallets if wallet.id == wallet_id] + return w[0] if w else None + + +class Payment(BaseModel): checking_id: str pending: bool amount: int fee: int - memo: str + memo: Optional[str] time: int bolt11: str preimage: str payment_hash: str - extra: Dict + extra: Optional[Dict] = {} wallet_id: str - webhook: str - webhook_status: int + webhook: Optional[str] + webhook_status: Optional[int] @classmethod def from_row(cls, row: Row): @@ -181,7 +156,7 @@ class Payment(NamedTuple): await delete_payment(self.checking_id) -class BalanceCheck(NamedTuple): +class BalanceCheck(BaseModel): wallet: str service: str url: str diff --git a/lnbits/core/services.py b/lnbits/core/services.py index 09b9f4f7..3d54e218 100644 --- a/lnbits/core/services.py +++ b/lnbits/core/services.py @@ -1,34 +1,36 @@ -import trio +import asyncio import json -import httpx -from io import BytesIO from binascii import unhexlify -from typing import Optional, Tuple, Dict -from urllib.parse import urlparse, parse_qs -from quart import g, url_for -from lnurl import LnurlErrorResponse, decode as decode_lnurl # type: ignore +from io import BytesIO +from typing import Dict, Optional, Tuple +from urllib.parse import parse_qs, urlparse + +import httpx +from lnurl import LnurlErrorResponse +from lnurl import decode as decode_lnurl # type: ignore + +from lnbits import bolt11 +from lnbits.db import Connection +from lnbits.helpers import url_for, urlsafe_short_hash +from lnbits.requestvars import g +from lnbits.settings import WALLET +from lnbits.wallets.base import PaymentResponse, PaymentStatus + +from . import db +from .crud import ( + check_internal, + create_payment, + delete_payment, + get_wallet, + get_wallet_payment, + update_payment_status, +) try: from typing import TypedDict # type: ignore except ImportError: # pragma: nocover from typing_extensions import TypedDict -from lnbits import bolt11 -from lnbits.db import Connection -from lnbits.helpers import urlsafe_short_hash -from lnbits.settings import WALLET -from lnbits.wallets.base import PaymentStatus, PaymentResponse - -from . import db -from .crud import ( - get_wallet, - create_payment, - delete_payment, - check_internal, - update_payment_status, - get_wallet_payment, -) - class PaymentFailure(Exception): pass @@ -49,7 +51,6 @@ async def create_invoice( conn: Optional[Connection] = None, ) -> Tuple[str, str]: invoice_memo = None if description_hash else memo - storeable_memo = memo ok, checking_id, payment_request, error_message = await WALLET.create_invoice( amount=amount, memo=invoice_memo, description_hash=description_hash @@ -66,7 +67,7 @@ async def create_invoice( payment_request=payment_request, payment_hash=invoice.payment_hash, amount=amount_msat, - memo=storeable_memo, + memo=memo, extra=extra, webhook=webhook, conn=conn, @@ -84,11 +85,12 @@ async def pay_invoice( description: str = "", conn: Optional[Connection] = None, ) -> str: + invoice = bolt11.decode(payment_request) + fee_reserve_msat = fee_reserve(invoice.amount_msat) async with (db.reuse_conn(conn) if conn else db.connect()) as conn: temp_id = f"temp_{urlsafe_short_hash()}" internal_id = f"internal_{urlsafe_short_hash()}" - invoice = bolt11.decode(payment_request) if invoice.amount_msat == 0: raise ValueError("Amountless invoices not supported.") if max_sat and invoice.amount_msat > max_sat * 1000: @@ -131,7 +133,7 @@ async def pay_invoice( # the balance is enough in the next step await create_payment( checking_id=temp_id, - fee=-fee_reserve(invoice.amount_msat), + fee=-fee_reserve_msat, conn=conn, **payment_kwargs, ) @@ -140,26 +142,33 @@ async def pay_invoice( wallet = await get_wallet(wallet_id, conn=conn) assert wallet if wallet.balance_msat < 0: + 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." + ) raise PermissionError("Insufficient balance.") - if 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 + if 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 + async with db.connect() as conn: await update_payment_status( - checking_id=internal_checking_id, - pending=False, - conn=conn, + checking_id=internal_checking_id, pending=False, conn=conn ) - # notify receiver asynchronously - from lnbits.tasks import internal_invoice_paid + # notify receiver asynchronously - await internal_invoice_paid.send(internal_checking_id) - else: - # actually pay the external invoice - payment: PaymentResponse = await WALLET.pay_invoice(payment_request) - if payment.checking_id: + from lnbits.tasks import internal_invoice_queue + + await internal_invoice_queue.put(internal_checking_id) + else: + # actually pay the external invoice + payment: PaymentResponse = await WALLET.pay_invoice( + payment_request, fee_reserve_msat + ) + if payment.checking_id: + async with db.connect() as conn: await create_payment( checking_id=payment.checking_id, fee=payment.fee_msat, @@ -169,13 +178,15 @@ async def pay_invoice( **payment_kwargs, ) await delete_payment(temp_id, conn=conn) - else: - raise PaymentFailure( - payment.error_message - or "Payment failed, but backend didn't give us an error message." - ) + else: + async with db.connect() as conn: + await delete_payment(temp_id, conn=conn) + raise PaymentFailure( + payment.error_message + or "Payment failed, but backend didn't give us an error message." + ) - return invoice.payment_hash + return invoice.payment_hash async def redeem_lnurl_withdraw( @@ -211,19 +222,15 @@ async def redeem_lnurl_withdraw( return None if wait_seconds: - await trio.sleep(wait_seconds) + await asyncio.sleep(wait_seconds) - params = { - "k1": res["k1"], - "pr": payment_request, - } + params = {"k1": res["k1"], "pr": payment_request} try: params["balanceNotify"] = url_for( - "core.lnurl_balance_notify", - service=urlparse(lnurl_request).netloc, + f"/withdraw/notify/{urlparse(lnurl_request).netloc}", + external=True, wal=wallet_id, - _external=True, ) except Exception: pass @@ -236,13 +243,12 @@ async def redeem_lnurl_withdraw( async def perform_lnurlauth( - callback: str, - conn: Optional[Connection] = None, + callback: str, 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 = g().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""" @@ -275,12 +281,12 @@ async def perform_lnurlauth( sign_len = 6 + r_len + s_len signature = BytesIO() - signature.write(0x30 .to_bytes(1, "big", signed=False)) + signature.write(0x30.to_bytes(1, "big", signed=False)) signature.write((sign_len - 2).to_bytes(1, "big", signed=False)) - signature.write(0x02 .to_bytes(1, "big", signed=False)) + signature.write(0x02.to_bytes(1, "big", signed=False)) signature.write(r_len.to_bytes(1, "big", signed=False)) signature.write(r) - signature.write(0x02 .to_bytes(1, "big", signed=False)) + signature.write(0x02.to_bytes(1, "big", signed=False)) signature.write(s_len.to_bytes(1, "big", signed=False)) signature.write(s) @@ -305,21 +311,30 @@ async def perform_lnurlauth( return LnurlErrorResponse(reason=resp["reason"]) except (KeyError, json.decoder.JSONDecodeError): return LnurlErrorResponse( - reason=r.text[:200] + "..." if len(r.text) > 200 else r.text, + reason=r.text[:200] + "..." if len(r.text) > 200 else r.text ) async def check_invoice_status( - wallet_id: str, - payment_hash: str, - conn: Optional[Connection] = None, + wallet_id: str, payment_hash: str, conn: Optional[Connection] = None ) -> PaymentStatus: payment = await get_wallet_payment(wallet_id, payment_hash, conn=conn) if not payment: return PaymentStatus(None) - - return await WALLET.get_invoice_status(payment.checking_id) + status = await WALLET.get_invoice_status(payment.checking_id) + if not payment.pending: + return status + if payment.is_out and status.failed: + print(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}" + ) + await payment.set_pending(status.pending) + return status +# WARN: this same value must be used for balance check and passed to WALLET.pay_invoice(), it may cause a vulnerability if the values differ def fee_reserve(amount_msat: int) -> int: - return max(1000, int(amount_msat * 0.01)) + return max(2000, int(amount_msat * 0.01)) diff --git a/lnbits/core/static/js/wallet.js b/lnbits/core/static/js/wallet.js index d0191051..8e782c54 100644 --- a/lnbits/core/static/js/wallet.js +++ b/lnbits/core/static/js/wallet.js @@ -161,14 +161,14 @@ new Vue({ { name: 'sat', align: 'right', - label: 'Amount (sat)', + label: 'Amount (' + LNBITS_DENOMINATION + ')', field: 'sat', sortable: true }, { name: 'fee', align: 'right', - label: 'Fee (msat)', + label: 'Fee (m' + LNBITS_DENOMINATION + ')', field: 'fee' } ], @@ -184,12 +184,18 @@ new Vue({ show: false, location: window.location }, - balance: 0 + balance: 0, + credit: 0, + newName: '' } }, computed: { formattedBalance: function () { - return LNbits.utils.formatSat(this.balance || this.g.wallet.sat) + if (LNBITS_DENOMINATION != 'sats') { + return this.balance / 100 + } else { + return LNbits.utils.formatSat(this.balance || this.g.wallet.sat) + } }, filteredPayments: function () { var q = this.paymentsTable.filter @@ -202,9 +208,7 @@ new Vue({ return this.parse.invoice.sat <= this.balance }, pendingPaymentsExist: function () { - return this.payments - ? _.where(this.payments, {pending: 1}).length > 0 - : false + return this.payments.findIndex(payment => payment.pending) !== -1 } }, filters: { @@ -250,6 +254,28 @@ new Vue({ this.parse.data.paymentChecker = null this.parse.camera.show = false }, + updateBalance: function (credit) { + if (LNBITS_DENOMINATION != 'sats') { + credit = credit * 100 + } + LNbits.api + .request('PUT', '/api/v1/wallet/balance/' + credit, this.g.wallet.inkey) + .catch(err => { + LNbits.utils.notifyApiError(err) + }) + .then(response => { + let data = response.data + if (data.status === 'ERROR') { + this.$q.notify({ + timeout: 5000, + type: 'warning', + message: `Failed to update.` + }) + return + } + this.balance = this.balance + data.balance + }) + }, closeReceiveDialog: function () { setTimeout(() => { clearInterval(this.receive.paymentChecker) @@ -272,7 +298,9 @@ new Vue({ }, createInvoice: function () { this.receive.status = 'loading' - + if (LNBITS_DENOMINATION != 'sats') { + this.receive.data.amount = this.receive.data.amount * 100 + } LNbits.api .createInvoice( this.g.wallet, @@ -336,18 +364,21 @@ new Vue({ }, decodeRequest: function () { this.parse.show = true - + let req = this.parse.data.request.toLowerCase() if (this.parse.data.request.startsWith('lightning:')) { this.parse.data.request = this.parse.data.request.slice(10) } else if (this.parse.data.request.startsWith('lnurl:')) { this.parse.data.request = this.parse.data.request.slice(6) - } else if (this.parse.data.request.indexOf('lightning=lnurl1') !== -1) { + } else if (req.indexOf('lightning=lnurl1') !== -1) { this.parse.data.request = this.parse.data.request .split('lightning=')[1] .split('&')[0] } - if (this.parse.data.request.toLowerCase().startsWith('lnurl1')) { + if ( + this.parse.data.request.toLowerCase().startsWith('lnurl1') || + this.parse.data.request.match(/[\w.+-~_]+@[\w.+-~_]/) + ) { LNbits.api .request( 'GET', @@ -585,6 +616,30 @@ new Vue({ } }) }, + updateWalletName: function () { + let newName = this.newName + if (!newName || !newName.length) return + // let data = {name: newName} + LNbits.api + .request('PUT', '/api/v1/wallet/' + newName, this.g.wallet.adminkey, {}) + .then(res => { + this.newName = '' + this.$q.notify({ + message: `Wallet named updated.`, + type: 'positive', + timeout: 3500 + }) + LNbits.href.updateWallet( + res.data.name, + this.user.id, + this.g.wallet.id + ) + }) + .catch(err => { + this.newName = '' + LNbits.utils.notifyApiError(err) + }) + }, deleteWallet: function (walletId, user) { LNbits.utils .confirmDialog('Are you sure you want to delete this wallet?') diff --git a/lnbits/core/tasks.py b/lnbits/core/tasks.py index fa2df964..1e3eaeab 100644 --- a/lnbits/core/tasks.py +++ b/lnbits/core/tasks.py @@ -1,4 +1,4 @@ -import trio +import asyncio import httpx from typing import List @@ -8,17 +8,19 @@ from . import db from .crud import get_balance_notify from .models import Payment -api_invoice_listeners: List[trio.MemorySendChannel] = [] +api_invoice_listeners: List[asyncio.Queue] = [] -async def register_listeners(): - invoice_paid_chan_send, invoice_paid_chan_recv = trio.open_memory_channel(5) - register_invoice_listener(invoice_paid_chan_send) - await wait_for_paid_invoices(invoice_paid_chan_recv) +async def register_task_listeners(): + invoice_paid_queue = asyncio.Queue(5) + register_invoice_listener(invoice_paid_queue) + asyncio.create_task(wait_for_paid_invoices(invoice_paid_queue)) -async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel): - async for payment in invoice_paid_chan: +async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue): + while True: + payment = await invoice_paid_queue.get() + # send information to sse channel await dispatch_invoice_listener(payment) @@ -31,10 +33,7 @@ async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel): if url: async with httpx.AsyncClient() as client: try: - r = await client.post( - url, - timeout=4, - ) + r = await client.post(url, timeout=4) await mark_webhook_sent(payment, r.status_code) except (httpx.ConnectError, httpx.RequestError): pass @@ -43,21 +42,17 @@ async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel): async def dispatch_invoice_listener(payment: Payment): for send_channel in api_invoice_listeners: try: - send_channel.send_nowait(payment) - except trio.WouldBlock: + send_channel.put_nowait(payment) + except asyncio.QueueFull: print("removing sse listener", send_channel) api_invoice_listeners.remove(send_channel) async def dispatch_webhook(payment: Payment): async with httpx.AsyncClient() as client: - data = payment._asdict() + data = payment.dict() try: - r = await client.post( - payment.webhook, - json=data, - timeout=40, - ) + r = await client.post(payment.webhook, json=data, timeout=40) await mark_webhook_sent(payment, r.status_code) except (httpx.ConnectError, httpx.RequestError): await mark_webhook_sent(payment, -1) diff --git a/lnbits/core/templates/core/_api_docs.html b/lnbits/core/templates/core/_api_docs.html index 4bb38fad..0e74f38e 100644 --- a/lnbits/core/templates/core/_api_docs.html +++ b/lnbits/core/templates/core/_api_docs.html @@ -19,7 +19,7 @@ GET /api/v1/wallet
Headers
- {"X-Api-Key": "{{ wallet.adminkey }}"}
+ {"X-Api-Key": "{{ wallet.inkey }}"}
Returns 200 OK (application/json)
@@ -29,7 +29,7 @@ >
Curl example
curl {{ request.url_root }}api/v1/wallet -H "X-Api-Key: + >curl {{ request.base_url }}api/v1/wallet -H "X-Api-Key: {{ wallet.inkey }}"
@@ -59,9 +59,9 @@ >
Curl example
curl -X POST {{ request.url_root }}api/v1/payments -d '{"out": false, + >curl -X POST {{ request.base_url }}api/v1/payments -d '{"out": false, "amount": <int>, "memo": <string>, "webhook": - <url:string>}' -H "X-Api-Key: {{ wallet.inkey }}" -H + <url:string>, "unit": <string>}' -H "X-Api-Key: {{ wallet.inkey }}" -H "Content-type: application/json" @@ -86,7 +86,7 @@ {"payment_hash": <string>}
Curl example
curl -X POST {{ request.url_root }}api/v1/payments -d '{"out": true, + >curl -X POST {{ request.base_url }}api/v1/payments -d '{"out": true, "bolt11": <string>}' -H "X-Api-Key: {{ wallet.adminkey }}" -H "Content-type: application/json" + + + + + POST + /api/v1/payments/decode +
Headers
+ {"X-Api-Key": "{{ wallet.inkey }}"}
+
Body (application/json)
+ {"invoice": <string>} +
+ Returns 200 (application/json) +
+
Curl example
+ curl -X POST {{ request.base_url }}api/v1/payments/decode -d + '{"data": <bolt11/lnurl, string>}' -H "X-Api-Key: + {{ wallet.inkey }}" -H "Content-type: application/json" +
+
+
{"paid": <bool>}
Curl example
curl -X GET {{ request.url_root + >curl -X GET {{ request.base_url }}api/v1/payments/<payment_hash> -H "X-Api-Key: {{ wallet.inkey }}" -H "Content-type: application/json" diff --git a/lnbits/core/templates/core/extensions.html b/lnbits/core/templates/core/extensions.html index 97fa8936..daeb660f 100644 --- a/lnbits/core/templates/core/extensions.html +++ b/lnbits/core/templates/core/extensions.html @@ -17,14 +17,14 @@ > {% raw %}
{{ extension.name }}
- {{ extension.shortDescription }} {% endraw %} + {{ extension.shortDescription }} {% endraw %}
Open diff --git a/lnbits/core/templates/core/index.html b/lnbits/core/templates/core/index.html index 7ca61a3a..f363a841 100644 --- a/lnbits/core/templates/core/index.html +++ b/lnbits/core/templates/core/index.html @@ -8,10 +8,10 @@ {% if lnurl %} Press to claim bitcoin @@ -21,11 +21,11 @@ filled dense v-model="walletName" - label="Name your LNbits wallet *" + label="Name your {{SITE_TITLE}} wallet *" > Add a new wallet -

LNbits

-
Free and open-source lightning wallet
-

- Easy to set up and lightweight, LNbits can run on any - lightning-network funding source, currently supporting LND, - c-lightning, OpenNode, lntxbot, LNPay and even LNbits itself! -

-

- You can run LNbits for yourself, or easily offer a custodian solution - for others. -

-

- Each wallet has its own API keys and there is no limit to the number - of wallets you can make. Being able to partition funds makes LNbits a - useful tool for money management and as a development tool. -

-

- Extensions add extra functionality to LNbits so you can experiment - with a range of cutting-edge technologies on the lightning network. We - have made developing extensions as easy as possible, and as a free and - open-source project, we encourage people to develop and submit their - own. -

-
- View project in GitHub - Donate +

{{SITE_TITLE}}

+
{{SITE_TAGLINE}}
+
+

+ Easy to set up and lightweight, LNbits can run on any + lightning-network funding source, currently supporting LND, + c-lightning, OpenNode, lntxbot, LNPay and even LNbits itself! +

+

+ You can run LNbits for yourself, or easily offer a custodian + solution for others. +

+

+ Each wallet has its own API keys and there is no limit to the number + of wallets you can make. Being able to partition funds makes LNbits + a useful tool for money management and as a development tool. +

+

+ Extensions add extra functionality to LNbits so you can experiment + with a range of cutting-edge technologies on the lightning network. + We have made developing extensions as easy as possible, and as a + free and open-source project, we encourage people to develop and + submit their own. +

+
+ View project in GitHub + Donate +
+

{{SITE_DESCRIPTION}}

-