Compare commits

...
119 Commits
Author SHA1 Message Date
arcbtc 810b918619 quick fix 2022-04-02 00:36:50 +01:00
ArcandGitHub 81860bdc5d Merge pull request #579 from ashkanjalaliQ/main
add "IRT" currency to TPos extention
2022-04-01 23:57:19 +01:00
Ashkan JalaliandGitHub c5c76468cb add "IRT" currency to TPos extention 2022-04-01 20:05:53 +04:30
fusion44andGitHub 799065e982 Merge pull request #576 from lilcheti/IRT-api
add IRT api
2022-03-31 18:52:53 +02:00
rimthekid a94444ae2c add IRT 2022-03-30 05:22:48 -07:00
ArcandGitHub 54264315f0 Merge pull request #575 from axelhodler/exception_handling
fail early if LNBITS_DATA_FOLDER missing
2022-03-30 12:04:28 +01:00
Axel Hodler 946ebc350e fail early if LNBITS_DATA_FOLDER missing 2022-03-27 17:03:24 +02:00
calleandGitHub b1c75813a0 fix decodepay (#568) 2022-03-24 12:50:57 +01:00
ArcandGitHub a17108f108 Update installation.md 2022-03-22 19:25:30 +00:00
calleandGitHub 1209526147 remove get_payment_status (#562)
* remove get_payment_status

* remove comments
2022-03-18 18:51:28 +01:00
calleandGitHub 0f97f8f18b Fee reserve for lightning backends (#557)
* preparing fees

* fee_limit_msat

* await resp result

* clightning

* fix tests

* fix test

* add fee to test

* mypy

* invoice_status

* checking id fix

* fee reserve error message

* only for external payments
2022-03-16 07:20:15 +01:00
ArcandGitHub 911fe92e03 Merge pull request #559 from lnbits/withdrawpayzerocheck
Added check so number must be more than zero
2022-03-14 11:30:08 +00:00
Ben Arc eb8bbd3896 Added check so number must be more than zero 2022-03-14 11:22:56 +00:00
Ben Arc ff70efd1eb Removes LNDRPC, if someone wants to fix we can implement again 2022-03-11 14:13:28 +00:00
Ben Arc ce6620882f Satspay tooltip fix (quart commit 8e3230287dd191a743479ee9ceb2f497c43dd9bc) 2022-03-11 13:25:14 +00:00
Ben Arc ffba97850b Stopped 0 invoice creation in tickets 2022-03-11 13:12:27 +00:00
Ben Arc 435b4056fd fixed conversion api 2022-03-10 12:16:51 +00:00
Ben Arc 30a0d6336b removed print 2022-03-08 22:29:13 +00:00
Ben Arc 52ef2da76d added wrong key type 2022-03-08 22:22:51 +00:00
Ben Arc e4dd0972a5 Wrong key int being returned 2022-03-08 22:15:45 +00:00
Ben Arc c708cb1e2b Important fix to payment check 2022-03-07 18:27:03 +00:00
ArcandGitHub 43f9394014 Merge pull request #555 from lnbits/extraPayDetails
Adds more payment details if the correct key is providedd
2022-03-07 08:20:07 +00:00
benarc 9bbf78e026 Assigned wallet var 2022-03-07 08:19:14 +00:00
ArcandGitHub 6df951b873 Merge pull request #554 from lnbits/extraPayDetails
Adds more payment details if the correct key is provided
2022-03-07 08:11:11 +00:00
benarc d62ef0bb80 Added extra pay details 2022-03-07 08:08:08 +00:00
benarc 4a9e7f4264 Merge remote-tracking branch 'origin/main' into main 2022-03-07 05:56:10 +00:00
ArcandGitHub b886e5b0ea Merge pull request #528 from CandleHater/patch-2
added "missing" libffi/libpq package
2022-03-07 05:47:29 +00:00
benarc 72f2ec0be8 Merge remote-tracking branch 'origin/main' into main 2022-03-07 04:26:40 +00:00
ArcandGitHub ef6d12fe10 Merge pull request #545 from talvasconcelos/fix/paymentsDecode
fix payments decode
2022-03-07 04:24:32 +00:00
ArcandGitHub 1e759283ce Merge pull request #540 from talvasconcelos/fix/watchonly_mempool
fix update mempool
2022-03-07 04:19:23 +00:00
ArcandGitHub e92447092d Merge pull request #532 from lnbits/retry_connection_on_fail
Retry backend connection on startup
2022-03-07 04:18:57 +00:00
ArcandGitHub 7988e2c303 Merge pull request #541 from talvasconcelos/fix/streamlabsInv
fix streamalerts
2022-03-07 04:17:15 +00:00
ArcandGitHub a13bb9cfea Merge pull request #542 from talvasconcelos/fix/copilot_db
fix copilots not being fetched
2022-03-07 04:16:41 +00:00
ArcandGitHub 7088a3240f Merge pull request #543 from talvasconcelos/fix/tipjar
change namedtuple to basemodel
2022-03-07 04:16:16 +00:00
ArcandGitHub 00e52001b5 Merge pull request #544 from talvasconcelos/fix/events
fix typo in table name
2022-03-07 04:15:54 +00:00
ArcandGitHub cd35966a56 Merge pull request #546 from talvasconcelos/fix/jukebox
fix jukebox
2022-03-07 04:15:28 +00:00
ArcandGitHub c59a65cdb4 Merge pull request #550 from talvasconcelos/fix/lnticket
Tickets issue #548
2022-03-07 04:14:16 +00:00
benarc 5f52658626 Merge remote-tracking branch 'origin/main' into main 2022-03-07 04:05:43 +00:00
Tiago vasconcelos 2399b05855 open ticket in dialog for large text 2022-03-03 23:05:34 +00:00
Tiago vasconcelos 8fb70041d2 fix flatrate per word toggle 2022-03-03 21:35:58 +00:00
Joseph PentlandandGitHub baf55afc28 api: Fix conversion from fiat to satoshis (#516)
Conversion to satoshis incorrectly used data.to as fiat currency rather
than data._from.
2022-03-01 23:11:39 +03:00
Graham KrizekandGitHub 251e6a8233 Add a fallback to LND_REST_CERT to use the system chain if its not present (#539) 2022-03-01 22:58:54 +03:00
ArcandGitHub d73c3950b9 Update README.md 2022-03-01 19:07:21 +00:00
ArcandGitHub ac61cd5427 Update README.md 2022-03-01 19:07:07 +00:00
ArcandGitHub 0c14b8fb82 Merge pull request #533 from louneskmt/fix-docker-image-build
Fix Docker image autobuild GitHub Action and build for arm
2022-03-01 18:57:45 +00:00
Tiago vasconcelos 6c0aa8fa8d info/error if no music is playing yet on jukebox 2022-02-28 10:09:40 +00:00
Tiago vasconcelos 9b95a7743d fix user keyword to "user" 2022-02-28 10:08:50 +00:00
benarc e96701cb59 Added template banner 2022-02-24 16:03:10 +00:00
benarc c1fa5bbcbf Change HIDE to false 2022-02-24 12:32:37 +00:00
Tiago vasconcelos 241c81c790 fix update mempool 2022-02-23 22:09:38 +00:00
Tiago vasconcelos 42b444f54d make "anonymous" as default name 2022-02-23 21:40:11 +00:00
Tiago vasconcelos f7ff9d27d2 fix copilots not being fetched 2022-02-23 21:03:25 +00:00
benarc 4a588dda1b Added ad space and option to turn off api docs 2022-02-23 13:51:30 +00:00
Tiago vasconcelos e82ce89e9a change namedtuple to basemodel 2022-02-23 13:32:23 +00:00
Tiago vasconcelos 270cac37ac fix typo in table name 2022-02-23 10:37:15 +00:00
Tiago vasconcelos a374526faf make Anonymous default name 2022-02-23 10:13:52 +00:00
Tiago vasconcelos 8930152b0f fix models int to str 2022-02-22 21:22:07 +00:00
Tiago vasconcelos 71e11b7387 fix typo 2022-02-22 21:12:46 +00:00
Tiago vasconcelos 510a28f3d0 create_charge data 2022-02-22 20:38:35 +00:00
ArcandGitHub c58e2c5d74 Merge pull request #538 from talvasconcelos/fix/withdrawQR
Fix LNURLw QR code
2022-02-22 18:28:29 +00:00
Tiago vasconcelos eb7b369b6d fix payments decode 2022-02-22 16:45:35 +00:00
Tiago vasconcelos a5a3331106 revert to fallback scheme + make request case insensitive 2022-02-22 16:23:50 +00:00
Tiago vasconcelos 4f3e48e280 fix lnurl encoded in QR 2022-02-22 15:16:39 +00:00
Lounès Ksouri 31bcb32e2d feat: dynamic port and host using environment variables 2022-02-22 08:41:57 +01:00
Lounès Ksouri a132387fa8 fix: add build deps for arm 2022-02-22 08:41:19 +01:00
Lounès Ksouri 27694ee23d refactor: GitHub Action to build and push to Docker Hub on tag 2022-02-22 08:40:23 +01:00
callebtc 36952981c2 remove success var 2022-02-21 09:25:45 +01:00
callebtc fbafe2b8f1 Retry backend connection on startup 2022-02-21 09:17:36 +01:00
ArcandGitHub 41a9689014 Update installation.md 2022-02-18 17:17:18 +00:00
ArcandGitHub 76edfa2134 Update installation.md 2022-02-18 17:16:49 +00:00
ArcandGitHub d7b3a28950 Update installation.md 2022-02-18 17:16:18 +00:00
ArcandGitHub 2c161a079d Update installation.md 2022-02-18 17:16:03 +00:00
CandleandGitHub 4793ea0d18 added "missing" libffi/libpq package
When I tryed to run "pipenv install --dev" on a Pi 4 with the latest Raspberry OS it failed with two dependencies. These additional installations fixed it.
2022-02-18 16:35:32 +01:00
ArcandGitHub 88df4266b7 Merge pull request #524 from lnbits/docs_api_add_unit_to_invoice
Fix fiat amount entry in invoice creation
2022-02-18 11:44:05 +00:00
callebtc 6496fac35f fill mask for fiat 2022-02-18 12:35:25 +01:00
Ben Arc 10492ec3d0 Changed bitcoin theme back 2022-02-17 23:40:29 +00:00
Ben Arc 302d79a30f Improved bitcoin theme 2022-02-17 23:33:55 +00:00
ArcandGitHub b88abd1a46 Merge pull request #527 from talvasconcelos/fix/instantWallet
Fix instant wallet form LNURLw
2022-02-17 23:21:58 +00:00
Ben Arc 573c4cf5ea Changed theme order to default to classic 2022-02-17 21:46:51 +00:00
Ben Arc e9065833c0 Fixed themes, added bitcoin theme 2022-02-17 21:34:36 +00:00
Tiago vasconcelos 251ecb8f1d url_for params 2022-02-17 21:01:14 +00:00
calleandGitHub c626e12fa3 LNDHhub for fastapi with LIMIT (#499)
* add limit to get_payments

* add limit to lndhub

* fix defaults

* default memo

* add offset cluase

* offset for the api
2022-02-17 13:10:34 +01:00
callebtc 13a72d9a41 replace string with LNBITS_SITE_TITLE 2022-02-17 11:42:08 +01:00
benarc 3c5fac3874 Fixed paywall, unnecessary key check added 2022-02-17 09:23:33 +00:00
callebtc 15c8557459 allow empty memo 2022-02-17 10:16:11 +01:00
callebtc d559c604fa invoice without memo 2022-02-17 09:29:10 +01:00
callebtc cd84623305 amount label adaptive to uni - look at me Im a frontent dev now 2022-02-17 09:14:07 +01:00
callebtc 9fb30080ab black 2022-02-16 22:42:27 +01:00
callebtc 30ab519c3a amount is float and sats are int 2022-02-16 22:41:12 +01:00
callebtc 914b9f3ffe allow amounts >0 2022-02-16 22:23:26 +01:00
0764f4fdf1 Support for encrypted macaroons (#521)
* encrypted macaroons

* fix GRPC env entry

* example config entry

* add pycryptodomex to requirements

* documentation

* Added pycryptodomex to pip file

Co-authored-by: Ben Arc <ben@arc.wales>
2022-02-14 17:54:05 +01:00
Ben Arc 0367ee85a7 removed dense from lower buttons 2022-02-14 15:51:45 +00:00
Ben Arc 0aef9d4d9a Made dense to save space 2022-02-14 15:48:56 +00:00
Ben Arc 8967011cbe Merge branch 'mobileui' into main 2022-02-14 15:38:17 +00:00
Ben Arc 682226c4b6 chnaged button names 2022-02-14 15:36:10 +00:00
Ben Arc c7e501ac08 Adds lower buttons to wallet UI for smaller screens 2022-02-14 15:29:10 +00:00
calleandGitHub 8d7302e4cd fix bleskomat test (#523) 2022-02-14 13:49:15 +01:00
callebtc 09b86605dd fix bleskomat tests 2022-02-14 13:42:43 +01:00
orangesurfandGitHub 9e8376b3fb Remove suggestion to switch to FastAPI branch (#520)
Branch does not exist
2022-02-14 13:38:22 +01:00
ArcandGitHub 1a492cbfb8 Update installation.md 2022-02-14 11:42:19 +00:00
ArcandGitHub 155c32504a Update api.py 2022-02-14 11:31:42 +00:00
ArcandGitHub 8c923ad653 Update README.md 2022-02-14 09:23:04 +00:00
ArcandGitHub 25b9014c3d Update README.md 2022-02-14 09:22:23 +00:00
ArcandGitHub 9807e7d646 Merge pull request #518 from talvasconcelos/fix/disapearingAdminExt
Disapearing Extensions in Admin Only
2022-02-11 11:17:55 +00:00
ArcandGitHub 728ab517f5 Merge branch 'main' into fix/disapearingAdminExt 2022-02-11 11:17:37 +00:00
benarc f1fb569e2b lnurlpayout fix 2022-02-11 10:03:26 +00:00
Tiago vasconcelos 6d58027147 remove print 2022-02-10 17:06:15 +00:00
Tiago vasconcelos 4a35f84e95 fix admin default false 2022-02-10 16:44:47 +00:00
ArcandGitHub f7b37fb843 Merge pull request #498 from pseudozach/main
fix livestream startup issue
2022-02-10 15:24:05 +00:00
ArcandGitHub 699bdd9a06 Merge pull request #497 from bitkarrot/main
fix curl example paths in API docs on LNURLw extension
2022-02-10 15:21:28 +00:00
benarc b5aad94ecd rolled back admin fix 2022-02-10 13:55:45 +00:00
benarc 0f79213b0f Tals admin extension fix 2022-02-10 10:09:44 +00:00
ArcandGitHub c1654a21d4 Merge pull request #515 from lnbits/revert-513-fix/disapearingAdminExt
Revert "fix disapearing admin extension"
2022-02-10 09:56:15 +00:00
ArcandGitHub da6a2e772b Revert "fix disapearing admin extension" 2022-02-10 09:56:02 +00:00
ArcandGitHub 1626cd2b4b Merge pull request #513 from talvasconcelos/fix/disapearingAdminExt
fix disapearing admin extension
2022-02-10 09:55:14 +00:00
Tiago vasconcelos d1d62583c4 fix for whitespace in admin users list 2022-02-09 11:16:13 +00:00
Tiago vasconcelos 4c18eb0b34 fix disapearing admin extension 2022-02-09 10:57:24 +00:00
pseudozach b00a39a7ce fix livestream startup issue 2022-01-24 15:58:51 -08:00
BitkarrotandGitHub b4dc07170f fix curl example paths 2022-01-24 14:51:39 -08:00
69 changed files with 1247 additions and 802 deletions
+7 -8
View File
@@ -11,6 +11,9 @@ LNBITS_ADMIN_USERS=""
LNBITS_ADMIN_EXTENSIONS="ngrok" LNBITS_ADMIN_EXTENSIONS="ngrok"
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet" LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
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 # Disable extensions for all users, use "all" to disable all extensions
LNBITS_DISABLED_EXTENSIONS="amilk" LNBITS_DISABLED_EXTENSIONS="amilk"
@@ -30,9 +33,9 @@ LNBITS_SITE_TITLE="LNbits"
LNBITS_SITE_TAGLINE="free and open-source lightning wallet" LNBITS_SITE_TAGLINE="free and open-source lightning wallet"
LNBITS_SITE_DESCRIPTION="Some description about your service, will display if title is not 'LNbits'" LNBITS_SITE_DESCRIPTION="Some description about your service, will display if title is not 'LNbits'"
# Choose from mint, flamingo, salvador, autumn, monochrome, classic # Choose from mint, flamingo, salvador, autumn, monochrome, classic
LNBITS_THEME_OPTIONS="mint, flamingo, classic, autumn, monochrome, salvador" LNBITS_THEME_OPTIONS="classic, bitcoin, flamingo, mint, autumn, monochrome, salvador"
# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, LndWallet (gRPC), # Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet,
# LndRestWallet, CLightningWallet, LNbitsWallet, SparkWallet, FakeWallet # LndRestWallet, CLightningWallet, LNbitsWallet, SparkWallet, FakeWallet
LNBITS_BACKEND_WALLET_CLASS=VoidWallet LNBITS_BACKEND_WALLET_CLASS=VoidWallet
# VoidWallet is just a fallback that works without any actual Lightning capabilities, # VoidWallet is just a fallback that works without any actual Lightning capabilities,
@@ -50,16 +53,12 @@ CLIGHTNING_RPC="/home/bob/.lightning/bitcoin/lightning-rpc"
LNBITS_ENDPOINT=https://legend.lnbits.com LNBITS_ENDPOINT=https://legend.lnbits.com
LNBITS_KEY=LNBITS_ADMIN_KEY 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 or HEXSTRING"
# LndRestWallet # LndRestWallet
LND_REST_ENDPOINT=https://127.0.0.1:8080/ 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_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert"
LND_REST_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/admin.macaroon or 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 # LNPayWallet
LNPAY_API_ENDPOINT=https://api.lnpay.co/v1/ LNPAY_API_ENDPOINT=https://api.lnpay.co/v1/
+1
View File
@@ -9,4 +9,5 @@ jobs:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- uses: jpetrucciani/mypy-check@master - uses: jpetrucciani/mypy-check@master
with: with:
mypy_flags: '--install-types --non-interactive'
path: lnbits path: lnbits
-58
View File
@@ -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" ./
+68
View File
@@ -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" ./
+6 -2
View File
@@ -8,8 +8,9 @@ ENV PATH="$VIRTUAL_ENV/bin:$PATH"
# Install build deps # Install build deps
RUN apt-get update RUN apt-get update
RUN apt-get install -y --no-install-recommends build-essential RUN apt-get install -y --no-install-recommends build-essential pkg-config
RUN python -m pip install --upgrade pip RUN python -m pip install --upgrade pip
RUN pip install wheel
# Install runtime deps # Install runtime deps
COPY requirements.txt /tmp/requirements.txt COPY requirements.txt /tmp/requirements.txt
@@ -36,6 +37,9 @@ ENV PATH="$VIRTUAL_ENV/bin:$PATH"
WORKDIR /app WORKDIR /app
COPY --chown=1000:1000 lnbits /app/lnbits COPY --chown=1000:1000 lnbits /app/lnbits
ENV LNBITS_PORT="5000"
ENV LNBITS_HOST="0.0.0.0"
EXPOSE 5000 EXPOSE 5000
CMD ["uvicorn", "lnbits.__main__:app", "--port", "5000", "--host", "0.0.0.0"] CMD ["sh", "-c", "uvicorn lnbits.__main__:app --port $LNBITS_PORT --host $LNBITS_HOST"]
+1
View File
@@ -30,6 +30,7 @@ sse-starlette = "*"
jinja2 = "3.0.1" jinja2 = "3.0.1"
pyngrok = "*" pyngrok = "*"
secp256k1 = "*" secp256k1 = "*"
pycryptodomex = "*"
[dev-packages] [dev-packages]
black = "==20.8b1" black = "==20.8b1"
Generated
+97 -67
View File
@@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "bd78144379115a1566549f5f2d7dba7a96539fb5893b3999e61bc13c6847827e" "sha256": "3e19364434fd2db3748162ccc1f3b6bddcf7a382473069d15cee6eda5e07eef1"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@@ -146,11 +146,11 @@
}, },
"charset-normalizer": { "charset-normalizer": {
"hashes": [ "hashes": [
"sha256:2842d8f5e82a1f6aa437380934d5e1cd4fcf2003b06fed6940769c164a480a45", "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597",
"sha256:98398a9d69ee80548c762ba991a4728bfc3836768ed226b3945908d1a688371c" "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"
], ],
"markers": "python_version >= '3.5'", "markers": "python_version >= '3.5'",
"version": "==2.0.11" "version": "==2.0.12"
}, },
"click": { "click": {
"hashes": [ "hashes": [
@@ -201,11 +201,11 @@
}, },
"httpcore": { "httpcore": {
"hashes": [ "hashes": [
"sha256:2621ee769d0236574df51b305c5f4c69ca8f0c7b215221ad247b1ee42a9a9de1", "sha256:47d772f754359e56dd9d892d9593b6f9870a37aeb8ba51e9a88b09b3d68cfade",
"sha256:435ab519628a6e2393f67812dea3ca5c6ad23b457412cd119295d9f906d96a2b" "sha256:7503ec1c0f559066e7e39bc4003fd2ce023d01cf51793e3c173b864eb456ead1"
], ],
"markers": "python_version >= '3.6'", "markers": "python_version >= '3.6'",
"version": "==0.14.5" "version": "==0.14.7"
}, },
"httptools": { "httptools": {
"hashes": [ "hashes": [
@@ -427,6 +427,39 @@
], ],
"version": "==2.21" "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": { "pydantic": {
"hashes": [ "hashes": [
"sha256:085ca1de245782e9b46cefcf99deecc67d418737a1fd3f6a4f511344b613a5b3", "sha256:085ca1de245782e9b46cefcf99deecc67d418737a1fd3f6a4f511344b613a5b3",
@@ -684,22 +717,22 @@
}, },
"typing-extensions": { "typing-extensions": {
"hashes": [ "hashes": [
"sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e", "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42",
"sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b" "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"
], ],
"index": "pypi", "index": "pypi",
"version": "==4.0.1" "version": "==4.1.1"
}, },
"uvicorn": { "uvicorn": {
"extras": [ "extras": [
"standard" "standard"
], ],
"hashes": [ "hashes": [
"sha256:8b16d9ecb76500f7804184f182835fe8a2b54716d3b0b6bb2da0b2b192f62c73", "sha256:25850bbc86195a71a6477b3e4b3b7b4c861fb687fb96912972ce5324472b1011",
"sha256:dffbacb8cc25d924d68d231d2c478c4fe6727c36537d8de21e5de591b37afc41" "sha256:e85872d84fb651cccc4c5d2a71cf7ead055b8fb4d8f1e78e36092282c0cf2aec"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.17.1" "version": "==0.17.4"
}, },
"uvloop": { "uvloop": {
"hashes": [ "hashes": [
@@ -819,53 +852,50 @@
"toml" "toml"
], ],
"hashes": [ "hashes": [
"sha256:012157499ec4f135fc36cd2177e3d1a1840af9b236cbe80e9a5ccfc83d912a69", "sha256:1245ab82e8554fa88c4b2ab1e098ae051faac5af829efdcf2ce6b34dccd5567c",
"sha256:0a34d313105cdd0d3644c56df2d743fe467270d6ab93b5d4a347eb9fec8924d6", "sha256:1bc6d709939ff262fd1432f03f080c5042dc6508b6e0d3d20e61dd045456a1a0",
"sha256:11e61c5548ecf74ea1f8b059730b049871f0e32b74f88bd0d670c20c819ad749", "sha256:25e73d4c81efa8ea3785274a2f7f3bfbbeccb6fcba2a0bdd3be9223371c37554",
"sha256:152cc2624381df4e4e604e21bd8e95eb8059535f7b768c1fb8b8ae0b26f47ab0", "sha256:276b13cc085474e482566c477c25ed66a097b44c6e77132f3304ac0b039f83eb",
"sha256:1b4285fde5286b946835a1a53bba3ad41ef74285ba9e8013e14b5ea93deaeafc", "sha256:2aed4761809640f02e44e16b8b32c1a5dee5e80ea30a0ff0912158bde9c501f2",
"sha256:27a94db5dc098c25048b0aca155f5fac674f2cf1b1736c5272ba28ead2fc267e", "sha256:2dd70a167843b4b4b2630c0c56f1b586fe965b4f8ac5da05b6690344fd065c6b",
"sha256:27ac7cb84538e278e07569ceaaa6f807a029dc194b1c819a9820b9bb5dbf63ab", "sha256:352c68e233409c31048a3725c446a9e48bbff36e39db92774d4f2380d630d8f8",
"sha256:2a491e159294d756e7fc8462f98175e2d2225e4dbe062cca7d3e0d5a75ba6260", "sha256:3f2b05757c92ad96b33dbf8e8ec8d4ccb9af6ae3c9e9bd141c7cc44d20c6bcba",
"sha256:2bc85664b06ba42d14bb74d6ddf19d8bfc520cb660561d2d9ce5786ae72f71b5", "sha256:448d7bde7ceb6c69e08474c2ddbc5b4cd13c9e4aa4a717467f716b5fc938a734",
"sha256:32168001f33025fd756884d56d01adebb34e6c8c0b3395ca8584cdcee9c7c9d2", "sha256:463e52616ea687fd323888e86bf25e864a3cc6335a043fad6bbb037dbf49bbe2",
"sha256:3c4ce3b647bd1792d4394f5690d9df6dc035b00bcdbc5595099c01282a59ae01", "sha256:482fb42eea6164894ff82abbcf33d526362de5d1a7ed25af7ecbdddd28fc124f",
"sha256:433b99f7b0613bdcdc0b00cc3d39ed6d756797e3b078d2c43f8a38288520aec6", "sha256:56c4a409381ddd7bbff134e9756077860d4e8a583d310a6f38a2315b9ce301d0",
"sha256:4578728c36de2801c1deb1c6b760d31883e62e33f33c7ba8f982e609dc95167d", "sha256:56d296cbc8254a7dffdd7bcc2eb70be5a233aae7c01856d2d936f5ac4e8ac1f1",
"sha256:509c68c3e2015022aeda03b003dd68fa19987cdcf64e9d4edc98db41cfc45d30", "sha256:5e15d424b8153756b7c903bde6d4610be0c3daca3986173c18dd5c1a1625e4cd",
"sha256:51372e24b1f7143ee2df6b45cff6a721f3abe93b1e506196f3ffa4155c2497f7", "sha256:618eeba986cea7f621d8607ee378ecc8c2504b98b3fdc4952b30fe3578304687",
"sha256:5d008e0f67ac800b0ca04d7914b8501312c8c6c00ad8c7ba17754609fae1231a", "sha256:61d47a897c1e91f33f177c21de897267b38fbb45f2cd8e22a710bcef1df09ac1",
"sha256:649df3641eb351cdfd0d5533c92fc9df507b6b2bf48a7ef8c71ab63cbc7b5c3c", "sha256:621f6ea7260ea2ffdaec64fe5cb521669984f567b66f62f81445221d4754df4c",
"sha256:6e78b1e25e5c5695dea012be473e442f7094d066925604be20b30713dbd47f89", "sha256:6a5cdc3adb4f8bb8d8f5e64c2e9e282bc12980ef055ec6da59db562ee9bdfefa",
"sha256:72d9d186508325a456475dd05b1756f9a204c7086b07fffb227ef8cee03b1dc2", "sha256:6c3f6158b02ac403868eea390930ae64e9a9a2a5bbfafefbb920d29258d9f2f8",
"sha256:7d82c610a2e10372e128023c5baf9ce3d270f3029fe7274ff5bc2897c68f1318", "sha256:704f89b87c4f4737da2860695a18c852b78ec7279b24eedacab10b29067d3a38",
"sha256:7ee317486593193e066fc5e98ac0ce712178c21529a85c07b7cb978171f25d53", "sha256:72128176fea72012063200b7b395ed8a57849282b207321124d7ff14e26988e8",
"sha256:7eed8459a2b81848cafb3280b39d7d49950d5f98e403677941c752e7e7ee47cb", "sha256:78fbb2be068a13a5d99dce9e1e7d168db880870f7bc73f876152130575bd6167",
"sha256:823f9325283dc9565ba0aa2d240471a93ca8999861779b2b6c7aded45b58ee0f", "sha256:7bff3a98f63b47464480de1b5bdd80c8fade0ba2832c9381253c9b74c4153c27",
"sha256:85c5fc9029043cf8b07f73fbb0a7ab6d3b717510c3b5642b77058ea55d7cacde", "sha256:84f2436d6742c01136dd940ee158bfc7cf5ced3da7e4c949662b8703b5cd8145",
"sha256:86c91c511853dfda81c2cf2360502cb72783f4b7cebabef27869f00cbe1db07d", "sha256:9976fb0a5709988778ac9bc44f3d50fccd989987876dfd7716dee28beed0a9fa",
"sha256:8e0c3525b1a182c8ffc9bca7e56b521e0c2b8b3e82f033c8e16d6d721f1b54d6", "sha256:9ad0a117b8dc2061ce9461ea4c1b4799e55edceb236522c5b8f958ce9ed8fa9a",
"sha256:987a84ff98a309994ca77ed3cc4b92424f824278e48e4bf7d1bb79a63cfe2099", "sha256:9e3dd806f34de38d4c01416344e98eab2437ac450b3ae39c62a0ede2f8b5e4ed",
"sha256:9ed3244b415725f08ca3bdf02ed681089fd95e9465099a21c8e2d9c5d6ca2606", "sha256:9eb494070aa060ceba6e4bbf44c1bc5fa97bfb883a0d9b0c9049415f9e944793",
"sha256:a189036c50dcd56100746139a459f0d27540fef95b09aba03e786540b8feaa5f", "sha256:9fde6b90889522c220dd56a670102ceef24955d994ff7af2cb786b4ba8fe11e4",
"sha256:a4748349734110fd32d46ff8897b561e6300d8989a494ad5a0a2e4f0ca974fc7", "sha256:9fff3ff052922cb99f9e52f63f985d4f7a54f6b94287463bc66b7cdf3eb41217",
"sha256:a5d79c9af3f410a2b5acad91258b4ae179ee9c83897eb9de69151b179b0227f5", "sha256:a06c358f4aed05fa1099c39decc8022261bb07dfadc127c08cfbd1391b09689e",
"sha256:a7596aa2f2b8fa5604129cfc9a27ad9beec0a96f18078cb424d029fdd707468d", "sha256:a4f923b9ab265136e57cc14794a15b9dcea07a9c578609cd5dbbfff28a0d15e6",
"sha256:ab4fc4b866b279740e0d917402f0e9a08683e002f43fa408e9655818ed392196", "sha256:c5b81fb37db76ebea79aa963b76d96ff854e7662921ce742293463635a87a78d",
"sha256:bde4aeabc0d1b2e52c4036c54440b1ad05beeca8113f47aceb4998bb7471e2c2", "sha256:d5ed164af5c9078596cfc40b078c3b337911190d3faeac830c3f1274f26b8320",
"sha256:c72bb4679283c6737f452eeb9b2a0e570acaef2197ad255fb20162adc80bea76", "sha256:d651fde74a4d3122e5562705824507e2f5b2d3d57557f1916c4b27635f8fbe3f",
"sha256:c8582e9280f8d0f38114fe95a92ae8d0790b56b099d728cc4f8a2e14b1c4a18c", "sha256:de73fca6fb403dd72d4da517cfc49fcf791f74eee697d3219f6be29adf5af6ce",
"sha256:ca29c352389ea27a24c79acd117abdd8a865c6eb01576b6f0990cd9a4e9c9f48", "sha256:e647a0be741edbb529a72644e999acb09f2ad60465f80757da183528941ff975",
"sha256:ce443a3e6df90d692c38762f108fc4c88314bf477689f04de76b3f252e7a351c", "sha256:e92c7a5f7d62edff50f60a045dc9542bf939758c95b2fcd686175dd10ce0ed10",
"sha256:d1675db48490e5fa0b300f6329ecb8a9a37c29b9ab64fa9c964d34111788ca2d", "sha256:eeffd96882d8c06d31b65dddcf51db7c612547babc1c4c5db6a011abe9798525",
"sha256:da1a428bdbe71f9a8c270c7baab29e9552ac9d0e0cba5e7e9a4c9ee6465d258d", "sha256:f5a4551dfd09c3bd12fca8144d47fe7745275adf3229b7223c2f9e29a975ebda",
"sha256:e4ff163602c5c77e7bb4ea81ba5d3b793b4419f8acd296aae149370902cf4e92", "sha256:fac0bcc5b7e8169bffa87f0dcc24435446d329cbc2b5486d155c2e0f3b493ae1"
"sha256:e67ccd53da5958ea1ec833a160b96357f90859c220a00150de011b787c27b98d",
"sha256:e8071e7d9ba9f457fc674afc3de054450be2c9b195c470147fbbc082468d8ff7",
"sha256:fff16a30fdf57b214778eff86391301c4509e327a65b877862f7c929f10a4253"
], ],
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.7'",
"version": "==6.3" "version": "==6.3.1"
}, },
"iniconfig": { "iniconfig": {
"hashes": [ "hashes": [
@@ -948,11 +978,11 @@
}, },
"pytest": { "pytest": {
"hashes": [ "hashes": [
"sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89", "sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db",
"sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134" "sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171"
], ],
"index": "pypi", "index": "pypi",
"version": "==6.2.5" "version": "==7.0.1"
}, },
"pytest-cov": { "pytest-cov": {
"hashes": [ "hashes": [
@@ -1051,10 +1081,10 @@
}, },
"tomli": { "tomli": {
"hashes": [ "hashes": [
"sha256:b5bde28da1fed24b9bd1d4d2b8cba62300bfb4ec9a6187a957e8ddb9434c5224", "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
"sha256:c292c34f58502a1eb2bbb9f5bbc9a5ebc37bee10ffb8c2d6bbdfa8eb13cc14e1" "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
], ],
"version": "==2.0.0" "version": "==2.0.1"
}, },
"typed-ast": { "typed-ast": {
"hashes": [ "hashes": [
@@ -1088,11 +1118,11 @@
}, },
"typing-extensions": { "typing-extensions": {
"hashes": [ "hashes": [
"sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e", "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42",
"sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b" "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"
], ],
"index": "pypi", "index": "pypi",
"version": "==4.0.1" "version": "==4.1.1"
} }
} }
} }
+2 -3
View File
@@ -1,9 +1,6 @@
LNbits LNbits
====== ======
[![github-tests-badge]][github-tests]
[![github-mypy-badge]][github-mypy]
[![codecov-badge]][codecov]
[![license-badge]](LICENSE) [![license-badge]](LICENSE)
[![docs-badge]][docs] [![docs-badge]][docs]
@@ -14,6 +11,8 @@ LNbits
(Join us on [https://t.me/lnbits](https://t.me/lnbits)) (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! 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: LNbits is a very simple Python server that sits on top of any funding source, and can be used as:
+11 -3
View File
@@ -12,13 +12,18 @@ LNbits uses [Pipenv][pipenv] to manage Python packages.
```sh ```sh
git clone https://github.com/lnbits/lnbits-legend.git git clone https://github.com/lnbits/lnbits-legend.git
cd lnbits-legend/ cd lnbits-legend/
sudo apt-get install pipenv
pipenv shell pipenv shell
# pipenv --python 3.8 shell (if you wish to use a version of Python higher than 3.7) # pipenv --python 3.9 shell (if you wish to use a version of Python higher than 3.7)
pipenv install --dev pipenv install --dev
# pipenv --python 3.8 install --dev (if you wish to use a version of Python higher than 3.7) # pipenv --python 3.9 install --dev (if you wish to use a version of Python higher than 3.7)
# If any of the modules fails to install, try checking and upgrading your setupTool module # If any of the modules fails to install, try checking and upgrading your setupTool module
# pip install -U setuptools # pip install -U setuptools
# install libffi/libpq in case "pipenv install" fails
# sudo apt-get install -y libffi-dev libpq-dev
``` ```
## Running the server ## Running the server
@@ -41,4 +46,7 @@ E.g. when you want to use LND you have to `pipenv run pip install lndgrpc` and `
Take a look at [Polar][polar] for an excellent way of spinning up a Lightning Network dev environment. Take a look at [Polar][polar] for an excellent way of spinning up a Lightning Network dev environment.
**Note**: We reccomend using <a href="https://caddyserver.com/docs/install#debian-ubuntu-raspbian">Caddy</a> for a reverse-proxy, if you want to serve your install through a domain, alternatively you can use [ngrok](https://ngrok.com/). **Notes**:
* We reccomend using <a href="https://caddyserver.com/docs/install#debian-ubuntu-raspbian">Caddy</a> for a reverse-proxy if you want to serve your install through a domain, alternatively you can use [ngrok](https://ngrok.com/).
* <a href="https://linuxize.com/post/how-to-use-linux-screen/#starting-linux-screen">Screen</a> works well if you want LNbits to continue running when you close your terminal session.
-1
View File
@@ -28,7 +28,6 @@ Download this repo and install the dependencies:
git clone https://github.com/lnbits/lnbits-legend.git git clone https://github.com/lnbits/lnbits-legend.git
cd lnbits-legend/ cd lnbits-legend/
# ensure you have virtualenv installed, on debian/ubuntu 'apt install python3-venv' should work # ensure you have virtualenv installed, on debian/ubuntu 'apt install python3-venv' should work
# for now you'll need to `git checkout FastAPI`
python3 -m venv venv python3 -m venv venv
./venv/bin/pip install -r requirements.txt ./venv/bin/pip install -r requirements.txt
cp .env.example .env cp .env.example .env
+10 -2
View File
@@ -36,16 +36,24 @@ Using this wallet requires the installation of the `grpcio` and `protobuf` Pytho
- `LND_GRPC_ENDPOINT`: ip_address - `LND_GRPC_ENDPOINT`: ip_address
- `LND_GRPC_PORT`: port - `LND_GRPC_PORT`: port
- `LND_GRPC_CERT`: /file/path/tls.cert - `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) ### LND (REST)
- `LNBITS_BACKEND_WALLET_CLASS`: **LndRestWallet** - `LNBITS_BACKEND_WALLET_CLASS`: **LndRestWallet**
- `LND_REST_ENDPOINT`: ip_address - `LND_REST_ENDPOINT`: ip_address
- `LND_REST_CERT`: /file/path/tls.cert - `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 ### LNbits
+9 -8
View File
@@ -84,18 +84,19 @@ def create_app(config_object="lnbits.settings") -> FastAPI:
def check_funding_source(app: FastAPI) -> None: def check_funding_source(app: FastAPI) -> None:
@app.on_event("startup") @app.on_event("startup")
async def check_wallet_status(): async def check_wallet_status():
error_message, balance = await WALLET.status() while True:
if error_message: error_message, balance = await WALLET.status()
if not error_message:
break
warnings.warn( warnings.warn(
f" × The backend for {WALLET.__class__.__name__} isn't working properly: '{error_message}'", f" × The backend for {WALLET.__class__.__name__} isn't working properly: '{error_message}'",
RuntimeWarning, RuntimeWarning,
) )
print("Retrying connection to backend in 5 seconds...")
sys.exit(4) await asyncio.sleep(5)
else: print(
print( f" ✔️ {WALLET.__class__.__name__} seems to be connected and with a balance of {balance} msat."
f" ✔️ {WALLET.__class__.__name__} seems to be connected and with a balance of {balance} msat." )
)
def register_routes(app: FastAPI) -> None: def register_routes(app: FastAPI) -> None:
+16 -2
View File
@@ -6,7 +6,7 @@ from urllib.parse import urlparse
from lnbits import bolt11 from lnbits import bolt11
from lnbits.db import Connection, POSTGRES, COCKROACH from lnbits.db import Connection, POSTGRES, COCKROACH
from lnbits.settings import DEFAULT_WALLET_NAME from lnbits.settings import DEFAULT_WALLET_NAME, LNBITS_ADMIN_USERS
from . import db from . import db
from .models import User, Wallet, Payment, BalanceCheck from .models import User, Wallet, Payment, BalanceCheck
@@ -61,6 +61,9 @@ async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[
email=user["email"], email=user["email"],
extensions=[e[0] for e in extensions], extensions=[e[0] for e in extensions],
wallets=[Wallet(**w) for w in wallets], 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,
) )
@@ -216,6 +219,8 @@ async def get_payments(
incoming: bool = False, incoming: bool = False,
since: Optional[int] = None, since: Optional[int] = None,
exclude_uncheckable: bool = False, exclude_uncheckable: bool = False,
limit: Optional[int] = None,
offset: Optional[int] = None,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> List[Payment]: ) -> List[Payment]:
""" """
@@ -260,6 +265,15 @@ async def get_payments(
clause.append("checking_id NOT LIKE 'temp_%'") clause.append("checking_id NOT LIKE 'temp_%'")
clause.append("checking_id NOT LIKE 'internal_%'") 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 = "" where = ""
if clause: if clause:
where = f"WHERE {' AND '.join(clause)}" where = f"WHERE {' AND '.join(clause)}"
@@ -270,10 +284,10 @@ async def get_payments(
FROM apipayments FROM apipayments
{where} {where}
ORDER BY time DESC ORDER BY time DESC
{limit_offset_clause}
""", """,
tuple(args), tuple(args),
) )
return [Payment.from_row(row) for row in rows] return [Payment.from_row(row) for row in rows]
+19 -23
View File
@@ -85,18 +85,17 @@ async def pay_invoice(
description: str = "", description: str = "",
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> str: ) -> 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: async with (db.reuse_conn(conn) if conn else db.connect()) as conn:
temp_id = f"temp_{urlsafe_short_hash()}" temp_id = f"temp_{urlsafe_short_hash()}"
internal_id = f"internal_{urlsafe_short_hash()}" internal_id = f"internal_{urlsafe_short_hash()}"
invoice = bolt11.decode(payment_request)
if invoice.amount_msat == 0: if invoice.amount_msat == 0:
raise ValueError("Amountless invoices not supported.") raise ValueError("Amountless invoices not supported.")
if max_sat and invoice.amount_msat > max_sat * 1000: if max_sat and invoice.amount_msat > max_sat * 1000:
raise ValueError("Amount in invoice is too high.") raise ValueError("Amount in invoice is too high.")
wallet = await get_wallet(wallet_id, conn=conn)
# put all parameters that don't change here # put all parameters that don't change here
PaymentKwargs = TypedDict( PaymentKwargs = TypedDict(
"PaymentKwargs", "PaymentKwargs",
@@ -134,26 +133,20 @@ async def pay_invoice(
# the balance is enough in the next step # the balance is enough in the next step
await create_payment( await create_payment(
checking_id=temp_id, checking_id=temp_id,
fee=-fee_reserve(invoice.amount_msat), fee=-fee_reserve_msat,
conn=conn, conn=conn,
**payment_kwargs, **payment_kwargs,
) )
# do the balance check if internal payment # do the balance check
if internal_checking_id: wallet = await get_wallet(wallet_id, conn=conn)
wallet = await get_wallet(wallet_id, conn=conn) assert wallet
assert wallet if wallet.balance_msat < 0:
if wallet.balance_msat < 0: if not internal_checking_id and wallet.balance_msat > -fee_reserve_msat:
raise PermissionError("Insufficient balance.") raise PaymentFailure(
f"You must reserve at least 1% ({round(fee_reserve_msat/1000)} sat) to cover potential routing fees."
# do the balance check if external payment
else:
if invoice.amount_msat > wallet.balance_msat - (
wallet.balance_msat / 100 * 2
):
raise PermissionError(
"LNbits requires you keep at least 2% reserve to cover potential routing fees."
) )
raise PermissionError("Insufficient balance.")
if internal_checking_id: if internal_checking_id:
# mark the invoice from the other side as not pending anymore # mark the invoice from the other side as not pending anymore
@@ -171,7 +164,9 @@ async def pay_invoice(
await internal_invoice_queue.put(internal_checking_id) await internal_invoice_queue.put(internal_checking_id)
else: else:
# actually pay the external invoice # actually pay the external invoice
payment: PaymentResponse = await WALLET.pay_invoice(payment_request) payment: PaymentResponse = await WALLET.pay_invoice(
payment_request, fee_reserve_msat
)
if payment.checking_id: if payment.checking_id:
async with db.connect() as conn: async with db.connect() as conn:
await create_payment( await create_payment(
@@ -286,12 +281,12 @@ async def perform_lnurlauth(
sign_len = 6 + r_len + s_len sign_len = 6 + r_len + s_len
signature = BytesIO() 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((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_len.to_bytes(1, "big", signed=False))
signature.write(r) 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_len.to_bytes(1, "big", signed=False))
signature.write(s) signature.write(s)
@@ -340,5 +335,6 @@ async def check_invoice_status(
return status 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: def fee_reserve(amount_msat: int) -> int:
return max(1000, int(amount_msat * 0.01)) return max(2000, int(amount_msat * 0.01))
+2 -2
View File
@@ -364,12 +364,12 @@ new Vue({
}, },
decodeRequest: function () { decodeRequest: function () {
this.parse.show = true this.parse.show = true
let req = this.parse.data.request.toLowerCase()
if (this.parse.data.request.startsWith('lightning:')) { if (this.parse.data.request.startsWith('lightning:')) {
this.parse.data.request = this.parse.data.request.slice(10) this.parse.data.request = this.parse.data.request.slice(10)
} else if (this.parse.data.request.startsWith('lnurl:')) { } else if (this.parse.data.request.startsWith('lnurl:')) {
this.parse.data.request = this.parse.data.request.slice(6) 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 this.parse.data.request = this.parse.data.request
.split('lightning=')[1] .split('lightning=')[1]
.split('&')[0] .split('&')[0]
+1 -1
View File
@@ -61,7 +61,7 @@
<code <code
>curl -X POST {{ request.base_url }}api/v1/payments -d '{"out": false, >curl -X POST {{ request.base_url }}api/v1/payments -d '{"out": false,
"amount": &lt;int&gt;, "memo": &lt;string&gt;, "webhook": "amount": &lt;int&gt;, "memo": &lt;string&gt;, "webhook":
&lt;url:string&gt;}' -H "X-Api-Key: <i>{{ wallet.inkey }}</i>" -H &lt;url:string&gt;, "unit": &lt;string&gt;}' -H "X-Api-Key: <i>{{ wallet.inkey }}</i>" -H
"Content-type: application/json"</code "Content-type: application/json"</code
> >
</q-card-section> </q-card-section>
+1 -1
View File
@@ -11,7 +11,7 @@
color="primary" color="primary"
@click="processing" @click="processing"
type="a" type="a"
href="{{ url_for('core.lnurlwallet', lightning=lnurl) }}" href="{{ url_for('core.lnurlwallet') }}?lightning={{ lnurl }}"
> >
Press to claim bitcoin Press to claim bitcoin
</q-btn> </q-btn>
+441 -397
View File
@@ -62,7 +62,7 @@
</q-btn> </q-btn>
</h3> </h3>
</q-card-section> </q-card-section>
<div class="row q-pb-md q-px-md q-col-gutter-md"> <div class="row q-pb-md q-px-md q-col-gutter-md gt-sm">
<div class="col"> <div class="col">
<q-btn <q-btn
unelevated unelevated
@@ -273,426 +273,470 @@
</q-card-section> </q-card-section>
</q-card> </q-card>
</div> </div>
<div class="col-12 col-md-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-mt-none q-mb-sm">
{{ SITE_TITLE }} Wallet: <strong><em>{{ wallet.name }}</em></strong>
</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list> {% if HIDE_API %}
{% include "core/_api_docs.html" %} <div class="col-12 col-md-4 q-gutter-y-md">
{% else %}
<div class="col-12 col-md-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-mt-none q-mb-sm">
{{ SITE_TITLE }} Wallet: <strong><em>{{ wallet.name }}</em></strong>
</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator> <q-separator></q-separator>
{% if wallet.lnurlwithdraw_full %} <q-list>
<q-expansion-item group="extras" icon="crop_free" label="Drain Funds"> {% include "core/_api_docs.html" %}
<q-card> <q-separator></q-separator>
<q-card-section class="text-center">
<p> {% if wallet.lnurlwithdraw_full %}
This is an LNURL-withdraw QR code for slurping everything from <q-expansion-item
this wallet. Do not share with anyone. group="extras"
</p> icon="crop_free"
<a href="lightning:{{wallet.lnurlwithdraw_full}}"> label="Drain Funds"
>
<q-card>
<q-card-section class="text-center">
<p>
This is an LNURL-withdraw QR code for slurping everything
from this wallet. Do not share with anyone.
</p>
<a href="lightning:{{wallet.lnurlwithdraw_full}}">
<qrcode
value="{{wallet.lnurlwithdraw_full}}"
:options="{width:240}"
></qrcode>
</a>
<p>
It is compatible with <code>balanceCheck</code> and
<code>balanceNotify</code> so your wallet may keep pulling
the funds continuously from here after the first withdraw.
</p>
</q-card-section>
</q-card>
</q-expansion-item>
<q-separator></q-separator>
{% endif %}
<q-expansion-item
group="extras"
icon="settings_cell"
label="Export to Phone with QR Code"
>
<q-card>
<q-card-section class="text-center">
<p>
This QR code contains your wallet URL with full access. You
can scan it from your phone to open your wallet from there.
</p>
<qrcode <qrcode
value="{{wallet.lnurlwithdraw_full}}" :value="'{{request.base_url}}' +'wallet?usr={{user.id}}&wal={{wallet.id}}'"
:options="{width:240}" :options="{width:240}"
></qrcode> ></qrcode>
</a> </q-card-section>
<p> </q-card>
It is compatible with <code>balanceCheck</code> and </q-expansion-item>
<code>balanceNotify</code> so your wallet may keep pulling the <q-separator></q-separator>
funds continuously from here after the first withdraw. <q-expansion-item group="extras" icon="edit" label="Rename wallet">
</p> <q-card>
</q-card-section> <q-card-section>
</q-card> <div class="" style="max-width: 320px">
</q-expansion-item> <q-input
<q-separator></q-separator> filled
{% endif %} v-model.trim="newName"
label="Label"
<q-expansion-item dense="dense"
group="extras" @update:model-value="(e) => console.log(e)"
icon="settings_cell" />
label="Export to Phone with QR Code" </div>
> <q-btn
<q-card> :disable="!newName.length"
<q-card-section class="text-center"> unelevated
<p> class="q-mt-sm"
This QR code contains your wallet URL with full access. You color="primary"
can scan it from your phone to open your wallet from there. @click="updateWalletName()"
</p> >Update name</q-btn
<qrcode >
:value="'{{request.base_url}}' +'wallet?usr={{user.id}}&wal={{wallet.id}}'" </q-card-section>
:options="{width:240}" </q-card>
></qrcode> </q-expansion-item>
</q-card-section> <q-separator></q-separator>
</q-card> <q-expansion-item
</q-expansion-item> group="extras"
<q-separator></q-separator> icon="remove_circle"
<q-expansion-item group="extras" icon="edit" label="Rename wallet"> label="Delete wallet"
<q-card> >
<q-card-section> <q-card>
<div class="" style="max-width: 320px"> <q-card-section>
<q-input <p>
filled This whole wallet will be deleted, the funds will be
v-model.trim="newName" <strong>UNRECOVERABLE</strong>.
label="Label" </p>
dense="dense" <q-btn
@update:model-value="(e) => console.log(e)" unelevated
/> color="red-10"
</div> @click="deleteWallet('{{ wallet.id }}', '{{ user.id }}')"
<q-btn >Delete wallet</q-btn
:disable="!newName.length" >
unelevated </q-card-section>
class="q-mt-sm" </q-card>
color="primary" </q-expansion-item>
@click="updateWalletName()" </q-list>
>Update name</q-btn </q-card-section>
> </q-card>
</q-card-section> {% endif %} {% if AD_SPACE %} {% for ADS in AD_SPACE %} {% set AD =
</q-card> ADS.split(';') %}
</q-expansion-item> <q-card>
<q-separator></q-separator> <a href="{{ AD[0] }}"
<q-expansion-item ><img width="100%" src="{{ AD[1] }}"
group="extras" /></a> </q-card
icon="remove_circle" >{% endfor %} {% endif %}
label="Delete wallet" </div>
>
<q-card>
<q-card-section>
<p>
This whole wallet will be deleted, the funds will be
<strong>UNRECOVERABLE</strong>.
</p>
<q-btn
unelevated
color="red-10"
@click="deleteWallet('{{ wallet.id }}', '{{ user.id }}')"
>Delete wallet</q-btn
>
</q-card-section>
</q-card>
</q-expansion-item>
</q-list>
</q-card-section>
</q-card>
</div> </div>
</div>
<q-dialog v-model="receive.show" @hide="closeReceiveDialog"> <q-dialog v-model="receive.show" @hide="closeReceiveDialog">
{% raw %} {% raw %}
<q-card <q-card
v-if="!receive.paymentReq" v-if="!receive.paymentReq"
class="q-pa-lg q-pt-xl lnbits__dialog-card" class="q-pa-lg q-pt-xl lnbits__dialog-card"
> >
<q-form @submit="createInvoice" class="q-gutter-md"> <q-form @submit="createInvoice" class="q-gutter-md">
<p v-if="receive.lnurl" class="text-h6 text-center q-my-none"> <p v-if="receive.lnurl" class="text-h6 text-center q-my-none">
<b>{{receive.lnurl.domain}}</b> is requesting an invoice: <b>{{receive.lnurl.domain}}</b> is requesting an invoice:
</p>
{% endraw %} {% if LNBITS_DENOMINATION != 'sats' %}
<q-input
filled
dense
v-model.number="receive.data.amount"
label="Amount ({{LNBITS_DENOMINATION}}) *"
mask="#.##"
fill-mask="0"
reverse-fill-mask
:min="receive.minMax[0]"
:max="receive.minMax[1]"
:readonly="receive.lnurl && receive.lnurl.fixed"
></q-input>
{% else %}
<q-select
filled
dense
v-model="receive.unit"
type="text"
label="Unit"
:options="receive.units"
></q-select>
<q-input
filled
dense
v-model.number="receive.data.amount"
type="number"
label="Amount ({{LNBITS_DENOMINATION}}) *"
:step="receive.unit != 'sat' ? '0.001' : '1'"
:min="receive.minMax[0]"
:max="receive.minMax[1]"
:readonly="receive.lnurl && receive.lnurl.fixed"
></q-input>
{% endif %}
<q-input
filled
dense
v-model.trim="receive.data.memo"
label="Memo"
></q-input>
{% raw %}
<div v-if="receive.status == 'pending'" class="row q-mt-lg">
<q-btn
unelevated
color="primary"
:disable="receive.data.memo == null || receive.data.amount == null || receive.data.amount <= 0"
type="submit"
>
<span v-if="receive.lnurl">
Withdraw from {{receive.lnurl.domain}}
</span>
<span v-else> Create invoice </span>
</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</div>
<q-spinner
v-if="receive.status == 'loading'"
color="primary"
size="2.55em"
></q-spinner>
</q-form>
</q-card>
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div class="text-center q-mb-lg">
<a :href="'lightning:' + receive.paymentReq">
<q-responsive :ratio="1" class="q-mx-xl">
<qrcode
:value="receive.paymentReq"
:options="{width: 340}"
class="rounded-borders"
></qrcode>
</q-responsive>
</a>
</div>
<div class="row q-mt-lg">
<q-btn outline color="grey" @click="copyText(receive.paymentReq)"
>Copy invoice</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
{% endraw %}
</q-dialog>
<q-dialog v-model="parse.show" @hide="closeParseDialog">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div v-if="parse.invoice">
<h6 v-if="'{{LNBITS_DENOMINATION}}' != 'sats'" class="q-my-none">
{% raw %} {{ parseFloat(String(parse.invoice.fsat).replaceAll(",", ""))
/ 100 }} {% endraw %} {{LNBITS_DENOMINATION}} {% raw %}
</h6>
<h6 v-else class="q-my-none">
{{ parse.invoice.fsat }}{% endraw %} {{LNBITS_DENOMINATION}} {% raw %}
</h6>
<q-separator class="q-my-sm"></q-separator>
<p class="text-wrap">
<strong>Description:</strong> {{ parse.invoice.description }}<br />
<strong>Expire date:</strong> {{ parse.invoice.expireDate }}<br />
<strong>Hash:</strong> {{ parse.invoice.hash }}
</p>
{% endraw %}
<div v-if="canPay" class="row q-mt-lg">
<q-btn unelevated color="primary" @click="payInvoice">Pay</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</div>
<div v-else class="row q-mt-lg">
<q-btn unelevated disabled color="yellow" text-color="black"
>Not enough funds!</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</div>
</div>
<div v-else-if="parse.lnurlauth">
{% raw %}
<q-form @submit="authLnurl" class="q-gutter-md">
<p class="q-my-none text-h6">
Authenticate with <b>{{ parse.lnurlauth.domain }}</b>?
</p> </p>
<q-separator class="q-my-sm"></q-separator> {% endraw %} {% if LNBITS_DENOMINATION != 'sats' %}
<p>
For every website and for every LNbits wallet, a new keypair will be
deterministically generated so your identity can't be tied to your
LNbits wallet or linked across websites. No other data will be shared
with {{ parse.lnurlauth.domain }}.
</p>
<p>Your public key for <b>{{ parse.lnurlauth.domain }}</b> is:</p>
<p class="q-mx-xl">
<code class="text-wrap"> {{ parse.lnurlauth.pubkey }} </code>
</p>
<div class="row q-mt-lg">
<q-btn unelevated color="primary" type="submit">Login</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
{% endraw %}
</div>
<div v-else-if="parse.lnurlpay">
{% raw %}
<q-form @submit="payLnurl" class="q-gutter-md">
<p v-if="parse.lnurlpay.fixed" class="q-my-none text-h6">
<b>{{ parse.lnurlpay.domain }}</b> is requesting {{
parse.lnurlpay.maxSendable | msatoshiFormat }} {{LNBITS_DENOMINATION}}
<span v-if="parse.lnurlpay.commentAllowed > 0">
<br />
and a {{parse.lnurlpay.commentAllowed}}-char comment
</span>
</p>
<p v-else class="q-my-none text-h6 text-center">
<b>{{ parse.lnurlpay.targetUser || parse.lnurlpay.domain }}</b> is
requesting <br />
between <b>{{ parse.lnurlpay.minSendable | msatoshiFormat }}</b> and
<b>{{ parse.lnurlpay.maxSendable | msatoshiFormat }}</b>
{% endraw %} {{LNBITS_DENOMINATION}} {% raw %}
<span v-if="parse.lnurlpay.commentAllowed > 0">
<br />
and a {{parse.lnurlpay.commentAllowed}}-char comment
</span>
</p>
<q-separator class="q-my-sm"></q-separator>
<div class="row">
<p class="col text-justify text-italic">
{{ parse.lnurlpay.description }}
</p>
<p class="col-4 q-pl-md" v-if="parse.lnurlpay.image">
<q-img :src="parse.lnurlpay.image" />
</p>
</div>
<div class="row">
<div class="col">
{% endraw %}
<q-input
filled
dense
v-model.number="parse.data.amount"
type="number"
label="Amount ({{LNBITS_DENOMINATION}}) *"
:min="parse.lnurlpay.minSendable / 1000"
:max="parse.lnurlpay.maxSendable / 1000"
:readonly="parse.lnurlpay.fixed"
></q-input>
{% raw %}
</div>
<div class="col-8 q-pl-md" v-if="parse.lnurlpay.commentAllowed > 0">
<q-input
filled
dense
v-model="parse.data.comment"
:type="parse.lnurlpay.commentAllowed > 64 ? 'textarea' : 'text'"
label="Comment (optional)"
:maxlength="parse.lnurlpay.commentAllowed"
></q-input>
</div>
</div>
<div class="row q-mt-lg">
<q-btn unelevated color="primary" type="submit"
>Send {{LNBITS_DENOMINATION}}</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
{% endraw %}
</div>
<div v-else>
<q-form
v-if="!parse.camera.show"
@submit="decodeRequest"
class="q-gutter-md"
>
<q-input <q-input
filled filled
dense dense
v-model.trim="parse.data.request" v-model.number="receive.data.amount"
type="textarea" label="Amount ({{LNBITS_DENOMINATION}}) *"
label="Paste an invoice, payment request or lnurl code *" mask="#.##"
> fill-mask="0"
</q-input> reverse-fill-mask
<div class="row q-mt-lg"> :min="receive.minMax[0]"
:max="receive.minMax[1]"
:readonly="receive.lnurl && receive.lnurl.fixed"
></q-input>
{% else %}
<q-select
filled
dense
v-model="receive.unit"
type="text"
label="Unit"
:options="receive.units"
></q-select>
<q-input
filled
dense
v-model.number="receive.data.amount"
:label="'Amount (' + receive.unit + ') *'"
:mask="receive.unit != 'sat' ? '#.##' : '#'"
fill-mask="0"
reverse-fill-mask
:step="receive.unit != 'sat' ? '0.01' : '1'"
:min="receive.minMax[0]"
:max="receive.minMax[1]"
:readonly="receive.lnurl && receive.lnurl.fixed"
></q-input>
{% endif %}
<q-input
filled
dense
v-model.trim="receive.data.memo"
label="Memo"
></q-input>
{% raw %}
<div v-if="receive.status == 'pending'" class="row q-mt-lg">
<q-btn <q-btn
unelevated unelevated
color="primary" color="primary"
:disable="parse.data.request == ''" :disable="receive.data.amount == null || receive.data.amount <= 0"
type="submit" type="submit"
>Read</q-btn >
<span v-if="receive.lnurl">
Withdraw from {{receive.lnurl.domain}}
</span>
<span v-else> Create invoice </span>
</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
<q-spinner
v-if="receive.status == 'loading'"
color="primary"
size="2.55em"
></q-spinner>
</q-form>
</q-card>
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div class="text-center q-mb-lg">
<a :href="'lightning:' + receive.paymentReq">
<q-responsive :ratio="1" class="q-mx-xl">
<qrcode
:value="receive.paymentReq"
:options="{width: 340}"
class="rounded-borders"
></qrcode>
</q-responsive>
</a>
</div>
<div class="row q-mt-lg">
<q-btn outline color="grey" @click="copyText(receive.paymentReq)"
>Copy invoice</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
{% endraw %}
</q-dialog>
<q-dialog v-model="parse.show" @hide="closeParseDialog">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div v-if="parse.invoice">
<h6 v-if="'{{LNBITS_DENOMINATION}}' != 'sats'" class="q-my-none">
{% raw %} {{ parseFloat(String(parse.invoice.fsat).replaceAll(",",
"")) / 100 }} {% endraw %} {{LNBITS_DENOMINATION}} {% raw %}
</h6>
<h6 v-else class="q-my-none">
{{ parse.invoice.fsat }}{% endraw %} {{LNBITS_DENOMINATION}} {% raw %}
</h6>
<q-separator class="q-my-sm"></q-separator>
<p class="text-wrap">
<strong>Description:</strong> {{ parse.invoice.description }}<br />
<strong>Expire date:</strong> {{ parse.invoice.expireDate }}<br />
<strong>Hash:</strong> {{ parse.invoice.hash }}
</p>
{% endraw %}
<div v-if="canPay" class="row q-mt-lg">
<q-btn unelevated color="primary" @click="payInvoice">Pay</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
<div v-else class="row q-mt-lg">
<q-btn unelevated disabled color="yellow" text-color="black"
>Not enough funds!</q-btn
> >
<q-btn v-close-popup flat color="grey" class="q-ml-auto" <q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn >Cancel</q-btn
> >
</div> </div>
</q-form> </div>
<div v-else-if="parse.lnurlauth">
{% raw %}
<q-form @submit="authLnurl" class="q-gutter-md">
<p class="q-my-none text-h6">
Authenticate with <b>{{ parse.lnurlauth.domain }}</b>?
</p>
<q-separator class="q-my-sm"></q-separator>
<p>
For every website and for every LNbits wallet, a new keypair will be
deterministically generated so your identity can't be tied to your
LNbits wallet or linked across websites. No other data will be
shared with {{ parse.lnurlauth.domain }}.
</p>
<p>Your public key for <b>{{ parse.lnurlauth.domain }}</b> is:</p>
<p class="q-mx-xl">
<code class="text-wrap"> {{ parse.lnurlauth.pubkey }} </code>
</p>
<div class="row q-mt-lg">
<q-btn unelevated color="primary" type="submit">Login</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
{% endraw %}
</div>
<div v-else-if="parse.lnurlpay">
{% raw %}
<q-form @submit="payLnurl" class="q-gutter-md">
<p v-if="parse.lnurlpay.fixed" class="q-my-none text-h6">
<b>{{ parse.lnurlpay.domain }}</b> is requesting {{
parse.lnurlpay.maxSendable | msatoshiFormat }}
{{LNBITS_DENOMINATION}}
<span v-if="parse.lnurlpay.commentAllowed > 0">
<br />
and a {{parse.lnurlpay.commentAllowed}}-char comment
</span>
</p>
<p v-else class="q-my-none text-h6 text-center">
<b>{{ parse.lnurlpay.targetUser || parse.lnurlpay.domain }}</b> is
requesting <br />
between <b>{{ parse.lnurlpay.minSendable | msatoshiFormat }}</b> and
<b>{{ parse.lnurlpay.maxSendable | msatoshiFormat }}</b>
{% endraw %} {{LNBITS_DENOMINATION}} {% raw %}
<span v-if="parse.lnurlpay.commentAllowed > 0">
<br />
and a {{parse.lnurlpay.commentAllowed}}-char comment
</span>
</p>
<q-separator class="q-my-sm"></q-separator>
<div class="row">
<p class="col text-justify text-italic">
{{ parse.lnurlpay.description }}
</p>
<p class="col-4 q-pl-md" v-if="parse.lnurlpay.image">
<q-img :src="parse.lnurlpay.image" />
</p>
</div>
<div class="row">
<div class="col">
{% endraw %}
<q-input
filled
dense
v-model.number="parse.data.amount"
type="number"
label="Amount ({{LNBITS_DENOMINATION}}) *"
:min="parse.lnurlpay.minSendable / 1000"
:max="parse.lnurlpay.maxSendable / 1000"
:readonly="parse.lnurlpay.fixed"
></q-input>
{% raw %}
</div>
<div class="col-8 q-pl-md" v-if="parse.lnurlpay.commentAllowed > 0">
<q-input
filled
dense
v-model="parse.data.comment"
:type="parse.lnurlpay.commentAllowed > 64 ? 'textarea' : 'text'"
label="Comment (optional)"
:maxlength="parse.lnurlpay.commentAllowed"
></q-input>
</div>
</div>
<div class="row q-mt-lg">
<q-btn unelevated color="primary" type="submit"
>Send {{LNBITS_DENOMINATION}}</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
{% endraw %}
</div>
<div v-else> <div v-else>
<q-responsive :ratio="1"> <q-form
<qrcode-stream v-if="!parse.camera.show"
@decode="decodeQR" @submit="decodeRequest"
class="rounded-borders" class="q-gutter-md"
></qrcode-stream> >
</q-responsive> <q-input
<div class="row q-mt-lg"> filled
<q-btn @click="closeCamera" flat color="grey" class="q-ml-auto"> dense
Cancel v-model.trim="parse.data.request"
</q-btn> type="textarea"
label="Paste an invoice, payment request or lnurl code *"
>
</q-input>
<div class="row q-mt-lg">
<q-btn
unelevated
color="primary"
:disable="parse.data.request == ''"
type="submit"
>Read</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
<div v-else>
<q-responsive :ratio="1">
<qrcode-stream
@decode="decodeQR"
class="rounded-borders"
></qrcode-stream>
</q-responsive>
<div class="row q-mt-lg">
<q-btn @click="closeCamera" flat color="grey" class="q-ml-auto">
Cancel
</q-btn>
</div>
</div> </div>
</div> </div>
</div> </q-card>
</q-card> </q-dialog>
</q-dialog>
<q-dialog v-model="parse.camera.show"> <q-dialog v-model="parse.camera.show">
<q-card class="q-pa-lg q-pt-xl"> <q-card class="q-pa-lg q-pt-xl">
<div class="text-center q-mb-lg"> <div class="text-center q-mb-lg">
<qrcode-stream @decode="decodeQR" class="rounded-borders"></qrcode-stream> <qrcode-stream
</div> @decode="decodeQR"
<div class="row q-mt-lg"> class="rounded-borders"
<q-btn @click="closeCamera" flat color="grey" class="q-ml-auto" ></qrcode-stream>
>Cancel</q-btn </div>
> <div class="row q-mt-lg">
</div> <q-btn @click="closeCamera" flat color="grey" class="q-ml-auto"
</q-card> >Cancel</q-btn
</q-dialog> >
</div>
</q-card>
</q-dialog>
<q-dialog v-model="paymentsChart.show"> <q-dialog v-model="paymentsChart.show">
<q-card class="q-pa-sm" style="width: 800px; max-width: unset"> <q-card class="q-pa-sm" style="width: 800px; max-width: unset">
<q-card-section> <q-card-section>
<canvas ref="canvas" width="600" height="400"></canvas> <canvas ref="canvas" width="600" height="400"></canvas>
</q-card-section> </q-card-section>
</q-card> </q-card>
</q-dialog> </q-dialog>
<q-tabs
class="lt-md fixed-bottom left-0 right-0 bg-primary text-white shadow-2 z-max"
active-class="px-0"
indicator-color="transparent"
>
<q-tab
icon="account_balance_wallet"
label="Wallets"
@click="g.visibleDrawer = !g.visibleDrawer"
>
</q-tab>
<q-tab icon="content_paste" label="Paste" @click="showParseDialog"> </q-tab>
<q-tab icon="file_download" label="Receive" @click="showReceiveDialog">
</q-tab>
{% if service_fee > 0 %} <q-tab icon="photo_camera" label="Scan" @click="showCamera"> </q-tab>
<div ref="disclaimer"></div> </q-tabs>
<q-dialog v-model="disclaimerDialog.show"> {% if service_fee > 0 %}
<q-card class="q-pa-lg"> <div ref="disclaimer"></div>
<h6 class="q-my-md text-deep-purple">Warning</h6> <q-dialog v-model="disclaimerDialog.show">
<p> <q-card class="q-pa-lg">
Login functionality to be released in v0.2, for now, <h6 class="q-my-md text-deep-purple">Warning</h6>
<strong <p>
>make sure you bookmark this page for future access to your Login functionality to be released in v0.2, for now,
wallet</strong <strong
>! >make sure you bookmark this page for future access to your
</p> wallet</strong
<p> >!
This service is in BETA, and we hold no responsibility for people losing </p>
access to funds. To encourage you to run your own LNbits installation, any <p>
balance on {% raw %}{{ disclaimerDialog.location.host }}{% endraw %} will This service is in BETA, and we hold no responsibility for people losing
incur a charge of <strong>{{ service_fee }}% service fee</strong> per access to funds. To encourage you to run your own LNbits installation,
week. any balance on {% raw %}{{ disclaimerDialog.location.host }}{% endraw %}
</p> will incur a charge of
<div class="row q-mt-lg"> <strong>{{ service_fee }}% service fee</strong> per week.
<q-btn </p>
outline <div class="row q-mt-lg">
color="grey" <q-btn
@click="copyText(disclaimerDialog.location.href)" outline
>Copy wallet URL</q-btn color="grey"
> @click="copyText(disclaimerDialog.location.href)"
<q-btn v-close-popup flat color="grey" class="q-ml-auto" >Copy wallet URL</q-btn
>I understand</q-btn >
> <q-btn v-close-popup flat color="grey" class="q-ml-auto"
</div> >I understand</q-btn
</q-card> >
</q-dialog> </div>
{% endif %} {% endblock %} </q-card>
</q-dialog>
{% endif %} {% endblock %}
</div>
+41 -23
View File
@@ -7,7 +7,7 @@ from typing import Dict, List, Optional, Union
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
import httpx import httpx
from fastapi import Query, Request from fastapi import Query, Request, Header
from fastapi.exceptions import HTTPException from fastapi.exceptions import HTTPException
from fastapi.param_functions import Depends from fastapi.param_functions import Depends
from fastapi.params import Body from fastapi.params import Body
@@ -23,9 +23,11 @@ from lnbits.decorators import (
WalletInvoiceKeyChecker, WalletInvoiceKeyChecker,
WalletTypeInfo, WalletTypeInfo,
get_key_type, get_key_type,
require_admin_key
) )
from lnbits.helpers import url_for from lnbits.helpers import url_for, urlsafe_short_hash
from lnbits.requestvars import g from lnbits.requestvars import g
from lnbits.settings import LNBITS_ADMIN_USERS, LNBITS_SITE_TITLE
from lnbits.utils.exchange_rates import ( from lnbits.utils.exchange_rates import (
currencies, currencies,
fiat_amount_as_satoshis, fiat_amount_as_satoshis,
@@ -34,13 +36,14 @@ from lnbits.utils.exchange_rates import (
from .. import core_app, db from .. import core_app, db
from ..crud import ( from ..crud import (
create_payment,
get_payments, get_payments,
get_standalone_payment, get_standalone_payment,
save_balance_check,
update_wallet,
create_payment,
get_wallet, get_wallet,
get_wallet_for_key,
save_balance_check,
update_payment_status, update_payment_status,
update_wallet,
) )
from ..services import ( from ..services import (
InvoiceFailure, InvoiceFailure,
@@ -51,8 +54,6 @@ from ..services import (
perform_lnurlauth, perform_lnurlauth,
) )
from ..tasks import api_invoice_listeners from ..tasks import api_invoice_listeners
from lnbits.settings import LNBITS_ADMIN_USERS
from lnbits.helpers import urlsafe_short_hash
@core_app.get("/api/v1/wallet") @core_app.get("/api/v1/wallet")
@@ -71,7 +72,7 @@ async def api_wallet(wallet: WalletTypeInfo = Depends(get_key_type)):
async def api_update_balance( async def api_update_balance(
amount: int, wallet: WalletTypeInfo = Depends(get_key_type) amount: int, wallet: WalletTypeInfo = Depends(get_key_type)
): ):
if LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS: if wallet.wallet.user not in LNBITS_ADMIN_USERS:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Not an admin user" status_code=HTTPStatus.FORBIDDEN, detail="Not an admin user"
) )
@@ -123,8 +124,8 @@ async def api_payments(wallet: WalletTypeInfo = Depends(get_key_type)):
class CreateInvoiceData(BaseModel): class CreateInvoiceData(BaseModel):
out: Optional[bool] = True out: Optional[bool] = True
amount: int = Query(None, ge=1) amount: float = Query(None, ge=0)
memo: str = None memo: Optional[str] = None
unit: Optional[str] = "sat" unit: Optional[str] = "sat"
description_hash: Optional[str] = None description_hash: Optional[str] = None
lnurl_callback: Optional[str] = None lnurl_callback: Optional[str] = None
@@ -140,9 +141,9 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
memo = "" memo = ""
else: else:
description_hash = b"" description_hash = b""
memo = data.memo memo = data.memo or LNBITS_SITE_TITLE
if data.unit == "sat": if data.unit == "sat":
amount = data.amount amount = int(data.amount)
else: else:
price_in_sats = await fiat_amount_as_satoshis(data.amount, data.unit) price_in_sats = await fiat_amount_as_satoshis(data.amount, data.unit)
amount = price_in_sats amount = price_in_sats
@@ -292,11 +293,11 @@ async def api_payments_pay_lnurl(
detail=f"{domain} returned an invalid invoice. Expected {data.amount} msat, got {invoice.amount_msat}.", detail=f"{domain} returned an invalid invoice. Expected {data.amount} msat, got {invoice.amount_msat}.",
) )
# if invoice.description_hash != data.description_hash: # if invoice.description_hash != data.description_hash:
# raise HTTPException( # raise HTTPException(
# status_code=HTTPStatus.BAD_REQUEST, # status_code=HTTPStatus.BAD_REQUEST,
# detail=f"{domain} returned an invalid invoice. Expected description_hash == {data.description_hash}, got {invoice.description_hash}.", # detail=f"{domain} returned an invalid invoice. Expected description_hash == {data.description_hash}, got {invoice.description_hash}.",
# ) # )
extra = {} extra = {}
@@ -363,7 +364,13 @@ async def api_payments_sse(
@core_app.get("/api/v1/payments/{payment_hash}") @core_app.get("/api/v1/payments/{payment_hash}")
async def api_payment(payment_hash): async def api_payment(payment_hash, X_Api_Key: Optional[str] = Header(None)):
wallet = None
try:
if X_Api_Key.extra:
print("No key")
except:
wallet = await get_wallet_for_key(X_Api_Key)
payment = await get_standalone_payment(payment_hash) payment = await get_standalone_payment(payment_hash)
await check_invoice_status(payment.wallet_id, payment_hash) await check_invoice_status(payment.wallet_id, payment_hash)
payment = await get_standalone_payment(payment_hash) payment = await get_standalone_payment(payment_hash)
@@ -372,13 +379,19 @@ async def api_payment(payment_hash):
status_code=HTTPStatus.NOT_FOUND, detail="Payment does not exist." status_code=HTTPStatus.NOT_FOUND, detail="Payment does not exist."
) )
elif not payment.pending: elif not payment.pending:
if wallet and wallet.id == payment.wallet_id:
return {"paid": True, "preimage": payment.preimage, "details": payment}
return {"paid": True, "preimage": payment.preimage} return {"paid": True, "preimage": payment.preimage}
try: try:
await payment.check_pending() await payment.check_pending()
except Exception: except Exception:
if wallet and wallet.id == payment.wallet_id:
return {"paid": False, "details": payment}
return {"paid": False} return {"paid": False}
if wallet and wallet.id == payment.wallet_id:
return {"paid": not payment.pending, "preimage": payment.preimage, "details": payment}
return {"paid": not payment.pending, "preimage": payment.preimage} return {"paid": not payment.pending, "preimage": payment.preimage}
@@ -500,14 +513,19 @@ async def api_lnurlscan(code: str):
return params return params
class DecodePayment(BaseModel):
data: str
@core_app.post("/api/v1/payments/decode") @core_app.post("/api/v1/payments/decode")
async def api_payments_decode(data: str = Query(None)): async def api_payments_decode(data: DecodePayment):
payment_str = data.data
try: try:
if data["data"][:5] == "LNURL": if payment_str[:5] == "LNURL":
url = lnurl.decode(data["data"]) url = lnurl.decode(payment_str)
return {"domain": url} return {"domain": url}
else: else:
invoice = bolt11.decode(data["data"]) invoice = bolt11.decode(payment_str)
return { return {
"payment_hash": invoice.payment_hash, "payment_hash": invoice.payment_hash,
"amount_msat": invoice.amount_msat, "amount_msat": invoice.amount_msat,
@@ -559,6 +577,6 @@ async def api_fiat_as_sats(data: ConversionData):
return output return output
else: else:
output[data.from_.upper()] = data.amount output[data.from_.upper()] = data.amount
output["sats"] = await fiat_amount_as_satoshis(data.amount, data.to) output["sats"] = await fiat_amount_as_satoshis(data.amount, data.from_)
output["BTC"] = output["sats"] / 100000000 output["BTC"] = output["sats"] / 100000000
return output return output
+2 -2
View File
@@ -15,8 +15,8 @@ from lnbits.core.models import User
from lnbits.decorators import check_user_exists from lnbits.decorators import check_user_exists
from lnbits.helpers import template_renderer, url_for from lnbits.helpers import template_renderer, url_for
from lnbits.settings import ( from lnbits.settings import (
LNBITS_ALLOWED_USERS,
LNBITS_ADMIN_USERS, LNBITS_ADMIN_USERS,
LNBITS_ALLOWED_USERS,
LNBITS_SITE_TITLE, LNBITS_SITE_TITLE,
SERVICE_FEE, SERVICE_FEE,
) )
@@ -226,7 +226,7 @@ async def lnurl_balance_notify(request: Request, service: str):
redeem_lnurl_withdraw(bc.wallet, bc.url) redeem_lnurl_withdraw(bc.wallet, bc.url)
@core_html_routes.get("/lnurlwallet", response_class=RedirectResponse) @core_html_routes.get("/lnurlwallet", response_class=RedirectResponse, name="core.lnurlwallet")
async def lnurlwallet(request: Request): async def lnurlwallet(request: Request):
async with db.connect() as conn: async with db.connect() as conn:
account = await create_account(conn=conn) account = await create_account(conn=conn)
+9 -3
View File
@@ -130,9 +130,15 @@ class Database(Compat):
) )
) )
else: else:
self.path = os.path.join(LNBITS_DATA_FOLDER, f"{self.name}.sqlite3") if os.path.isdir(LNBITS_DATA_FOLDER):
database_uri = f"sqlite:///{self.path}" self.path = os.path.join(LNBITS_DATA_FOLDER, f"{self.name}.sqlite3")
self.type = SQLITE database_uri = f"sqlite:///{self.path}"
self.type = SQLITE
else:
raise NotADirectoryError(
f"LNBITS_DATA_FOLDER named {LNBITS_DATA_FOLDER} was not created"
f" - please 'mkdir {LNBITS_DATA_FOLDER}' and try again"
)
self.schema = self.name self.schema = self.name
if self.name.startswith("ext_"): if self.name.startswith("ext_"):
+1 -1
View File
@@ -147,7 +147,7 @@ async def get_key_type(
try: try:
checker = WalletInvoiceKeyChecker(api_key=token) checker = WalletInvoiceKeyChecker(api_key=token)
await checker.__call__(r) await checker.__call__(r)
wallet = WalletTypeInfo(0, checker.wallet) wallet = WalletTypeInfo(1, checker.wallet)
if (LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS) and (LNBITS_ADMIN_EXTENSIONS and pathname in LNBITS_ADMIN_EXTENSIONS): if (LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS) and (LNBITS_ADMIN_EXTENSIONS and pathname in LNBITS_ADMIN_EXTENSIONS):
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized.") raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized.")
return wallet return wallet
+1 -1
View File
@@ -88,7 +88,7 @@ async def get_copilot(copilot_id: str) -> Copilots:
async def get_copilots(user: str) -> List[Copilots]: async def get_copilots(user: str) -> List[Copilots]:
rows = await db.fetchall( rows = await db.fetchall(
"SELECT * FROM copilot.newer_copilots WHERE user = ?", (user,) 'SELECT * FROM copilot.newer_copilots WHERE "user" = ?', (user,)
) )
return [Copilots(**row) for row in rows] return [Copilots(**row) for row in rows]
+1 -1
View File
@@ -76,7 +76,7 @@ async def delete_ticket(payment_hash: str) -> None:
async def delete_event_tickets(event_id: str) -> None: async def delete_event_tickets(event_id: str) -> None:
await db.execute("DELETE FROM events.tickets WHERE event = ?", (event_id,)) await db.execute("DELETE FROM events.ticket WHERE event = ?", (event_id,))
# EVENTS # EVENTS
+4 -3
View File
@@ -12,7 +12,7 @@ async def create_jukebox(
juke_id = urlsafe_short_hash() juke_id = urlsafe_short_hash()
result = await db.execute( result = await db.execute(
""" """
INSERT INTO jukebox.jukebox (id, user, title, wallet, sp_user, sp_secret, sp_access_token, sp_refresh_token, sp_device, sp_playlists, price, profit) INSERT INTO jukebox.jukebox (id, "user", title, wallet, sp_user, sp_secret, sp_access_token, sp_refresh_token, sp_device, sp_playlists, price, profit)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", """,
( (
@@ -41,6 +41,7 @@ async def update_jukebox(
q = ", ".join([f"{field[0]} = ?" for field in data]) q = ", ".join([f"{field[0]} = ?" for field in data])
items = [f"{field[1]}" for field in data] items = [f"{field[1]}" for field in data]
items.append(juke_id) items.append(juke_id)
q = q.replace("user", '"user"', 1) # hack to make user be "user"!
await db.execute(f"UPDATE jukebox.jukebox SET {q} WHERE id = ?", (items)) await db.execute(f"UPDATE jukebox.jukebox SET {q} WHERE id = ?", (items))
row = await db.fetchone("SELECT * FROM jukebox.jukebox WHERE id = ?", (juke_id,)) row = await db.fetchone("SELECT * FROM jukebox.jukebox WHERE id = ?", (juke_id,))
return Jukebox(**row) if row else None return Jukebox(**row) if row else None
@@ -57,11 +58,11 @@ async def get_jukebox_by_user(user: str) -> Optional[Jukebox]:
async def get_jukeboxs(user: str) -> List[Jukebox]: async def get_jukeboxs(user: str) -> List[Jukebox]:
rows = await db.fetchall("SELECT * FROM jukebox.jukebox WHERE user = ?", (user,)) rows = await db.fetchall('SELECT * FROM jukebox.jukebox WHERE "user" = ?', (user,))
for row in rows: for row in rows:
if row.sp_playlists == None: if row.sp_playlists == None:
await delete_jukebox(row.id) await delete_jukebox(row.id)
rows = await db.fetchall("SELECT * FROM jukebox.jukebox WHERE user = ?", (user,)) rows = await db.fetchall('SELECT * FROM jukebox.jukebox WHERE "user" = ?', (user,))
return [Jukebox(**row) for row in rows] return [Jukebox(**row) for row in rows]
+2 -3
View File
@@ -75,7 +75,6 @@ async def api_check_credentials_check(
juke_id: str = Query(None), wallet: WalletTypeInfo = Depends(require_admin_key) juke_id: str = Query(None), wallet: WalletTypeInfo = Depends(require_admin_key)
): ):
jukebox = await get_jukebox(juke_id) jukebox = await get_jukebox(juke_id)
return jukebox return jukebox
@@ -442,7 +441,7 @@ async def api_get_jukebox_currently(
token = await api_get_token(juke_id) token = await api_get_token(juke_id)
if token == False: if token == False:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="INvoice not paid" status_code=HTTPStatus.FORBIDDEN, detail="Invoice not paid"
) )
elif retry: elif retry:
raise HTTPException( raise HTTPException(
@@ -456,5 +455,5 @@ async def api_get_jukebox_currently(
) )
except: except:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Something went wrong" status_code=HTTPStatus.NOT_FOUND, detail="Something went wrong, or no song is playing yet"
) )
+1 -1
View File
@@ -30,6 +30,6 @@ from .views import * # noqa
from .views_api import * # noqa from .views_api import * # noqa
def lnticket_start(): def livestream_start():
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices)) loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
+1 -1
View File
@@ -1,5 +1,5 @@
from http import HTTPStatus from http import HTTPStatus
from mmap import MAP_DENYWRITE # from mmap import MAP_DENYWRITE
from fastapi.param_functions import Depends from fastapi.param_functions import Depends
from fastapi.params import Query from fastapi.params import Query
+23 -8
View File
@@ -1,4 +1,6 @@
import time import time
import asyncio
from base64 import urlsafe_b64encode from base64 import urlsafe_b64encode
from http import HTTPStatus from http import HTTPStatus
@@ -11,7 +13,7 @@ from lnbits import bolt11
from lnbits.core.crud import delete_expired_invoices, get_payments from lnbits.core.crud import delete_expired_invoices, get_payments
from lnbits.core.services import create_invoice, pay_invoice from lnbits.core.services import create_invoice, pay_invoice
from lnbits.decorators import WalletTypeInfo from lnbits.decorators import WalletTypeInfo
from lnbits.settings import WALLET from lnbits.settings import WALLET, LNBITS_SITE_TITLE
from . import lndhub_ext from . import lndhub_ext
from .decorators import check_wallet, require_admin_key from .decorators import check_wallet, require_admin_key
@@ -55,14 +57,13 @@ async def lndhub_addinvoice(
_, pr = await create_invoice( _, pr = await create_invoice(
wallet_id=wallet.wallet.id, wallet_id=wallet.wallet.id,
amount=int(data.amt), amount=int(data.amt),
memo=data.memo or "received sats", memo=data.memo or LNBITS_SITE_TITLE,
extra={"tag": "lndhub"}, extra={"tag": "lndhub"},
) )
except: except:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Failed to create invoice" status_code=HTTPStatus.NOT_FOUND, detail="Failed to create invoice"
) )
invoice = bolt11.decode(pr) invoice = bolt11.decode(pr)
return { return {
"pay_req": pr, "pay_req": pr,
@@ -116,7 +117,9 @@ async def lndhub_balance(
@lndhub_ext.get("/ext/gettxs") @lndhub_ext.get("/ext/gettxs")
async def lndhub_gettxs( async def lndhub_gettxs(
wallet: WalletTypeInfo = Depends(check_wallet), limit: int = Query(0, ge=0, lt=200) wallet: WalletTypeInfo = Depends(check_wallet),
limit: int = Query(20, ge=1, le=20),
offset: int = Query(0, ge=0),
): ):
for payment in await get_payments( for payment in await get_payments(
wallet_id=wallet.wallet.id, wallet_id=wallet.wallet.id,
@@ -124,11 +127,15 @@ async def lndhub_gettxs(
pending=True, pending=True,
outgoing=True, outgoing=True,
incoming=False, incoming=False,
limit=limit,
offset=offset,
exclude_uncheckable=True, exclude_uncheckable=True,
): ):
await payment.set_pending( await payment.set_pending(
(await WALLET.get_payment_status(payment.checking_id)).pending (await WALLET.get_payment_status(payment.checking_id)).pending
) )
await asyncio.sleep(0.1)
return [ return [
{ {
"payment_preimage": payment.preimage, "payment_preimage": payment.preimage,
@@ -148,28 +155,34 @@ async def lndhub_gettxs(
complete=True, complete=True,
outgoing=True, outgoing=True,
incoming=False, incoming=False,
limit=limit,
offset=offset,
) )
)[:limit] )
) )
] ]
@lndhub_ext.get("/ext/getuserinvoices") @lndhub_ext.get("/ext/getuserinvoices")
async def lndhub_getuserinvoices( async def lndhub_getuserinvoices(
wallet: WalletTypeInfo = Depends(check_wallet), limit: int = Query(0, ge=0, lt=200) wallet: WalletTypeInfo = Depends(check_wallet),
limit: int = Query(20, ge=1, le=20),
offset: int = Query(0, ge=0),
): ):
await delete_expired_invoices()
for invoice in await get_payments( for invoice in await get_payments(
wallet_id=wallet.wallet.id, wallet_id=wallet.wallet.id,
complete=False, complete=False,
pending=True, pending=True,
outgoing=False, outgoing=False,
incoming=True, incoming=True,
limit=limit,
offset=offset,
exclude_uncheckable=True, exclude_uncheckable=True,
): ):
await invoice.set_pending( await invoice.set_pending(
(await WALLET.get_invoice_status(invoice.checking_id)).pending (await WALLET.get_invoice_status(invoice.checking_id)).pending
) )
await asyncio.sleep(0.1)
return [ return [
{ {
@@ -192,8 +205,10 @@ async def lndhub_getuserinvoices(
complete=True, complete=True,
incoming=True, incoming=True,
outgoing=False, outgoing=False,
limit=limit,
offset=offset,
) )
)[:limit] )
) )
] ]
@@ -117,9 +117,10 @@
{% raw %} {% raw %}
<template v-slot:header="props"> <template v-slot:header="props">
<q-tr :props="props"> <q-tr :props="props">
<q-th auto-width></q-th>
<q-th auto-width></q-th> <q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props"> <q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }} {{ col.label }}
</q-th> </q-th>
</q-tr> </q-tr>
</template> </template>
@@ -136,9 +137,19 @@
:href="'mailto:' + props.row.email" :href="'mailto:' + props.row.email"
></q-btn> ></q-btn>
</q-td> </q-td>
<q-td auto-width>
<q-btn
unelevated
dense
size="xs"
icon="launch"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="ticketCard(props)"
><q-tooltip> Click to show ticket </q-tooltip></q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props"> <q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }} {{ col.label == "Ticket" ? col.value.length > 20 ? `${col.value.substring(0, 20)}...` : col.value : col.value }}
</q-td> </q-td>
<q-td auto-width> <q-td auto-width>
@@ -249,6 +260,29 @@
</q-form> </q-form>
</q-card> </q-card>
</q-dialog> </q-dialog>
<!-- Read Ticket Dialog -->
<q-dialog v-model="ticketDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
{% raw %}
<q-card-section>
<h4 class="text-subtitle1 q-my-none">
<i>{{this.ticketDialog.data.name}}</i> sent a ticket
</h4>
<div v-if="this.ticketDialog.data.email">
<small>{{this.ticketDialog.data.email}}</small>
</div>
<small>{{this.ticketDialog.data.date}}</small>
</q-card-section>
<q-separator></q-separator>
<q-card-section>
<p>{{this.ticketDialog.data.content}}</p>
</q-card-section>
{% endraw %}
<q-card-actions align="right">
<q-btn flat label="CLOSE" color="primary" v-close-popup />
</q-card-actions>
</q-card>
</q-dialog>
</div> </div>
{% endblock %} {% block scripts %} {{ window_vars(user) }} {% endblock %} {% block scripts %} {{ window_vars(user) }}
<script> <script>
@@ -318,6 +352,10 @@
formDialog: { formDialog: {
show: false, show: false,
data: {flatrate: false} data: {flatrate: false}
},
ticketDialog: {
show: false,
data: {}
} }
} }
}, },
@@ -372,6 +410,16 @@
}) })
}) })
}, },
ticketCard(ticket){
this.ticketDialog.show = true
let {date, email, ltext, name} = ticket.row
this.ticketDialog.data = {
date,
email,
content: ltext,
name
}
},
exportticketsCSV: function () { exportticketsCSV: function () {
LNbits.utils.exportCSV(this.ticketsTable.columns, this.tickets) LNbits.utils.exportCSV(this.ticketsTable.columns, this.tickets)
}, },
@@ -421,12 +469,13 @@
}, },
updateformDialog: function (formId) { updateformDialog: function (formId) {
var link = _.findWhere(this.forms, {id: formId}) var link = _.findWhere(this.forms, {id: formId})
console.log("LINK", link)
this.formDialog.data.id = link.id this.formDialog.data.id = link.id
this.formDialog.data.wallet = link.wallet this.formDialog.data.wallet = link.wallet
this.formDialog.data.name = link.name this.formDialog.data.name = link.name
this.formDialog.data.description = link.description this.formDialog.data.description = link.description
this.formDialog.data.flatrate = link.flatrate this.formDialog.data.flatrate = Boolean(link.flatrate)
this.formDialog.data.amount = link.amount this.formDialog.data.amount = link.amount
this.formDialog.show = true this.formDialog.show = true
}, },
+4
View File
@@ -103,6 +103,10 @@ async def api_ticket_make_ticket(data: CreateTicketData, form_id):
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail=f"LNTicket does not exist." status_code=HTTPStatus.NOT_FOUND, detail=f"LNTicket does not exist."
) )
if data.sats < 1:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail=f"0 invoices not allowed."
)
nwords = len(re.split(r"\s+", data.ltext)) nwords = len(re.split(r"\s+", data.ltext))
+5
View File
@@ -76,6 +76,11 @@ async def api_link_create_or_update(
link_id=None, link_id=None,
wallet: WalletTypeInfo = Depends(get_key_type), wallet: WalletTypeInfo = Depends(get_key_type),
): ):
if data.min < 1:
raise HTTPException(
detail="Min must be more than 1.", status_code=HTTPStatus.BAD_REQUEST
)
if data.min > data.max: if data.min > data.max:
raise HTTPException( raise HTTPException(
detail="Min is greater than max.", status_code=HTTPStatus.BAD_REQUEST detail="Min is greater than max.", status_code=HTTPStatus.BAD_REQUEST
+22 -11
View File
@@ -1,15 +1,17 @@
import asyncio import asyncio
import json from http import HTTPStatus
import httpx import httpx
from starlette.exceptions import HTTPException
from lnbits.core import db as core_db from lnbits.core import db as core_db
from lnbits.core.models import Payment
from lnbits.tasks import register_invoice_listener
from lnbits.core.views.api import api_wallet
from lnbits.core.crud import get_wallet from lnbits.core.crud import get_wallet
from lnbits.core.views.api import api_payment, api_payments_decode, pay_invoice from lnbits.core.models import Payment
from lnbits.core.services import pay_invoice
from lnbits.core.views.api import api_payments_decode
from lnbits.tasks import register_invoice_listener
from .crud import get_lnurlpayout, get_lnurlpayout_from_wallet from .crud import get_lnurlpayout_from_wallet
async def wait_for_paid_invoices(): async def wait_for_paid_invoices():
@@ -25,16 +27,16 @@ async def on_invoice_paid(payment: Payment) -> None:
try: try:
# Check its got a payout associated with it # Check its got a payout associated with it
lnurlpayout_link = await get_lnurlpayout_from_wallet(payment.wallet_id) lnurlpayout_link = await get_lnurlpayout_from_wallet(payment.wallet_id)
print("LNURLpayout", lnurlpayout_link)
if lnurlpayout_link: if lnurlpayout_link:
# Check the wallet balance is more than the threshold # Check the wallet balance is more than the threshold
wallet = await get_wallet(lnurlpayout_link.wallet) wallet = await get_wallet(lnurlpayout_link.wallet)
if wallet.balance < lnurlpayout_link.threshold + ( threshold = lnurlpayout_link.threshold + (lnurlpayout_link.threshold * 0.02)
lnurlpayout_link.threshold * 0.02
):
return
if wallet.balance < threshold:
return
# Get the invoice from the LNURL to pay # Get the invoice from the LNURL to pay
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
try: try:
@@ -43,6 +45,7 @@ async def on_invoice_paid(payment: Payment) -> None:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="LNURL broken" status_code=HTTPStatus.FORBIDDEN, detail="LNURL broken"
) )
try: try:
r = await client.get(str(url["domain"]), timeout=40) r = await client.get(str(url["domain"]), timeout=40)
res = r.json() res = r.json()
@@ -56,6 +59,12 @@ async def on_invoice_paid(payment: Payment) -> None:
timeout=40, timeout=40,
) )
res = r.json() res = r.json()
if hasattr(res, "status") and res["status"] == "ERROR":
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN,
detail=res["reason"],
)
try: try:
await pay_invoice( await pay_invoice(
wallet_id=payment.wallet_id, wallet_id=payment.wallet_id,
@@ -65,7 +74,9 @@ async def on_invoice_paid(payment: Payment) -> None:
return return
except: except:
pass pass
except:
except Exception as e:
print("ERROR", str(e))
return return
except (httpx.ConnectError, httpx.RequestError): except (httpx.ConnectError, httpx.RequestError):
return return
+20 -9
View File
@@ -4,7 +4,8 @@ from fastapi import Query
from fastapi.params import Depends from fastapi.params import Depends
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user, get_payments from lnbits.core.crud import get_payments, get_user
from lnbits.core.models import Payment
from lnbits.core.services import create_invoice from lnbits.core.services import create_invoice
from lnbits.core.views.api import api_payment, api_payments_decode from lnbits.core.views.api import api_payment, api_payments_decode
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
@@ -14,10 +15,10 @@ from .crud import (
create_lnurlpayout, create_lnurlpayout,
delete_lnurlpayout, delete_lnurlpayout,
get_lnurlpayout, get_lnurlpayout,
get_lnurlpayouts,
get_lnurlpayout_from_wallet, get_lnurlpayout_from_wallet,
get_lnurlpayouts,
) )
from .models import lnurlpayout, CreateLnurlPayoutData from .models import CreateLnurlPayoutData, lnurlpayout
from .tasks import on_invoice_paid from .tasks import on_invoice_paid
@@ -87,14 +88,24 @@ async def api_lnurlpayout_check(
lnurlpayout_id: str, wallet: WalletTypeInfo = Depends(get_key_type) lnurlpayout_id: str, wallet: WalletTypeInfo = Depends(get_key_type)
): ):
lnurlpayout = await get_lnurlpayout(lnurlpayout_id) lnurlpayout = await get_lnurlpayout(lnurlpayout_id)
payments = await get_payments( ## THIS
wallet_id=lnurlpayout.wallet, mock_payment = Payment(
complete=True, checking_id="mock",
pending=False, pending=False,
outgoing=True, amount=1,
incoming=True, fee=1,
time=0000,
bolt11="mock",
preimage="mock",
payment_hash="mock",
wallet_id=lnurlpayout.wallet,
) )
result = await on_invoice_paid(payments[0]) ## INSTEAD OF THIS
# payments = await get_payments(
# wallet_id=lnurlpayout.wallet, complete=True, pending=False, outgoing=True, incoming=True
# )
result = await on_invoice_paid(mock_payment)
return return
@@ -77,8 +77,8 @@
mixins: [windowMixin], mixins: [windowMixin],
data: function () { data: function () {
return { return {
userAmount: {{ paywall.amount }}, userAmount: '{{ paywall.amount }}',
paywallAmount: {{ paywall.amount }}, paywallAmount: '{{ paywall.amount }}',
paymentReq: null, paymentReq: null,
redirectUrl: null, redirectUrl: null,
paymentDialog: { paymentDialog: {
@@ -89,7 +89,9 @@
}, },
computed: { computed: {
amount: function () { amount: function () {
return (this.paywallAmount > this.userAmount) ? this.paywallAmount : this.userAmount return this.paywallAmount > this.userAmount
? this.paywallAmount
: this.userAmount
} }
}, },
methods: { methods: {
@@ -102,48 +104,55 @@
}, },
createInvoice: function () { createInvoice: function () {
var self = this var self = this
console.log(this.amount) LNbits.api
axios .request(
.post( 'POST',
'/paywall/api/v1/paywalls/{{ paywall.id }}/invoice', '/paywall/api/v1/paywalls/invoice/{{ paywall.id }}',
{amount: self.amount} 'filler',
{
amount: self.amount
}
) )
.then(function (response) { .then(function (response) {
self.paymentReq = response.data.payment_request.toUpperCase() if (response.data) {
self.paymentReq = response.data.payment_request.toUpperCase()
self.paymentDialog.dismissMsg = self.$q.notify({
timeout: 0,
message: 'Waiting for payment...'
})
self.paymentDialog.dismissMsg = self.$q.notify({ self.paymentDialog.checker = setInterval(function () {
timeout: 0, LNbits.api
message: 'Waiting for payment...' .request(
}) 'POST',
'/paywall/api/v1/paywalls/check_invoice/{{ paywall.id }}',
self.paymentDialog.checker = setInterval(function () { 'filler',
axios {payment_hash: response.data.payment_hash}
.post( )
'/paywall/api/v1/paywalls/{{ paywall.id }}/check_invoice', .then(function (response) {
{payment_hash: response.data.payment_hash} if (response.data) {
) if (response.data.paid) {
.then(function (res) { self.cancelPayment()
if (res.data.paid) { self.redirectUrl = response.data.url
self.cancelPayment() if (response.data.remembers) {
self.redirectUrl = res.data.url self.$q.localStorage.set(
if (res.data.remembers) { 'lnbits.paywall.{{ paywall.id }}',
self.$q.localStorage.set( response.data.url
'lnbits.paywall.{{ paywall.id }}', )
res.data.url }
) self.$q.notify({
type: 'positive',
message: 'Payment received!',
icon: null
})
}
} }
})
self.$q.notify({ .catch(function (error) {
type: 'positive', LNbits.utils.notifyApiError(error)
message: 'Payment received!', })
icon: null }, 2000)
}) }
}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
}, 2000)
}) })
.catch(function (error) { .catch(function (error) {
LNbits.utils.notifyApiError(error) LNbits.utils.notifyApiError(error)
+4 -9
View File
@@ -52,20 +52,17 @@ async def api_paywall_delete(
raise HTTPException(status_code=HTTPStatus.NO_CONTENT) raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
@paywall_ext.post("/api/v1/paywalls/{paywall_id}/invoice") @paywall_ext.post("/api/v1/paywalls/invoice/{paywall_id}")
async def api_paywall_create_invoice( async def api_paywall_create_invoice(
paywall_id,
data: CreatePaywallInvoice, data: CreatePaywallInvoice,
wallet: WalletTypeInfo = Depends(get_key_type), paywall_id: str = Query(None)
): ):
paywall = await get_paywall(paywall_id) paywall = await get_paywall(paywall_id)
if data.amount < paywall.amount: if data.amount < paywall.amount:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, status_code=HTTPStatus.BAD_REQUEST,
detail=f"Minimum amount is {paywall.amount} sat.", detail=f"Minimum amount is {paywall.amount} sat.",
) )
try: try:
amount = data.amount if data.amount > paywall.amount else paywall.amount amount = data.amount if data.amount > paywall.amount else paywall.amount
payment_hash, payment_request = await create_invoice( payment_hash, payment_request = await create_invoice(
@@ -80,15 +77,14 @@ async def api_paywall_create_invoice(
return {"payment_hash": payment_hash, "payment_request": payment_request} return {"payment_hash": payment_hash, "payment_request": payment_request}
@paywall_ext.post("/api/v1/paywalls/{paywall_id}/check_invoice") @paywall_ext.post("/api/v1/paywalls/check_invoice/{paywall_id}")
async def api_paywal_check_invoice(data: CheckPaywallInvoice, paywall_id): async def api_paywal_check_invoice(data: CheckPaywallInvoice, paywall_id: str = Query(None)):
paywall = await get_paywall(paywall_id) paywall = await get_paywall(paywall_id)
payment_hash = data.payment_hash payment_hash = data.payment_hash
if not paywall: if not paywall:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Paywall does not exist." status_code=HTTPStatus.NOT_FOUND, detail="Paywall does not exist."
) )
try: try:
status = await check_invoice_status(paywall.wallet, payment_hash) status = await check_invoice_status(paywall.wallet, payment_hash)
is_paid = not status.pending is_paid = not status.pending
@@ -101,5 +97,4 @@ async def api_paywal_check_invoice(data: CheckPaywallInvoice, paywall_id):
await payment.set_pending(False) await payment.set_pending(False)
return {"paid": True, "url": paywall.url, "remembers": paywall.remembers} return {"paid": True, "url": paywall.url, "remembers": paywall.remembers}
return {"paid": False} return {"paid": False}
@@ -64,7 +64,7 @@
label="lightning⚡" label="lightning⚡"
> >
<q-tooltip> <q-tooltip>
bitcoin onchain payment method not available bitcoin lightning payment method not available
</q-tooltip> </q-tooltip>
</q-btn> </q-btn>
<q-btn <q-btn
@@ -86,7 +86,7 @@
label="onchain⛓️" label="onchain⛓️"
> >
<q-tooltip> <q-tooltip>
bitcoin lightning payment method not available bitcoin onchain payment method not available
</q-tooltip> </q-tooltip>
</q-btn> </q-btn>
<q-btn <q-btn
@@ -243,7 +243,7 @@
} }
}, },
methods: { methods: {
startPaymentNotifier(){ startPaymentNotifier() {
this.cancelListener() this.cancelListener()
this.cancelListener = LNbits.event.onInvoicePaid( this.cancelListener = LNbits.event.onInvoicePaid(
+1 -1
View File
@@ -60,7 +60,7 @@ class Service(BaseModel):
onchain: Optional[str] onchain: Optional[str]
servicename: str # Currently, this will just always be "Streamlabs" servicename: str # Currently, this will just always be "Streamlabs"
authenticated: bool # Whether a token (see below) has been acquired yet authenticated: bool # Whether a token (see below) has been acquired yet
token: Optional[int] # The token with which to authenticate requests token: Optional[str] # The token with which to authenticate requests
@classmethod @classmethod
def from_row(cls, row: Row) -> "Service": def from_row(cls, row: Row) -> "Service":
@@ -62,7 +62,7 @@
donationDialog: { donationDialog: {
show: false, show: false,
data: { data: {
name: '', name: null,
sats: '', sats: '',
message: '' message: ''
} }
+5 -3
View File
@@ -7,6 +7,7 @@ from starlette.responses import RedirectResponse
from lnbits.core.crud import get_user from lnbits.core.crud import get_user
from lnbits.decorators import WalletTypeInfo, get_key_type from lnbits.decorators import WalletTypeInfo, get_key_type
from lnbits.extensions.satspay.models import CreateCharge
from lnbits.extensions.streamalerts.models import ( from lnbits.extensions.streamalerts.models import (
CreateDonation, CreateDonation,
CreateService, CreateService,
@@ -113,17 +114,18 @@ async def api_create_donation(data: CreateDonation, request: Request):
service_id = data.service service_id = data.service
service = await get_service(service_id) service = await get_service(service_id)
charge_details = await get_charge_details(service.id) charge_details = await get_charge_details(service.id)
name = data.name name = data.name if data.name else "Anonymous"
description = f"{sats} sats donation from {name} to {service.twitchuser}" description = f"{sats} sats donation from {name} to {service.twitchuser}"
charge = await create_charge( create_charge_data = CreateCharge(
amount=sats, amount=sats,
completelink=f"https://twitch.tv/{service.twitchuser}", completelink=f"https://twitch.tv/{service.twitchuser}",
completelinktext="Back to Stream!", completelinktext="Back to Stream!",
webhook=webhook_base + "/streamalerts/api/v1/postdonation", webhook=webhook_base + "/streamalerts/api/v1/postdonation",
description=description, description=description,
**charge_details, **charge_details
) )
charge = await create_charge(user=charge_details["user"], data=create_charge_data)
await create_donation( await create_donation(
id=charge.id, id=charge.id,
wallet=service.wallet, wallet=service.wallet,
+3 -3
View File
@@ -1,5 +1,5 @@
from sqlite3 import Row from sqlite3 import Row
from typing import NamedTuple, Optional from typing import Optional
from fastapi.param_functions import Query from fastapi.param_functions import Query
from pydantic import BaseModel from pydantic import BaseModel
@@ -26,7 +26,7 @@ class createTip(BaseModel):
message: str = "" message: str = ""
class Tip(NamedTuple): class Tip(BaseModel):
"""A Tip represents a single donation""" """A Tip represents a single donation"""
id: str # This ID always corresponds to a satspay charge ID id: str # This ID always corresponds to a satspay charge ID
@@ -55,7 +55,7 @@ class createTips(BaseModel):
message: str message: str
class TipJar(NamedTuple): class TipJar(BaseModel):
"""A TipJar represents a user's tip jar""" """A TipJar represents a user's tip jar"""
id: int id: int
@@ -222,6 +222,7 @@
'INR', 'INR',
'IQD', 'IQD',
'IRR', 'IRR',
'IRT',
'ISK', 'ISK',
'JEP', 'JEP',
'JMD', 'JMD',
+1 -1
View File
@@ -112,7 +112,7 @@ async def api_get_addresses(wallet_id, w: WalletTypeInfo = Depends(get_key_type)
async def api_update_mempool( async def api_update_mempool(
endpoint: str = Query(...), w: WalletTypeInfo = Depends(require_admin_key) endpoint: str = Query(...), w: WalletTypeInfo = Depends(require_admin_key)
): ):
mempool = await update_mempool(endpoint, user=w.wallet.user) mempool = await update_mempool(**{"endpoint": endpoint}, user=w.wallet.user)
return mempool.dict() return mempool.dict()
@@ -22,7 +22,7 @@
<code>[&lt;withdraw_link_object&gt;, ...]</code> <code>[&lt;withdraw_link_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X GET {{ request.base_url }}api/v1/links -H "X-Api-Key: {{ >curl -X GET {{ request.base_url }}withdraw/api/v1/links -H "X-Api-Key: {{
user.wallets[0].inkey }}" user.wallets[0].inkey }}"
</code> </code>
</q-card-section> </q-card-section>
@@ -49,7 +49,7 @@
<code>{"lnurl": &lt;string&gt;}</code> <code>{"lnurl": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X GET {{ request.base_url }}api/v1/links/&lt;withdraw_id&gt; -H >curl -X GET {{ request.base_url }}withdraw/api/v1/links/&lt;withdraw_id&gt; -H
"X-Api-Key: {{ user.wallets[0].inkey }}" "X-Api-Key: {{ user.wallets[0].inkey }}"
</code> </code>
</q-card-section> </q-card-section>
@@ -78,7 +78,7 @@
<code>{"lnurl": &lt;string&gt;}</code> <code>{"lnurl": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X POST {{ request.base_url }}api/v1/links -d '{"title": >curl -X POST {{ request.base_url }}withdraw/api/v1/links -d '{"title":
&lt;string&gt;, "min_withdrawable": &lt;integer&gt;, &lt;string&gt;, "min_withdrawable": &lt;integer&gt;,
"max_withdrawable": &lt;integer&gt;, "uses": &lt;integer&gt;, "max_withdrawable": &lt;integer&gt;, "uses": &lt;integer&gt;,
"wait_time": &lt;integer&gt;, "is_unique": &lt;boolean&gt;}' -H "wait_time": &lt;integer&gt;, "is_unique": &lt;boolean&gt;}' -H
@@ -114,7 +114,7 @@
<code>{"lnurl": &lt;string&gt;}</code> <code>{"lnurl": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X PUT {{ request.base_url }}api/v1/links/&lt;withdraw_id&gt; -d >curl -X PUT {{ request.base_url }}withdraw/api/v1/links/&lt;withdraw_id&gt; -d
'{"title": &lt;string&gt;, "min_withdrawable": &lt;integer&gt;, '{"title": &lt;string&gt;, "min_withdrawable": &lt;integer&gt;,
"max_withdrawable": &lt;integer&gt;, "uses": &lt;integer&gt;, "max_withdrawable": &lt;integer&gt;, "uses": &lt;integer&gt;,
"wait_time": &lt;integer&gt;, "is_unique": &lt;boolean&gt;}' -H "wait_time": &lt;integer&gt;, "is_unique": &lt;boolean&gt;}' -H
@@ -142,7 +142,7 @@
<code></code> <code></code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X DELETE {{ request.base_url }}api/v1/links/&lt;withdraw_id&gt; >curl -X DELETE {{ request.base_url }}withdraw/api/v1/links/&lt;withdraw_id&gt;
-H "X-Api-Key: {{ user.wallets[0].adminkey }}" -H "X-Api-Key: {{ user.wallets[0].adminkey }}"
</code> </code>
</q-card-section> </q-card-section>
@@ -170,7 +170,7 @@
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X GET {{ request.base_url >curl -X GET {{ request.base_url
}}api/v1/links/&lt;the_hash&gt;/&lt;lnurl_id&gt; -H "X-Api-Key: {{ }}withdraw/api/v1/links/&lt;the_hash&gt;/&lt;lnurl_id&gt; -H "X-Api-Key: {{
user.wallets[0].inkey }}" user.wallets[0].inkey }}"
</code> </code>
</q-card-section> </q-card-section>
@@ -191,7 +191,7 @@
> >
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X GET {{ request.base_url }}/withdraw/img/&lt;lnurl_id&gt;" >curl -X GET {{ request.base_url }}withdraw/img/&lt;lnurl_id&gt;"
</code> </code>
</q-card-section> </q-card-section>
</q-card> </q-card>
+5
View File
@@ -71,6 +71,11 @@ async def api_link_create_or_update(
link_id: str = None, link_id: str = None,
wallet: WalletTypeInfo = Depends(require_admin_key), wallet: WalletTypeInfo = Depends(require_admin_key),
): ):
if data.min_withdrawable < 1:
raise HTTPException(
detail="Min must be more than 1.", status_code=HTTPStatus.BAD_REQUEST
)
if data.max_withdrawable < data.min_withdrawable: if data.max_withdrawable < data.min_withdrawable:
raise HTTPException( raise HTTPException(
detail="`max_withdrawable` needs to be at least `min_withdrawable`.", detail="`max_withdrawable` needs to be at least `min_withdrawable`.",
+3
View File
@@ -160,6 +160,9 @@ def template_renderer(additional_folders: List = []) -> Jinja2Templates:
["lnbits/templates", "lnbits/core/templates", *additional_folders] ["lnbits/templates", "lnbits/core/templates", *additional_folders]
) )
) )
if settings.LNBITS_AD_SPACE:
t.env.globals["AD_SPACE"] = settings.LNBITS_AD_SPACE
t.env.globals["HIDE_API"] = settings.LNBITS_HIDE_API
t.env.globals["SITE_TITLE"] = settings.LNBITS_SITE_TITLE t.env.globals["SITE_TITLE"] = settings.LNBITS_SITE_TITLE
t.env.globals["LNBITS_DENOMINATION"] = settings.LNBITS_DENOMINATION t.env.globals["LNBITS_DENOMINATION"] = settings.LNBITS_DENOMINATION
t.env.globals["SITE_TAGLINE"] = settings.LNBITS_SITE_TAGLINE t.env.globals["SITE_TAGLINE"] = settings.LNBITS_SITE_TAGLINE
+2
View File
@@ -34,6 +34,8 @@ LNBITS_DISABLED_EXTENSIONS: List[str] = env.list(
"LNBITS_DISABLED_EXTENSIONS", default=[], subcast=str "LNBITS_DISABLED_EXTENSIONS", default=[], subcast=str
) )
LNBITS_AD_SPACE = env.list("LNBITS_AD_SPACE", default=[])
LNBITS_HIDE_API = env.bool("LNBITS_HIDE_API", default=False)
LNBITS_SITE_TITLE = env.str("LNBITS_SITE_TITLE", default="LNbits") LNBITS_SITE_TITLE = env.str("LNBITS_SITE_TITLE", default="LNbits")
LNBITS_DENOMINATION = env.str("LNBITS_DENOMINATION", default="sats") LNBITS_DENOMINATION = env.str("LNBITS_DENOMINATION", default="sats")
LNBITS_SITE_TAGLINE = env.str( LNBITS_SITE_TAGLINE = env.str(
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

+11 -3
View File
@@ -345,17 +345,25 @@ window.windowMixin = {
} }
}, },
created: function () { created: function () {
this.$q.dark.set(this.$q.localStorage.getItem('lnbits.darkMode'))
this.g.allowedThemes = window.allowedThemes ?? ['classic'] if(this.$q.localStorage.getItem('lnbits.darkMode') == true || this.$q.localStorage.getItem('lnbits.darkMode') == false){
this.$q.dark.set(this.$q.localStorage.getItem('lnbits.darkMode'))
}
else{
this.$q.dark.set(true)
}
this.g.allowedThemes = window.allowedThemes ?? ['bitcoin']
// failsafe if admin changes themes halfway // failsafe if admin changes themes halfway
if (!this.$q.localStorage.getItem('lnbits.theme')){
this.changeColor(this.g.allowedThemes[0])
}
if ( if (
this.$q.localStorage.getItem('lnbits.theme') && this.$q.localStorage.getItem('lnbits.theme') &&
!this.g.allowedThemes.includes( !this.g.allowedThemes.includes(
this.$q.localStorage.getItem('lnbits.theme') this.$q.localStorage.getItem('lnbits.theme')
) )
) { ) {
console.log('allowedThemes changed by Admin', this.g.allowedThemes[0])
this.changeColor(this.g.allowedThemes[0]) this.changeColor(this.g.allowedThemes[0])
} }
+8
View File
@@ -7,6 +7,14 @@ $themes: (
marginal-bg: #1f2234, marginal-bg: #1f2234,
marginal-text: #fff marginal-text: #fff
), ),
'bitcoin': (
primary: #ff9853,
secondary: #ff7353,
dark: #2d293b,
info: #333646,
marginal-bg: #2d293b,
marginal-text: #fff
),
'mint': ( 'mint': (
primary: #3ab77d, primary: #3ab77d,
secondary: #27b065, secondary: #27b065,
+17 -4
View File
@@ -20,7 +20,7 @@
{% block head_scripts %}{% endblock %} {% block head_scripts %}{% endblock %}
</head> </head>
<body data-theme="classic"> <body data-theme="bitcoin">
<q-layout id="vue" view="hHh lpR lfr" v-cloak> <q-layout id="vue" view="hHh lpR lfr" v-cloak>
<q-header bordered class="bg-marginal-bg"> <q-header bordered class="bg-marginal-bg">
<q-toolbar> <q-toolbar>
@@ -67,8 +67,19 @@
icon="format_color_fill" icon="format_color_fill"
color="deep-purple" color="deep-purple"
size="md" size="md"
><q-tooltip>classic</q-tooltip> </q-btn ><q-tooltip>classic</q-tooltip>
><q-btn </q-btn>
<q-btn
v-if="g.allowedThemes.includes('bitcoin')"
dense
flat
@click="changeColor('bitcoin')"
icon="format_color_fill"
color="orange"
size="md"
><q-tooltip>bitcoin</q-tooltip>
</q-btn>
<q-btn
v-if="g.allowedThemes.includes('mint')" v-if="g.allowedThemes.includes('mint')"
dense dense
flat flat
@@ -151,11 +162,12 @@
</q-page> </q-page>
</q-page-container> </q-page-container>
{% endblock %} {% block footer %} {% endblock %} {% block footer %}
<q-footer <q-footer
class="bg-transparent q-px-lg q-py-md" class="bg-transparent q-px-lg q-py-md"
:class="{'text-dark': !$q.dark.isActive}" :class="{'text-dark': !$q.dark.isActive}"
> >
<q-toolbar> <q-toolbar class="gt-sm">
<q-toolbar-title class="text-caption"> <q-toolbar-title class="text-caption">
{{ SITE_TITLE }}, {{SITE_TAGLINE}} {{ SITE_TITLE }}, {{SITE_TAGLINE}}
<br /> <br />
@@ -179,6 +191,7 @@
</q-btn> </q-btn>
</q-toolbar> </q-toolbar>
</q-footer> </q-footer>
{% endblock %} {% endblock %}
</q-layout> </q-layout>
+7
View File
@@ -71,6 +71,7 @@ currencies = {
"IMP": "Isle of Man Pound", "IMP": "Isle of Man Pound",
"INR": "Indian Rupee", "INR": "Indian Rupee",
"IQD": "Iraqi Dinar", "IQD": "Iraqi Dinar",
"IRT": "Iranian Toman",
"ISK": "Icelandic Króna", "ISK": "Icelandic Króna",
"JEP": "Jersey Pound", "JEP": "Jersey Pound",
"JMD": "Jamaican Dollar", "JMD": "Jamaican Dollar",
@@ -179,6 +180,12 @@ class Provider(NamedTuple):
exchange_rate_providers = { exchange_rate_providers = {
"exir": Provider(
"Exir",
"exir.io",
"https://api.exir.io/v1/ticker?symbol={from}-{to}",
lambda data, replacements: data["last"],
),
"bitfinex": Provider( "bitfinex": Provider(
"Bitfinex", "Bitfinex",
"bitfinex.com", "bitfinex.com",
-1
View File
@@ -2,7 +2,6 @@
from .void import VoidWallet from .void import VoidWallet
from .clightning import CLightningWallet from .clightning import CLightningWallet
from .lndgrpc import LndWallet
from .lntxbot import LntxbotWallet from .lntxbot import LntxbotWallet
from .opennode import OpenNodeWallet from .opennode import OpenNodeWallet
from .lnpay import LNPayWallet from .lnpay import LNPayWallet
+3 -1
View File
@@ -60,7 +60,9 @@ class Wallet(ABC):
pass pass
@abstractmethod @abstractmethod
def pay_invoice(self, bolt11: str) -> Coroutine[None, None, PaymentResponse]: def pay_invoice(
self, bolt11: str, fee_limit_msat: int
) -> Coroutine[None, None, PaymentResponse]:
pass pass
@abstractmethod @abstractmethod
+13 -4
View File
@@ -18,6 +18,7 @@ from .base import (
Unsupported, Unsupported,
Wallet, Wallet,
) )
from lnbits import bolt11 as lnbits_bolt11
def async_wrap(func): def async_wrap(func):
@@ -31,8 +32,8 @@ def async_wrap(func):
return run return run
def _pay_invoice(ln, bolt11): def _pay_invoice(ln, payload):
return ln.pay(bolt11) return ln.call("pay", payload)
def _paid_invoices_stream(ln, last_pay_index): def _paid_invoices_stream(ln, last_pay_index):
@@ -102,10 +103,18 @@ class CLightningWallet(Wallet):
error_message = f"lightningd '{exc.method}' failed with '{exc.error}'." error_message = f"lightningd '{exc.method}' failed with '{exc.error}'."
return InvoiceResponse(False, label, None, error_message) return InvoiceResponse(False, label, None, error_message)
async def pay_invoice(self, bolt11: str) -> PaymentResponse: async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
invoice = lnbits_bolt11.decode(bolt11)
fee_limit_percent = fee_limit_msat / invoice.amount_msat * 100
payload = {
"bolt11": bolt11,
"maxfeepercent": "{:.11}".format(fee_limit_percent),
"exemptfee": 0, # so fee_limit_percent is applied even on payments with fee under 5000 millisatoshi (which is default value of exemptfee)
}
try: try:
wrapped = async_wrap(_pay_invoice) wrapped = async_wrap(_pay_invoice)
r = await wrapped(self.ln, bolt11) r = await wrapped(self.ln, payload)
except RpcError as exc: except RpcError as exc:
return PaymentResponse(False, None, 0, None, str(exc)) return PaymentResponse(False, None, 0, None, str(exc))
+22 -9
View File
@@ -36,7 +36,13 @@ class FakeWallet(Wallet):
"out": False, "out": False,
"amount": amount, "amount": amount,
"currency": "bc", "currency": "bc",
"privkey": hashlib.pbkdf2_hmac('sha256', secret.encode("utf-8"), ("FakeWallet").encode("utf-8"), 2048, 32).hex(), "privkey": hashlib.pbkdf2_hmac(
"sha256",
secret.encode("utf-8"),
("FakeWallet").encode("utf-8"),
2048,
32,
).hex(),
"memo": None, "memo": None,
"description_hash": None, "description_hash": None,
"description": "", "description": "",
@@ -53,22 +59,29 @@ class FakeWallet(Wallet):
data["tags_set"] = ["d"] data["tags_set"] = ["d"]
data["memo"] = memo data["memo"] = memo
data["description"] = memo data["description"] = memo
randomHash = data["privkey"][:6] + hashlib.sha256( randomHash = (
str(random.getrandbits(256)).encode("utf-8") data["privkey"][:6]
).hexdigest()[6:] + hashlib.sha256(str(random.getrandbits(256)).encode("utf-8")).hexdigest()[
6:
]
)
data["paymenthash"] = randomHash data["paymenthash"] = randomHash
payment_request = encode(data) payment_request = encode(data)
checking_id = randomHash checking_id = randomHash
return InvoiceResponse(True, checking_id, payment_request) return InvoiceResponse(True, checking_id, payment_request)
async def pay_invoice(self, bolt11: str) -> PaymentResponse: async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
invoice = decode(bolt11) invoice = decode(bolt11)
if hasattr(invoice, 'checking_id') and invoice.checking_id[6:] == data["privkey"][:6]: if (
hasattr(invoice, "checking_id")
and invoice.checking_id[6:] == data["privkey"][:6]
):
return PaymentResponse(True, invoice.payment_hash, 0) return PaymentResponse(True, invoice.payment_hash, 0)
else: else:
return PaymentResponse(ok = False, error_message="Only internal invoices can be used!") return PaymentResponse(
ok=False, error_message="Only internal invoices can be used!"
)
async def get_invoice_status(self, checking_id: str) -> PaymentStatus: async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
return PaymentStatus(False) return PaymentStatus(False)
+1 -1
View File
@@ -80,7 +80,7 @@ class LNbitsWallet(Wallet):
return InvoiceResponse(ok, checking_id, payment_request, error_message) return InvoiceResponse(ok, checking_id, payment_request, error_message)
async def pay_invoice(self, bolt11: str) -> PaymentResponse: async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
r = await client.post( r = await client.post(
url=f"{self.endpoint}/api/v1/payments", url=f"{self.endpoint}/api/v1/payments",
+12 -15
View File
@@ -11,6 +11,7 @@ import base64
import hashlib import hashlib
from os import environ, error, getenv from os import environ, error, getenv
from typing import Optional, Dict, AsyncGenerator from typing import Optional, Dict, AsyncGenerator
from .macaroon import load_macaroon, AESCipher
if imports_ok: if imports_ok:
import lnbits.wallets.lnd_grpc_files.lightning_pb2 as ln import lnbits.wallets.lnd_grpc_files.lightning_pb2 as ln
@@ -58,12 +59,6 @@ def get_ssl_context(cert_path: str):
return context return context
def load_macaroon(macaroon_path: str):
with open(macaroon_path, "rb") as f:
macaroon_bytes = f.read()
return macaroon_bytes.hex()
def parse_checking_id(checking_id: str) -> bytes: def parse_checking_id(checking_id: str) -> bytes:
return base64.b64decode(checking_id.replace("_", "/")) return base64.b64decode(checking_id.replace("_", "/"))
@@ -90,7 +85,7 @@ class LndWallet(Wallet):
self.port = int(getenv("LND_GRPC_PORT")) self.port = int(getenv("LND_GRPC_PORT"))
self.cert_path = getenv("LND_GRPC_CERT") or getenv("LND_CERT") self.cert_path = getenv("LND_GRPC_CERT") or getenv("LND_CERT")
macaroon_path = ( macaroon = (
getenv("LND_GRPC_MACAROON") getenv("LND_GRPC_MACAROON")
or getenv("LND_GRPC_ADMIN_MACAROON") or getenv("LND_GRPC_ADMIN_MACAROON")
or getenv("LND_ADMIN_MACAROON") or getenv("LND_ADMIN_MACAROON")
@@ -98,10 +93,12 @@ class LndWallet(Wallet):
or getenv("LND_INVOICE_MACAROON") or getenv("LND_INVOICE_MACAROON")
) )
if macaroon_path.split(".")[-1] == "macaroon": encrypted_macaroon = getenv("LND_GRPC_MACAROON_ENCRYPTED")
self.macaroon = load_macaroon(macaroon_path) if encrypted_macaroon:
else: macaroon = AESCipher(description="macaroon decryption").decrypt(
self.macaroon = macaroon_path encrypted_macaroon
)
self.macaroon = load_macaroon(macaroon)
cert = open(self.cert_path, "rb").read() cert = open(self.cert_path, "rb").read()
creds = grpc.ssl_channel_credentials(cert) creds = grpc.ssl_channel_credentials(cert)
@@ -147,10 +144,10 @@ class LndWallet(Wallet):
payment_request = str(resp.payment_request) payment_request = str(resp.payment_request)
return InvoiceResponse(True, checking_id, payment_request, None) return InvoiceResponse(True, checking_id, payment_request, None)
async def pay_invoice(self, bolt11: str) -> PaymentResponse: async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
resp = await self.rpc.SendPayment( fee_limit_fixed = ln.FeeLimit(fixed=fee_limit_msat // 1000)
lnrpc.SendPaymentRequest(payment_request=bolt11) req = ln.SendRequest(payment_request=bolt11, fee_limit=fee_limit_fixed)
) resp = await self.rpc.SendPaymentSync(req)
if resp.payment_error: if resp.payment_error:
return PaymentResponse(False, "", 0, None, resp.payment_error) return PaymentResponse(False, "", 0, None, resp.payment_error)
+15 -8
View File
@@ -1,4 +1,5 @@
import asyncio import asyncio
from pydoc import describe
import httpx import httpx
import json import json
import base64 import base64
@@ -6,6 +7,7 @@ from os import getenv
from typing import Optional, Dict, AsyncGenerator from typing import Optional, Dict, AsyncGenerator
from lnbits import bolt11 as lnbits_bolt11 from lnbits import bolt11 as lnbits_bolt11
from .macaroon import load_macaroon, AESCipher
from .base import ( from .base import (
StatusResponse, StatusResponse,
@@ -34,8 +36,16 @@ class LndRestWallet(Wallet):
or getenv("LND_INVOICE_MACAROON") or getenv("LND_INVOICE_MACAROON")
or getenv("LND_REST_INVOICE_MACAROON") or getenv("LND_REST_INVOICE_MACAROON")
) )
self.auth = {"Grpc-Metadata-macaroon": macaroon}
self.cert = getenv("LND_REST_CERT") encrypted_macaroon = getenv("LND_REST_MACAROON_ENCRYPTED")
if encrypted_macaroon:
macaroon = AESCipher(description="macaroon decryption").decrypt(
encrypted_macaroon
)
self.macaroon = load_macaroon(macaroon)
self.auth = {"Grpc-Metadata-macaroon": self.macaroon}
self.cert = getenv("LND_REST_CERT", True)
async def status(self) -> StatusResponse: async def status(self) -> StatusResponse:
try: try:
@@ -89,15 +99,11 @@ class LndRestWallet(Wallet):
return InvoiceResponse(True, checking_id, payment_request, None) return InvoiceResponse(True, checking_id, payment_request, None)
async def pay_invoice(self, bolt11: str) -> PaymentResponse: async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
async with httpx.AsyncClient(verify=self.cert) as client: async with httpx.AsyncClient(verify=self.cert) as client:
# set the fee limit for the payment # set the fee limit for the payment
invoice = lnbits_bolt11.decode(bolt11)
lnrpcFeeLimit = dict() lnrpcFeeLimit = dict()
if invoice.amount_msat > 1000_000: lnrpcFeeLimit["fixed_msat"] = "{}".format(fee_limit_msat)
lnrpcFeeLimit["percent"] = "1" # in percent
else:
lnrpcFeeLimit["fixed"] = "10" # in sat
r = await client.post( r = await client.post(
url=f"{self.endpoint}/v1/channels/transactions", url=f"{self.endpoint}/v1/channels/transactions",
@@ -154,6 +160,7 @@ class LndRestWallet(Wallet):
# for some reason our checking_ids are in base64 but the payment hashes # for some reason our checking_ids are in base64 but the payment hashes
# returned here are in hex, lnd is weird # returned here are in hex, lnd is weird
checking_id = checking_id.replace("_", "/")
checking_id = base64.b64decode(checking_id).hex() checking_id = base64.b64decode(checking_id).hex()
for p in r.json()["payments"]: for p in r.json()["payments"]:
+1 -1
View File
@@ -76,7 +76,7 @@ class LNPayWallet(Wallet):
return InvoiceResponse(ok, checking_id, payment_request, error_message) return InvoiceResponse(ok, checking_id, payment_request, error_message)
async def pay_invoice(self, bolt11: str) -> PaymentResponse: async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
r = await client.post( r = await client.post(
f"{self.endpoint}/wallet/{self.wallet_key}/withdraw", f"{self.endpoint}/wallet/{self.wallet_key}/withdraw",
+1 -1
View File
@@ -74,7 +74,7 @@ class LntxbotWallet(Wallet):
data = r.json() data = r.json()
return InvoiceResponse(True, data["payment_hash"], data["pay_req"], None) return InvoiceResponse(True, data["payment_hash"], data["pay_req"], None)
async def pay_invoice(self, bolt11: str) -> PaymentResponse: async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
r = await client.post( r = await client.post(
f"{self.endpoint}/payinvoice", f"{self.endpoint}/payinvoice",
+1
View File
@@ -0,0 +1 @@
from .macaroon import load_macaroon, AESCipher
+103
View File
@@ -0,0 +1,103 @@
from Cryptodome import Random
from Cryptodome.Cipher import AES
import base64
from hashlib import md5
import getpass
BLOCK_SIZE = 16
import getpass
def load_macaroon(macaroon: str) -> str:
"""Returns hex version of a macaroon encoded in base64 or the file path.
:param macaroon: Macaroon encoded in base64 or file path.
:type macaroon: str
:return: Hex version of macaroon.
:rtype: str
"""
# if the macaroon is a file path, load it
if macaroon.split(".")[-1] == "macaroon":
with open(macaroon, "rb") as f:
macaroon_bytes = f.read()
return macaroon_bytes.hex()
else:
# convert the bas64 macaroon to hex
try:
macaroon = base64.b64decode(macaroon).hex()
except:
pass
return macaroon
class AESCipher(object):
"""This class is compatible with crypto-js/aes.js
Encrypt and decrypt in Javascript using:
import AES from "crypto-js/aes.js";
import Utf8 from "crypto-js/enc-utf8.js";
AES.encrypt(decrypted, password).toString()
AES.decrypt(encrypted, password).toString(Utf8);
"""
def __init__(self, key=None, description=""):
self.key = key
self.description = description + " "
def pad(self, data):
length = BLOCK_SIZE - (len(data) % BLOCK_SIZE)
return data + (chr(length) * length).encode()
def unpad(self, data):
return data[: -(data[-1] if type(data[-1]) == int else ord(data[-1]))]
@property
def passphrase(self):
passphrase = self.key if self.key is not None else None
if passphrase is None:
passphrase = getpass.getpass(f"Enter {self.description}password:")
return passphrase
def bytes_to_key(self, data, salt, output=48):
# extended from https://gist.github.com/gsakkis/4546068
assert len(salt) == 8, len(salt)
data += salt
key = md5(data).digest()
final_key = key
while len(final_key) < output:
key = md5(key + data).digest()
final_key += key
return final_key[:output]
def decrypt(self, encrypted: str) -> str:
"""Decrypts a string using AES-256-CBC.
"""
passphrase = self.passphrase
encrypted = base64.b64decode(encrypted)
assert encrypted[0:8] == b"Salted__"
salt = encrypted[8:16]
key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16)
key = key_iv[:32]
iv = key_iv[32:]
aes = AES.new(key, AES.MODE_CBC, iv)
try:
return self.unpad(aes.decrypt(encrypted[16:])).decode()
except UnicodeDecodeError:
raise ValueError("Wrong passphrase")
def encrypt(self, message: bytes) -> str:
passphrase = self.passphrase
salt = Random.new().read(8)
key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16)
key = key_iv[:32]
iv = key_iv[32:]
aes = AES.new(key, AES.MODE_CBC, iv)
return base64.b64encode(b"Salted__" + salt + aes.encrypt(self.pad(message))).decode()
# if this file is executed directly, ask for a macaroon and encrypt it
if __name__ == "__main__":
macaroon = input("Enter macaroon: ")
macaroon = load_macaroon(macaroon)
macaroon = AESCipher(description="encryption").encrypt(macaroon.encode())
print("Encrypted macaroon:")
print(macaroon)
+1 -1
View File
@@ -77,7 +77,7 @@ class OpenNodeWallet(Wallet):
payment_request = data["lightning_invoice"]["payreq"] payment_request = data["lightning_invoice"]["payreq"]
return InvoiceResponse(True, checking_id, payment_request, None) return InvoiceResponse(True, checking_id, payment_request, None)
async def pay_invoice(self, bolt11: str) -> PaymentResponse: async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
r = await client.post( r = await client.post(
f"{self.endpoint}/v2/withdrawals", f"{self.endpoint}/v2/withdrawals",
+1 -1
View File
@@ -107,7 +107,7 @@ class SparkWallet(Wallet):
return InvoiceResponse(ok, checking_id, payment_request, error_message) return InvoiceResponse(ok, checking_id, payment_request, error_message)
async def pay_invoice(self, bolt11: str) -> PaymentResponse: async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
try: try:
r = await self.pay(bolt11) r = await self.pay(bolt11)
except (SparkError, UnknownError) as exc: except (SparkError, UnknownError) as exc:
+1 -1
View File
@@ -25,7 +25,7 @@ class VoidWallet(Wallet):
) )
return StatusResponse(None, 0) return StatusResponse(None, 0)
async def pay_invoice(self, bolt11: str) -> PaymentResponse: async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
raise Unsupported("") raise Unsupported("")
async def get_invoice_status(self, checking_id: str) -> PaymentStatus: async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
+2 -1
View File
@@ -25,6 +25,7 @@ markupsafe==2.0.1
marshmallow==3.13.0 marshmallow==3.13.0
outcome==1.1.0 outcome==1.1.0
psycopg2-binary==2.9.1 psycopg2-binary==2.9.1
pycryptodomex==3.14.1
pydantic==1.8.2 pydantic==1.8.2
pypng==0.0.21 pypng==0.0.21
pyqrcode==1.2.1 pyqrcode==1.2.1
@@ -46,4 +47,4 @@ uvicorn==0.15.0
uvloop==0.16.0 uvloop==0.16.0
watchgod==0.7 watchgod==0.7
websockets==10.0 websockets==10.0
zipp==3.5.0 zipp==3.5.0
+25 -12
View File
@@ -3,24 +3,30 @@ import secrets
from lnbits.core.crud import get_wallet from lnbits.core.crud import get_wallet
from lnbits.settings import HOST, PORT from lnbits.settings import HOST, PORT
from lnbits.extensions.bleskomat.crud import get_bleskomat_lnurl from lnbits.extensions.bleskomat.crud import get_bleskomat_lnurl
from lnbits.extensions.bleskomat.helpers import generate_bleskomat_lnurl_signature, query_to_signing_payload from lnbits.extensions.bleskomat.helpers import (
generate_bleskomat_lnurl_signature,
query_to_signing_payload,
)
from tests.conftest import client from tests.conftest import client
from tests.helpers import credit_wallet from tests.helpers import credit_wallet
from tests.extensions.bleskomat.conftest import bleskomat, lnurl from tests.extensions.bleskomat.conftest import bleskomat, lnurl
from tests.mocks import WALLET from tests.mocks import WALLET
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_bleskomat_lnurl_api_missing_secret(client): async def test_bleskomat_lnurl_api_missing_secret(client):
response = await client.get("/bleskomat/u") response = await client.get("/bleskomat/u")
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == {"status": "ERROR", "reason": "Missing secret"} assert response.json() == {"status": "ERROR", "reason": "Missing secret"}
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_bleskomat_lnurl_api_invalid_secret(client): async def test_bleskomat_lnurl_api_invalid_secret(client):
response = await client.get("/bleskomat/u?k1=invalid-secret") response = await client.get("/bleskomat/u?k1=invalid-secret")
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == {"status": "ERROR", "reason": "Invalid secret"} assert response.json() == {"status": "ERROR", "reason": "Invalid secret"}
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_bleskomat_lnurl_api_unknown_api_key(client): async def test_bleskomat_lnurl_api_unknown_api_key(client):
query = { query = {
@@ -33,11 +39,12 @@ async def test_bleskomat_lnurl_api_unknown_api_key(client):
"f": "EUR", "f": "EUR",
} }
payload = query_to_signing_payload(query) payload = query_to_signing_payload(query)
signature = "xxx"# not checked, so doesn't matter signature = "xxx" # not checked, so doesn't matter
response = await client.get(f'/bleskomat/u?{payload}&signature={signature}') response = await client.get(f"/bleskomat/u?{payload}&signature={signature}")
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == {"status": "ERROR", "reason": "Unknown API key"} assert response.json() == {"status": "ERROR", "reason": "Unknown API key"}
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_bleskomat_lnurl_api_invalid_signature(client, bleskomat): async def test_bleskomat_lnurl_api_invalid_signature(client, bleskomat):
query = { query = {
@@ -51,10 +58,11 @@ async def test_bleskomat_lnurl_api_invalid_signature(client, bleskomat):
} }
payload = query_to_signing_payload(query) payload = query_to_signing_payload(query)
signature = "invalid" signature = "invalid"
response = await client.get(f'/bleskomat/u?{payload}&signature={signature}') response = await client.get(f"/bleskomat/u?{payload}&signature={signature}")
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == {"status": "ERROR", "reason": "Invalid API key signature"} assert response.json() == {"status": "ERROR", "reason": "Invalid API key signature"}
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_bleskomat_lnurl_api_valid_signature(client, bleskomat): async def test_bleskomat_lnurl_api_valid_signature(client, bleskomat):
query = { query = {
@@ -64,41 +72,46 @@ async def test_bleskomat_lnurl_api_valid_signature(client, bleskomat):
"minWithdrawable": "1", "minWithdrawable": "1",
"maxWithdrawable": "1", "maxWithdrawable": "1",
"defaultDescription": "test valid sig", "defaultDescription": "test valid sig",
"f": "EUR",# tests use the dummy exchange rate provider "f": "EUR", # tests use the dummy exchange rate provider
} }
payload = query_to_signing_payload(query) payload = query_to_signing_payload(query)
signature = generate_bleskomat_lnurl_signature( signature = generate_bleskomat_lnurl_signature(
payload=payload, payload=payload,
api_key_secret=bleskomat.api_key_secret, api_key_secret=bleskomat.api_key_secret,
api_key_encoding=bleskomat.api_key_encoding api_key_encoding=bleskomat.api_key_encoding,
) )
response = await client.get(f'/bleskomat/u?{payload}&signature={signature}') response = await client.get(f"/bleskomat/u?{payload}&signature={signature}")
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
assert data["tag"] == "withdrawRequest" assert data["tag"] == "withdrawRequest"
assert data["minWithdrawable"] == 1000 assert data["minWithdrawable"] == 1000
assert data["maxWithdrawable"] == 1000 assert data["maxWithdrawable"] == 1000
assert data["defaultDescription"] == "test valid sig" assert data["defaultDescription"] == "test valid sig"
assert data["callback"] == f'http://{HOST}:{PORT}/bleskomat/u' assert data["callback"] == f"http://{HOST}:{PORT}/bleskomat/u"
k1 = data["k1"] k1 = data["k1"]
lnurl = await get_bleskomat_lnurl(secret=k1) lnurl = await get_bleskomat_lnurl(secret=k1)
assert lnurl assert lnurl
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_bleskomat_lnurl_api_action_insufficient_balance(client, lnurl): async def test_bleskomat_lnurl_api_action_insufficient_balance(client, lnurl):
bleskomat = lnurl["bleskomat"] bleskomat = lnurl["bleskomat"]
secret = lnurl["secret"] secret = lnurl["secret"]
pr = "lntb500n1pseq44upp5xqd38rgad72lnlh4gl339njlrsl3ykep82j6gj4g02dkule7k54qdqqcqzpgxqyz5vqsp5h0zgewuxdxcl2rnlumh6g520t4fr05rgudakpxm789xgjekha75s9qyyssq5vhwsy9knhfeqg0wn6hcnppwmum8fs3g3jxkgw45havgfl6evchjsz3s8e8kr6eyacz02szdhs7v5lg0m7wehd5rpf6yg8480cddjlqpae52xu" pr = "lntb500n1pseq44upp5xqd38rgad72lnlh4gl339njlrsl3ykep82j6gj4g02dkule7k54qdqqcqzpgxqyz5vqsp5h0zgewuxdxcl2rnlumh6g520t4fr05rgudakpxm789xgjekha75s9qyyssq5vhwsy9knhfeqg0wn6hcnppwmum8fs3g3jxkgw45havgfl6evchjsz3s8e8kr6eyacz02szdhs7v5lg0m7wehd5rpf6yg8480cddjlqpae52xu"
WALLET.pay_invoice.reset_mock() WALLET.pay_invoice.reset_mock()
response = await client.get(f'/bleskomat/u?k1={secret}&pr={pr}') response = await client.get(f"/bleskomat/u?k1={secret}&pr={pr}")
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == {"status": "ERROR", "reason": "Failed to pay invoice: Insufficient balance."} assert response.json()["status"] == "ERROR"
assert ("Insufficient balance" in response.json()["reason"]) or (
"fee" in response.json()["reason"]
)
wallet = await get_wallet(bleskomat.wallet) wallet = await get_wallet(bleskomat.wallet)
assert wallet.balance_msat == 0 assert wallet.balance_msat == 0
bleskomat_lnurl = await get_bleskomat_lnurl(secret) bleskomat_lnurl = await get_bleskomat_lnurl(secret)
assert bleskomat_lnurl.has_uses_remaining() == True assert bleskomat_lnurl.has_uses_remaining() == True
WALLET.pay_invoice.assert_not_called() WALLET.pay_invoice.assert_not_called()
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_bleskomat_lnurl_api_action_success(client, lnurl): async def test_bleskomat_lnurl_api_action_success(client, lnurl):
bleskomat = lnurl["bleskomat"] bleskomat = lnurl["bleskomat"]
@@ -111,10 +124,10 @@ async def test_bleskomat_lnurl_api_action_success(client, lnurl):
wallet = await get_wallet(bleskomat.wallet) wallet = await get_wallet(bleskomat.wallet)
assert wallet.balance_msat == 100000 assert wallet.balance_msat == 100000
WALLET.pay_invoice.reset_mock() WALLET.pay_invoice.reset_mock()
response = await client.get(f'/bleskomat/u?k1={secret}&pr={pr}') response = await client.get(f"/bleskomat/u?k1={secret}&pr={pr}")
assert response.json() == {"status": "OK"} assert response.json() == {"status": "OK"}
wallet = await get_wallet(bleskomat.wallet) wallet = await get_wallet(bleskomat.wallet)
assert wallet.balance_msat == 50000 assert wallet.balance_msat == 50000
bleskomat_lnurl = await get_bleskomat_lnurl(secret) bleskomat_lnurl = await get_bleskomat_lnurl(secret)
assert bleskomat_lnurl.has_uses_remaining() == False assert bleskomat_lnurl.has_uses_remaining() == False
WALLET.pay_invoice.assert_called_once_with(pr) WALLET.pay_invoice.assert_called_once_with(pr, 2000)
+35 -21
View File
@@ -9,28 +9,42 @@ from lnbits.wallets.base import (
) )
from lnbits.settings import WALLET from lnbits.settings import WALLET
WALLET.status = AsyncMock(return_value=StatusResponse( WALLET.status = AsyncMock(
"",# no error return_value=StatusResponse(
1000000,# msats "", # no error
)) 1000000, # msats
WALLET.create_invoice = AsyncMock(return_value=InvoiceResponse( )
True,# ok )
"6621aafbdd7709ca6eea6203f362d64bd7cb2911baa91311a176b3ecaf2274bd",# checking_id (i.e. payment_hash) WALLET.create_invoice = AsyncMock(
"lntb1u1psezhgspp5vcs6477awuyu5mh2vgplxckkf0tuk2g3h253xydpw6e7etezwj7sdqqcqzpgxqyz5vqsp5dxpw8zs77hw5pla4wz4mfujllyxtlpu443auur2uxqdrs8q2h56q9qyyssq65zk30ylmygvv5y4tuwalnf3ttnqjn57ef6rmcqg0s53akem560jh8ptemjcmytn3lrlatw4hv9smg88exv3v4f4lqnp96s0psdrhxsp6pp75q",# payment_request return_value=InvoiceResponse(
"",# no error True, # ok
)) "6621aafbdd7709ca6eea6203f362d64bd7cb2911baa91311a176b3ecaf2274bd", # checking_id (i.e. payment_hash)
def pay_invoice_side_effect(payment_request: str): "lntb1u1psezhgspp5vcs6477awuyu5mh2vgplxckkf0tuk2g3h253xydpw6e7etezwj7sdqqcqzpgxqyz5vqsp5dxpw8zs77hw5pla4wz4mfujllyxtlpu443auur2uxqdrs8q2h56q9qyyssq65zk30ylmygvv5y4tuwalnf3ttnqjn57ef6rmcqg0s53akem560jh8ptemjcmytn3lrlatw4hv9smg88exv3v4f4lqnp96s0psdrhxsp6pp75q", # payment_request
"", # no error
)
)
def pay_invoice_side_effect(
payment_request: str, fee_limit_msat: int
) -> PaymentResponse:
invoice = bolt11.decode(payment_request) invoice = bolt11.decode(payment_request)
return PaymentResponse( return PaymentResponse(
True,# ok True, # ok
invoice.payment_hash,# checking_id (i.e. payment_hash) invoice.payment_hash, # checking_id (i.e. payment_hash)
0,# fee_msat 0, # fee_msat
"",# no error "", # no error
) )
WALLET.pay_invoice = AsyncMock(side_effect=pay_invoice_side_effect) WALLET.pay_invoice = AsyncMock(side_effect=pay_invoice_side_effect)
WALLET.get_invoice_status = AsyncMock(return_value=PaymentStatus( WALLET.get_invoice_status = AsyncMock(
True,# paid return_value=PaymentStatus(
)) True, # paid
WALLET.get_payment_status = AsyncMock(return_value=PaymentStatus( )
True,# paid )
)) WALLET.get_payment_status = AsyncMock(
return_value=PaymentStatus(
True, # paid
)
)