Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
810b918619 | ||
|
|
81860bdc5d | ||
|
|
c5c76468cb | ||
|
|
799065e982 | ||
|
|
a94444ae2c | ||
|
|
54264315f0 | ||
|
|
946ebc350e | ||
|
|
b1c75813a0 | ||
|
|
a17108f108 | ||
|
|
1209526147 | ||
|
|
0f97f8f18b | ||
|
|
911fe92e03 | ||
|
|
eb8bbd3896 | ||
|
|
ff70efd1eb | ||
|
|
ce6620882f | ||
|
|
ffba97850b | ||
|
|
435b4056fd | ||
|
|
30a0d6336b | ||
|
|
52ef2da76d | ||
|
|
e4dd0972a5 | ||
|
|
c708cb1e2b | ||
|
|
43f9394014 | ||
|
|
9bbf78e026 | ||
|
|
6df951b873 | ||
|
|
d62ef0bb80 | ||
|
|
4a9e7f4264 | ||
|
|
b886e5b0ea | ||
|
|
72f2ec0be8 | ||
|
|
ef6d12fe10 | ||
|
|
1e759283ce | ||
|
|
e92447092d | ||
|
|
7988e2c303 | ||
|
|
a13bb9cfea | ||
|
|
7088a3240f | ||
|
|
00e52001b5 | ||
|
|
cd35966a56 | ||
|
|
c59a65cdb4 | ||
|
|
5f52658626 | ||
|
|
2399b05855 | ||
|
|
8fb70041d2 | ||
|
|
baf55afc28 | ||
|
|
251e6a8233 | ||
|
|
d73c3950b9 | ||
|
|
ac61cd5427 | ||
|
|
0c14b8fb82 | ||
|
|
6c0aa8fa8d | ||
|
|
9b95a7743d | ||
|
|
e96701cb59 | ||
|
|
c1fa5bbcbf | ||
|
|
241c81c790 | ||
|
|
42b444f54d | ||
|
|
f7ff9d27d2 | ||
|
|
4a588dda1b | ||
|
|
e82ce89e9a | ||
|
|
270cac37ac | ||
|
|
a374526faf | ||
|
|
8930152b0f | ||
|
|
71e11b7387 | ||
|
|
510a28f3d0 | ||
|
|
c58e2c5d74 | ||
|
|
eb7b369b6d | ||
|
|
a5a3331106 | ||
|
|
4f3e48e280 | ||
|
|
31bcb32e2d | ||
|
|
a132387fa8 | ||
|
|
27694ee23d | ||
|
|
36952981c2 | ||
|
|
fbafe2b8f1 | ||
|
|
41a9689014 | ||
|
|
76edfa2134 | ||
|
|
d7b3a28950 | ||
|
|
2c161a079d | ||
|
|
4793ea0d18 | ||
|
|
88df4266b7 | ||
|
|
6496fac35f | ||
|
|
10492ec3d0 | ||
|
|
302d79a30f | ||
|
|
b88abd1a46 | ||
|
|
573c4cf5ea | ||
|
|
e9065833c0 | ||
|
|
251ecb8f1d | ||
|
|
c626e12fa3 | ||
|
|
13a72d9a41 | ||
|
|
3c5fac3874 | ||
|
|
15c8557459 | ||
|
|
d559c604fa | ||
|
|
cd84623305 | ||
|
|
9fb30080ab | ||
|
|
30ab519c3a | ||
|
|
914b9f3ffe | ||
|
|
0764f4fdf1 | ||
|
|
0367ee85a7 | ||
|
|
0aef9d4d9a | ||
|
|
8967011cbe | ||
|
|
682226c4b6 | ||
|
|
c7e501ac08 | ||
|
|
8d7302e4cd | ||
|
|
09b86605dd | ||
|
|
9e8376b3fb | ||
|
|
1a492cbfb8 | ||
|
|
155c32504a | ||
|
|
8c923ad653 | ||
|
|
25b9014c3d | ||
|
|
9807e7d646 | ||
|
|
728ab517f5 | ||
|
|
f1fb569e2b | ||
|
|
6d58027147 | ||
|
|
4a35f84e95 | ||
|
|
f7b37fb843 | ||
|
|
699bdd9a06 | ||
|
|
b5aad94ecd | ||
|
|
0f79213b0f | ||
|
|
c1654a21d4 | ||
|
|
da6a2e772b | ||
|
|
1626cd2b4b | ||
|
|
d1d62583c4 | ||
|
|
4c18eb0b34 | ||
|
|
35bb26b6fd | ||
|
|
2c853e7a52 | ||
|
|
6bc1474c19 | ||
|
|
4b11342f0c | ||
|
|
b00a39a7ce | ||
|
|
b4dc07170f |
+12
-11
@@ -5,11 +5,18 @@ QUART_DEBUG=true
|
||||
HOST=127.0.0.1
|
||||
PORT=5000
|
||||
|
||||
|
||||
LNBITS_ALLOWED_USERS=""
|
||||
LNBITS_ADMIN_USERS=""
|
||||
# Extensions only admin can access
|
||||
LNBITS_ADMIN_EXTENSIONS="ngrok"
|
||||
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
|
||||
LNBITS_DISABLED_EXTENSIONS="amilk"
|
||||
|
||||
# Database: to use SQLite, specify LNBITS_DATA_FOLDER
|
||||
# to use PostgreSQL, specify LNBITS_DATABASE_URL=postgres://...
|
||||
# to use CockroachDB, specify LNBITS_DATABASE_URL=cockroachdb://...
|
||||
@@ -18,8 +25,6 @@ LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
|
||||
LNBITS_DATA_FOLDER="./data"
|
||||
# LNBITS_DATABASE_URL="postgres://user:password@host:port/databasename"
|
||||
|
||||
# disable selected extensions, or use "all" to disable all extensions
|
||||
LNBITS_DISABLED_EXTENSIONS="amilk,ngrok"
|
||||
LNBITS_FORCE_HTTPS=true
|
||||
LNBITS_SERVICE_FEE="0.0"
|
||||
|
||||
@@ -28,9 +33,9 @@ LNBITS_SITE_TITLE="LNbits"
|
||||
LNBITS_SITE_TAGLINE="free and open-source lightning wallet"
|
||||
LNBITS_SITE_DESCRIPTION="Some description about your service, will display if title is not 'LNbits'"
|
||||
# Choose from mint, flamingo, 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
|
||||
LNBITS_BACKEND_WALLET_CLASS=VoidWallet
|
||||
# VoidWallet is just a fallback that works without any actual Lightning capabilities,
|
||||
@@ -48,16 +53,12 @@ CLIGHTNING_RPC="/home/bob/.lightning/bitcoin/lightning-rpc"
|
||||
LNBITS_ENDPOINT=https://legend.lnbits.com
|
||||
LNBITS_KEY=LNBITS_ADMIN_KEY
|
||||
|
||||
# LndWallet
|
||||
LND_GRPC_ENDPOINT=127.0.0.1
|
||||
LND_GRPC_PORT=11009
|
||||
LND_GRPC_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert"
|
||||
LND_GRPC_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/admin.macaroon or HEXSTRING"
|
||||
|
||||
# LndRestWallet
|
||||
LND_REST_ENDPOINT=https://127.0.0.1:8080/
|
||||
LND_REST_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert"
|
||||
LND_REST_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/admin.macaroon or HEXSTRING"
|
||||
# To use an AES-encrypted macaroon, set
|
||||
# LND_REST_MACAROON_ENCRYPTED="eNcRyPtEdMaCaRoOn"
|
||||
|
||||
# LNPayWallet
|
||||
LNPAY_API_ENDPOINT=https://api.lnpay.co/v1/
|
||||
|
||||
@@ -9,4 +9,5 @@ jobs:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: jpetrucciani/mypy-check@master
|
||||
with:
|
||||
mypy_flags: '--install-types --non-interactive'
|
||||
path: lnbits
|
||||
|
||||
@@ -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" ./
|
||||
@@ -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
@@ -8,8 +8,9 @@ ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
|
||||
# Install build deps
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y --no-install-recommends build-essential
|
||||
RUN apt-get install -y --no-install-recommends build-essential pkg-config
|
||||
RUN python -m pip install --upgrade pip
|
||||
RUN pip install wheel
|
||||
|
||||
# Install runtime deps
|
||||
COPY requirements.txt /tmp/requirements.txt
|
||||
@@ -36,6 +37,9 @@ ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
WORKDIR /app
|
||||
COPY --chown=1000:1000 lnbits /app/lnbits
|
||||
|
||||
ENV LNBITS_PORT="5000"
|
||||
ENV LNBITS_HOST="0.0.0.0"
|
||||
|
||||
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"]
|
||||
|
||||
@@ -30,6 +30,7 @@ sse-starlette = "*"
|
||||
jinja2 = "3.0.1"
|
||||
pyngrok = "*"
|
||||
secp256k1 = "*"
|
||||
pycryptodomex = "*"
|
||||
|
||||
[dev-packages]
|
||||
black = "==20.8b1"
|
||||
|
||||
Generated
+97
-67
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "bd78144379115a1566549f5f2d7dba7a96539fb5893b3999e61bc13c6847827e"
|
||||
"sha256": "3e19364434fd2db3748162ccc1f3b6bddcf7a382473069d15cee6eda5e07eef1"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@@ -146,11 +146,11 @@
|
||||
},
|
||||
"charset-normalizer": {
|
||||
"hashes": [
|
||||
"sha256:2842d8f5e82a1f6aa437380934d5e1cd4fcf2003b06fed6940769c164a480a45",
|
||||
"sha256:98398a9d69ee80548c762ba991a4728bfc3836768ed226b3945908d1a688371c"
|
||||
"sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597",
|
||||
"sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==2.0.11"
|
||||
"version": "==2.0.12"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
@@ -201,11 +201,11 @@
|
||||
},
|
||||
"httpcore": {
|
||||
"hashes": [
|
||||
"sha256:2621ee769d0236574df51b305c5f4c69ca8f0c7b215221ad247b1ee42a9a9de1",
|
||||
"sha256:435ab519628a6e2393f67812dea3ca5c6ad23b457412cd119295d9f906d96a2b"
|
||||
"sha256:47d772f754359e56dd9d892d9593b6f9870a37aeb8ba51e9a88b09b3d68cfade",
|
||||
"sha256:7503ec1c0f559066e7e39bc4003fd2ce023d01cf51793e3c173b864eb456ead1"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==0.14.5"
|
||||
"version": "==0.14.7"
|
||||
},
|
||||
"httptools": {
|
||||
"hashes": [
|
||||
@@ -427,6 +427,39 @@
|
||||
],
|
||||
"version": "==2.21"
|
||||
},
|
||||
"pycryptodomex": {
|
||||
"hashes": [
|
||||
"sha256:1ca8e1b4c62038bb2da55451385246f51f412c5f5eabd64812c01766a5989b4a",
|
||||
"sha256:298c00ea41a81a491d5b244d295d18369e5aac4b61b77b2de5b249ca61cd6659",
|
||||
"sha256:2aa887683eee493e015545bd69d3d21ac8d5ad582674ec98f4af84511e353e45",
|
||||
"sha256:2ce76ed0081fd6ac8c74edc75b9d14eca2064173af79843c24fa62573263c1f2",
|
||||
"sha256:3da13c2535b7aea94cc2a6d1b1b37746814c74b6e80790daddd55ca5c120a489",
|
||||
"sha256:406ec8cfe0c098fadb18d597dc2ee6de4428d640c0ccafa453f3d9b2e58d29e2",
|
||||
"sha256:4d0db8df9ffae36f416897ad184608d9d7a8c2b46c4612c6bc759b26c073f750",
|
||||
"sha256:530756d2faa40af4c1f74123e1d889bd07feae45bac2fd32f259a35f7aa74151",
|
||||
"sha256:77931df40bb5ce5e13f4de2bfc982b2ddc0198971fbd947776c8bb5050896eb2",
|
||||
"sha256:797a36bd1f69df9e2798e33edb4bd04e5a30478efc08f9428c087f17f65a7045",
|
||||
"sha256:8085bd0ad2034352eee4d4f3e2da985c2749cb7344b939f4d95ead38c2520859",
|
||||
"sha256:8536bc08d130cae6dcba1ea689f2913dfd332d06113904d171f2f56da6228e89",
|
||||
"sha256:a4d412eba5679ede84b41dbe48b1bed8f33131ab9db06c238a235334733acc5e",
|
||||
"sha256:aebecde2adc4a6847094d3bd6a8a9538ef3438a5ea84ac1983fcb167db614461",
|
||||
"sha256:b276cc4deb4a80f9dfd47a41ebb464b1fe91efd8b1b8620cf5ccf8b824b850d6",
|
||||
"sha256:b5a185ae79f899b01ca49f365bdf15a45d78d9856f09b0de1a41b92afce1a07f",
|
||||
"sha256:c4d8977ccda886d88dc3ca789de2f1adc714df912ff3934b3d0a3f3d777deafb",
|
||||
"sha256:c5dd3ffa663c982d7f1be9eb494a8924f6d40e2e2f7d1d27384cfab1b2ac0662",
|
||||
"sha256:ca88f2f7020002638276439a01ffbb0355634907d1aa5ca91f3dc0c2e44e8f3b",
|
||||
"sha256:d2cce1c82a7845d7e2e8a0956c6b7ed3f1661c9acf18eb120fc71e098ab5c6fe",
|
||||
"sha256:d709572d64825d8d59ea112e11cc7faf6007f294e9951324b7574af4251e4de8",
|
||||
"sha256:da8db8374295fb532b4b0c467e66800ef17d100e4d5faa2bbbd6df35502da125",
|
||||
"sha256:e36c7e3b5382cd5669cf199c4a04a0279a43b2a3bdd77627e9b89778ac9ec08c",
|
||||
"sha256:e95a4a6c54d27a84a4624d2af8bb9ee178111604653194ca6880c98dcad92f48",
|
||||
"sha256:ee835def05622e0c8b1435a906491760a43d0c462f065ec9143ec4b8d79f8bff",
|
||||
"sha256:f75009715dcf4a3d680c2338ab19dac5498f8121173a929872950f4fb3a48fbf",
|
||||
"sha256:f8524b8bc89470cec7ac51734907818d3620fb1637f8f8b542d650ebec42a126"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.14.1"
|
||||
},
|
||||
"pydantic": {
|
||||
"hashes": [
|
||||
"sha256:085ca1de245782e9b46cefcf99deecc67d418737a1fd3f6a4f511344b613a5b3",
|
||||
@@ -684,22 +717,22 @@
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
"sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e",
|
||||
"sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"
|
||||
"sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42",
|
||||
"sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.0.1"
|
||||
"version": "==4.1.1"
|
||||
},
|
||||
"uvicorn": {
|
||||
"extras": [
|
||||
"standard"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:8b16d9ecb76500f7804184f182835fe8a2b54716d3b0b6bb2da0b2b192f62c73",
|
||||
"sha256:dffbacb8cc25d924d68d231d2c478c4fe6727c36537d8de21e5de591b37afc41"
|
||||
"sha256:25850bbc86195a71a6477b3e4b3b7b4c861fb687fb96912972ce5324472b1011",
|
||||
"sha256:e85872d84fb651cccc4c5d2a71cf7ead055b8fb4d8f1e78e36092282c0cf2aec"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.17.1"
|
||||
"version": "==0.17.4"
|
||||
},
|
||||
"uvloop": {
|
||||
"hashes": [
|
||||
@@ -819,53 +852,50 @@
|
||||
"toml"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:012157499ec4f135fc36cd2177e3d1a1840af9b236cbe80e9a5ccfc83d912a69",
|
||||
"sha256:0a34d313105cdd0d3644c56df2d743fe467270d6ab93b5d4a347eb9fec8924d6",
|
||||
"sha256:11e61c5548ecf74ea1f8b059730b049871f0e32b74f88bd0d670c20c819ad749",
|
||||
"sha256:152cc2624381df4e4e604e21bd8e95eb8059535f7b768c1fb8b8ae0b26f47ab0",
|
||||
"sha256:1b4285fde5286b946835a1a53bba3ad41ef74285ba9e8013e14b5ea93deaeafc",
|
||||
"sha256:27a94db5dc098c25048b0aca155f5fac674f2cf1b1736c5272ba28ead2fc267e",
|
||||
"sha256:27ac7cb84538e278e07569ceaaa6f807a029dc194b1c819a9820b9bb5dbf63ab",
|
||||
"sha256:2a491e159294d756e7fc8462f98175e2d2225e4dbe062cca7d3e0d5a75ba6260",
|
||||
"sha256:2bc85664b06ba42d14bb74d6ddf19d8bfc520cb660561d2d9ce5786ae72f71b5",
|
||||
"sha256:32168001f33025fd756884d56d01adebb34e6c8c0b3395ca8584cdcee9c7c9d2",
|
||||
"sha256:3c4ce3b647bd1792d4394f5690d9df6dc035b00bcdbc5595099c01282a59ae01",
|
||||
"sha256:433b99f7b0613bdcdc0b00cc3d39ed6d756797e3b078d2c43f8a38288520aec6",
|
||||
"sha256:4578728c36de2801c1deb1c6b760d31883e62e33f33c7ba8f982e609dc95167d",
|
||||
"sha256:509c68c3e2015022aeda03b003dd68fa19987cdcf64e9d4edc98db41cfc45d30",
|
||||
"sha256:51372e24b1f7143ee2df6b45cff6a721f3abe93b1e506196f3ffa4155c2497f7",
|
||||
"sha256:5d008e0f67ac800b0ca04d7914b8501312c8c6c00ad8c7ba17754609fae1231a",
|
||||
"sha256:649df3641eb351cdfd0d5533c92fc9df507b6b2bf48a7ef8c71ab63cbc7b5c3c",
|
||||
"sha256:6e78b1e25e5c5695dea012be473e442f7094d066925604be20b30713dbd47f89",
|
||||
"sha256:72d9d186508325a456475dd05b1756f9a204c7086b07fffb227ef8cee03b1dc2",
|
||||
"sha256:7d82c610a2e10372e128023c5baf9ce3d270f3029fe7274ff5bc2897c68f1318",
|
||||
"sha256:7ee317486593193e066fc5e98ac0ce712178c21529a85c07b7cb978171f25d53",
|
||||
"sha256:7eed8459a2b81848cafb3280b39d7d49950d5f98e403677941c752e7e7ee47cb",
|
||||
"sha256:823f9325283dc9565ba0aa2d240471a93ca8999861779b2b6c7aded45b58ee0f",
|
||||
"sha256:85c5fc9029043cf8b07f73fbb0a7ab6d3b717510c3b5642b77058ea55d7cacde",
|
||||
"sha256:86c91c511853dfda81c2cf2360502cb72783f4b7cebabef27869f00cbe1db07d",
|
||||
"sha256:8e0c3525b1a182c8ffc9bca7e56b521e0c2b8b3e82f033c8e16d6d721f1b54d6",
|
||||
"sha256:987a84ff98a309994ca77ed3cc4b92424f824278e48e4bf7d1bb79a63cfe2099",
|
||||
"sha256:9ed3244b415725f08ca3bdf02ed681089fd95e9465099a21c8e2d9c5d6ca2606",
|
||||
"sha256:a189036c50dcd56100746139a459f0d27540fef95b09aba03e786540b8feaa5f",
|
||||
"sha256:a4748349734110fd32d46ff8897b561e6300d8989a494ad5a0a2e4f0ca974fc7",
|
||||
"sha256:a5d79c9af3f410a2b5acad91258b4ae179ee9c83897eb9de69151b179b0227f5",
|
||||
"sha256:a7596aa2f2b8fa5604129cfc9a27ad9beec0a96f18078cb424d029fdd707468d",
|
||||
"sha256:ab4fc4b866b279740e0d917402f0e9a08683e002f43fa408e9655818ed392196",
|
||||
"sha256:bde4aeabc0d1b2e52c4036c54440b1ad05beeca8113f47aceb4998bb7471e2c2",
|
||||
"sha256:c72bb4679283c6737f452eeb9b2a0e570acaef2197ad255fb20162adc80bea76",
|
||||
"sha256:c8582e9280f8d0f38114fe95a92ae8d0790b56b099d728cc4f8a2e14b1c4a18c",
|
||||
"sha256:ca29c352389ea27a24c79acd117abdd8a865c6eb01576b6f0990cd9a4e9c9f48",
|
||||
"sha256:ce443a3e6df90d692c38762f108fc4c88314bf477689f04de76b3f252e7a351c",
|
||||
"sha256:d1675db48490e5fa0b300f6329ecb8a9a37c29b9ab64fa9c964d34111788ca2d",
|
||||
"sha256:da1a428bdbe71f9a8c270c7baab29e9552ac9d0e0cba5e7e9a4c9ee6465d258d",
|
||||
"sha256:e4ff163602c5c77e7bb4ea81ba5d3b793b4419f8acd296aae149370902cf4e92",
|
||||
"sha256:e67ccd53da5958ea1ec833a160b96357f90859c220a00150de011b787c27b98d",
|
||||
"sha256:e8071e7d9ba9f457fc674afc3de054450be2c9b195c470147fbbc082468d8ff7",
|
||||
"sha256:fff16a30fdf57b214778eff86391301c4509e327a65b877862f7c929f10a4253"
|
||||
"sha256:1245ab82e8554fa88c4b2ab1e098ae051faac5af829efdcf2ce6b34dccd5567c",
|
||||
"sha256:1bc6d709939ff262fd1432f03f080c5042dc6508b6e0d3d20e61dd045456a1a0",
|
||||
"sha256:25e73d4c81efa8ea3785274a2f7f3bfbbeccb6fcba2a0bdd3be9223371c37554",
|
||||
"sha256:276b13cc085474e482566c477c25ed66a097b44c6e77132f3304ac0b039f83eb",
|
||||
"sha256:2aed4761809640f02e44e16b8b32c1a5dee5e80ea30a0ff0912158bde9c501f2",
|
||||
"sha256:2dd70a167843b4b4b2630c0c56f1b586fe965b4f8ac5da05b6690344fd065c6b",
|
||||
"sha256:352c68e233409c31048a3725c446a9e48bbff36e39db92774d4f2380d630d8f8",
|
||||
"sha256:3f2b05757c92ad96b33dbf8e8ec8d4ccb9af6ae3c9e9bd141c7cc44d20c6bcba",
|
||||
"sha256:448d7bde7ceb6c69e08474c2ddbc5b4cd13c9e4aa4a717467f716b5fc938a734",
|
||||
"sha256:463e52616ea687fd323888e86bf25e864a3cc6335a043fad6bbb037dbf49bbe2",
|
||||
"sha256:482fb42eea6164894ff82abbcf33d526362de5d1a7ed25af7ecbdddd28fc124f",
|
||||
"sha256:56c4a409381ddd7bbff134e9756077860d4e8a583d310a6f38a2315b9ce301d0",
|
||||
"sha256:56d296cbc8254a7dffdd7bcc2eb70be5a233aae7c01856d2d936f5ac4e8ac1f1",
|
||||
"sha256:5e15d424b8153756b7c903bde6d4610be0c3daca3986173c18dd5c1a1625e4cd",
|
||||
"sha256:618eeba986cea7f621d8607ee378ecc8c2504b98b3fdc4952b30fe3578304687",
|
||||
"sha256:61d47a897c1e91f33f177c21de897267b38fbb45f2cd8e22a710bcef1df09ac1",
|
||||
"sha256:621f6ea7260ea2ffdaec64fe5cb521669984f567b66f62f81445221d4754df4c",
|
||||
"sha256:6a5cdc3adb4f8bb8d8f5e64c2e9e282bc12980ef055ec6da59db562ee9bdfefa",
|
||||
"sha256:6c3f6158b02ac403868eea390930ae64e9a9a2a5bbfafefbb920d29258d9f2f8",
|
||||
"sha256:704f89b87c4f4737da2860695a18c852b78ec7279b24eedacab10b29067d3a38",
|
||||
"sha256:72128176fea72012063200b7b395ed8a57849282b207321124d7ff14e26988e8",
|
||||
"sha256:78fbb2be068a13a5d99dce9e1e7d168db880870f7bc73f876152130575bd6167",
|
||||
"sha256:7bff3a98f63b47464480de1b5bdd80c8fade0ba2832c9381253c9b74c4153c27",
|
||||
"sha256:84f2436d6742c01136dd940ee158bfc7cf5ced3da7e4c949662b8703b5cd8145",
|
||||
"sha256:9976fb0a5709988778ac9bc44f3d50fccd989987876dfd7716dee28beed0a9fa",
|
||||
"sha256:9ad0a117b8dc2061ce9461ea4c1b4799e55edceb236522c5b8f958ce9ed8fa9a",
|
||||
"sha256:9e3dd806f34de38d4c01416344e98eab2437ac450b3ae39c62a0ede2f8b5e4ed",
|
||||
"sha256:9eb494070aa060ceba6e4bbf44c1bc5fa97bfb883a0d9b0c9049415f9e944793",
|
||||
"sha256:9fde6b90889522c220dd56a670102ceef24955d994ff7af2cb786b4ba8fe11e4",
|
||||
"sha256:9fff3ff052922cb99f9e52f63f985d4f7a54f6b94287463bc66b7cdf3eb41217",
|
||||
"sha256:a06c358f4aed05fa1099c39decc8022261bb07dfadc127c08cfbd1391b09689e",
|
||||
"sha256:a4f923b9ab265136e57cc14794a15b9dcea07a9c578609cd5dbbfff28a0d15e6",
|
||||
"sha256:c5b81fb37db76ebea79aa963b76d96ff854e7662921ce742293463635a87a78d",
|
||||
"sha256:d5ed164af5c9078596cfc40b078c3b337911190d3faeac830c3f1274f26b8320",
|
||||
"sha256:d651fde74a4d3122e5562705824507e2f5b2d3d57557f1916c4b27635f8fbe3f",
|
||||
"sha256:de73fca6fb403dd72d4da517cfc49fcf791f74eee697d3219f6be29adf5af6ce",
|
||||
"sha256:e647a0be741edbb529a72644e999acb09f2ad60465f80757da183528941ff975",
|
||||
"sha256:e92c7a5f7d62edff50f60a045dc9542bf939758c95b2fcd686175dd10ce0ed10",
|
||||
"sha256:eeffd96882d8c06d31b65dddcf51db7c612547babc1c4c5db6a011abe9798525",
|
||||
"sha256:f5a4551dfd09c3bd12fca8144d47fe7745275adf3229b7223c2f9e29a975ebda",
|
||||
"sha256:fac0bcc5b7e8169bffa87f0dcc24435446d329cbc2b5486d155c2e0f3b493ae1"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==6.3"
|
||||
"version": "==6.3.1"
|
||||
},
|
||||
"iniconfig": {
|
||||
"hashes": [
|
||||
@@ -948,11 +978,11 @@
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89",
|
||||
"sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"
|
||||
"sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db",
|
||||
"sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==6.2.5"
|
||||
"version": "==7.0.1"
|
||||
},
|
||||
"pytest-cov": {
|
||||
"hashes": [
|
||||
@@ -1051,10 +1081,10 @@
|
||||
},
|
||||
"tomli": {
|
||||
"hashes": [
|
||||
"sha256:b5bde28da1fed24b9bd1d4d2b8cba62300bfb4ec9a6187a957e8ddb9434c5224",
|
||||
"sha256:c292c34f58502a1eb2bbb9f5bbc9a5ebc37bee10ffb8c2d6bbdfa8eb13cc14e1"
|
||||
"sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
|
||||
"sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
|
||||
],
|
||||
"version": "==2.0.0"
|
||||
"version": "==2.0.1"
|
||||
},
|
||||
"typed-ast": {
|
||||
"hashes": [
|
||||
@@ -1088,11 +1118,11 @@
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
"sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e",
|
||||
"sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"
|
||||
"sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42",
|
||||
"sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.0.1"
|
||||
"version": "==4.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
LNbits
|
||||
======
|
||||
|
||||
[![github-tests-badge]][github-tests]
|
||||
[![github-mypy-badge]][github-mypy]
|
||||
[![codecov-badge]][codecov]
|
||||
[![license-badge]](LICENSE)
|
||||
[![docs-badge]][docs]
|
||||
|
||||
@@ -14,6 +11,8 @@ 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!
|
||||
|
||||
LNbits is a very simple Python server that sits on top of any funding source, and can be used as:
|
||||
|
||||
@@ -12,13 +12,18 @@ LNbits uses [Pipenv][pipenv] to manage Python packages.
|
||||
```sh
|
||||
git clone https://github.com/lnbits/lnbits-legend.git
|
||||
cd lnbits-legend/
|
||||
|
||||
sudo apt-get install pipenv
|
||||
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 --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
|
||||
# pip install -U setuptools
|
||||
|
||||
# install libffi/libpq in case "pipenv install" fails
|
||||
# sudo apt-get install -y libffi-dev libpq-dev
|
||||
```
|
||||
## 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.
|
||||
|
||||
**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.
|
||||
|
||||
@@ -28,7 +28,6 @@ Download this repo and install the dependencies:
|
||||
git clone https://github.com/lnbits/lnbits-legend.git
|
||||
cd lnbits-legend/
|
||||
# ensure you have virtualenv installed, on debian/ubuntu 'apt install python3-venv' should work
|
||||
# for now you'll need to `git checkout FastAPI`
|
||||
python3 -m venv venv
|
||||
./venv/bin/pip install -r requirements.txt
|
||||
cp .env.example .env
|
||||
|
||||
+10
-2
@@ -36,16 +36,24 @@ Using this wallet requires the installation of the `grpcio` and `protobuf` Pytho
|
||||
- `LND_GRPC_ENDPOINT`: ip_address
|
||||
- `LND_GRPC_PORT`: port
|
||||
- `LND_GRPC_CERT`: /file/path/tls.cert
|
||||
- `LND_GRPC_MACAROON`: /file/path/admin.macaroon
|
||||
- `LND_GRPC_MACAROON`: /file/path/admin.macaroon or Bech64/Hex
|
||||
|
||||
You can also use an AES-encrypted macaroon (more info) instead by using
|
||||
|
||||
- `LND_GRPC_MACAROON_ENCRYPTED`: eNcRyPtEdMaCaRoOn
|
||||
|
||||
To encrypt your macaroon, run `./venv/bin/python lnbits/wallets/macaroon/macaroon.py`.
|
||||
|
||||
### LND (REST)
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **LndRestWallet**
|
||||
- `LND_REST_ENDPOINT`: ip_address
|
||||
- `LND_REST_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
|
||||
|
||||
|
||||
+9
-8
@@ -84,18 +84,19 @@ def create_app(config_object="lnbits.settings") -> FastAPI:
|
||||
def check_funding_source(app: FastAPI) -> None:
|
||||
@app.on_event("startup")
|
||||
async def check_wallet_status():
|
||||
error_message, balance = await WALLET.status()
|
||||
if error_message:
|
||||
while True:
|
||||
error_message, balance = await WALLET.status()
|
||||
if not error_message:
|
||||
break
|
||||
warnings.warn(
|
||||
f" × The backend for {WALLET.__class__.__name__} isn't working properly: '{error_message}'",
|
||||
RuntimeWarning,
|
||||
)
|
||||
|
||||
sys.exit(4)
|
||||
else:
|
||||
print(
|
||||
f" ✔️ {WALLET.__class__.__name__} seems to be connected and with a balance of {balance} msat."
|
||||
)
|
||||
print("Retrying connection to backend in 5 seconds...")
|
||||
await asyncio.sleep(5)
|
||||
print(
|
||||
f" ✔️ {WALLET.__class__.__name__} seems to be connected and with a balance of {balance} msat."
|
||||
)
|
||||
|
||||
|
||||
def register_routes(app: FastAPI) -> None:
|
||||
|
||||
+16
-2
@@ -6,7 +6,7 @@ from urllib.parse import urlparse
|
||||
|
||||
from lnbits import bolt11
|
||||
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 .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"],
|
||||
extensions=[e[0] for e in extensions],
|
||||
wallets=[Wallet(**w) for w in wallets],
|
||||
admin=user["id"] in [x.strip() for x in LNBITS_ADMIN_USERS]
|
||||
if LNBITS_ADMIN_USERS
|
||||
else False,
|
||||
)
|
||||
|
||||
|
||||
@@ -216,6 +219,8 @@ async def get_payments(
|
||||
incoming: bool = False,
|
||||
since: Optional[int] = None,
|
||||
exclude_uncheckable: bool = False,
|
||||
limit: Optional[int] = None,
|
||||
offset: Optional[int] = None,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> List[Payment]:
|
||||
"""
|
||||
@@ -260,6 +265,15 @@ async def get_payments(
|
||||
clause.append("checking_id NOT LIKE 'temp_%'")
|
||||
clause.append("checking_id NOT LIKE 'internal_%'")
|
||||
|
||||
limit_clause = f"LIMIT {limit}" if type(limit) == int and limit > 0 else ""
|
||||
offset_clause = f"OFFSET {offset}" if type(offset) == int and offset > 0 else ""
|
||||
# combine limit and offset clauses
|
||||
limit_offset_clause = (
|
||||
f"{limit_clause} {offset_clause}"
|
||||
if limit_clause and offset_clause
|
||||
else limit_clause or offset_clause
|
||||
)
|
||||
|
||||
where = ""
|
||||
if clause:
|
||||
where = f"WHERE {' AND '.join(clause)}"
|
||||
@@ -270,10 +284,10 @@ async def get_payments(
|
||||
FROM apipayments
|
||||
{where}
|
||||
ORDER BY time DESC
|
||||
{limit_offset_clause}
|
||||
""",
|
||||
tuple(args),
|
||||
)
|
||||
|
||||
return [Payment.from_row(row) for row in rows]
|
||||
|
||||
|
||||
|
||||
+19
-23
@@ -85,18 +85,17 @@ async def pay_invoice(
|
||||
description: str = "",
|
||||
conn: Optional[Connection] = None,
|
||||
) -> str:
|
||||
invoice = bolt11.decode(payment_request)
|
||||
fee_reserve_msat = fee_reserve(invoice.amount_msat)
|
||||
async with (db.reuse_conn(conn) if conn else db.connect()) as conn:
|
||||
temp_id = f"temp_{urlsafe_short_hash()}"
|
||||
internal_id = f"internal_{urlsafe_short_hash()}"
|
||||
|
||||
invoice = bolt11.decode(payment_request)
|
||||
if invoice.amount_msat == 0:
|
||||
raise ValueError("Amountless invoices not supported.")
|
||||
if max_sat and invoice.amount_msat > max_sat * 1000:
|
||||
raise ValueError("Amount in invoice is too high.")
|
||||
|
||||
wallet = await get_wallet(wallet_id, conn=conn)
|
||||
|
||||
# put all parameters that don't change here
|
||||
PaymentKwargs = TypedDict(
|
||||
"PaymentKwargs",
|
||||
@@ -134,26 +133,20 @@ async def pay_invoice(
|
||||
# the balance is enough in the next step
|
||||
await create_payment(
|
||||
checking_id=temp_id,
|
||||
fee=-fee_reserve(invoice.amount_msat),
|
||||
fee=-fee_reserve_msat,
|
||||
conn=conn,
|
||||
**payment_kwargs,
|
||||
)
|
||||
|
||||
# do the balance check if internal payment
|
||||
if internal_checking_id:
|
||||
wallet = await get_wallet(wallet_id, conn=conn)
|
||||
assert wallet
|
||||
if wallet.balance_msat < 0:
|
||||
raise PermissionError("Insufficient balance.")
|
||||
|
||||
# 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."
|
||||
# do the balance check
|
||||
wallet = await get_wallet(wallet_id, conn=conn)
|
||||
assert wallet
|
||||
if wallet.balance_msat < 0:
|
||||
if not internal_checking_id and wallet.balance_msat > -fee_reserve_msat:
|
||||
raise PaymentFailure(
|
||||
f"You must reserve at least 1% ({round(fee_reserve_msat/1000)} sat) to cover potential routing fees."
|
||||
)
|
||||
raise PermissionError("Insufficient balance.")
|
||||
|
||||
if internal_checking_id:
|
||||
# mark the invoice from the other side as not pending anymore
|
||||
@@ -171,7 +164,9 @@ async def pay_invoice(
|
||||
await internal_invoice_queue.put(internal_checking_id)
|
||||
else:
|
||||
# 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:
|
||||
async with db.connect() as conn:
|
||||
await create_payment(
|
||||
@@ -286,12 +281,12 @@ async def perform_lnurlauth(
|
||||
sign_len = 6 + r_len + s_len
|
||||
|
||||
signature = BytesIO()
|
||||
signature.write(0x30 .to_bytes(1, "big", signed=False))
|
||||
signature.write(0x30.to_bytes(1, "big", signed=False))
|
||||
signature.write((sign_len - 2).to_bytes(1, "big", signed=False))
|
||||
signature.write(0x02 .to_bytes(1, "big", signed=False))
|
||||
signature.write(0x02.to_bytes(1, "big", signed=False))
|
||||
signature.write(r_len.to_bytes(1, "big", signed=False))
|
||||
signature.write(r)
|
||||
signature.write(0x02 .to_bytes(1, "big", signed=False))
|
||||
signature.write(0x02.to_bytes(1, "big", signed=False))
|
||||
signature.write(s_len.to_bytes(1, "big", signed=False))
|
||||
signature.write(s)
|
||||
|
||||
@@ -340,5 +335,6 @@ async def check_invoice_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:
|
||||
return max(1000, int(amount_msat * 0.01))
|
||||
return max(2000, int(amount_msat * 0.01))
|
||||
|
||||
@@ -364,12 +364,12 @@ new Vue({
|
||||
},
|
||||
decodeRequest: function () {
|
||||
this.parse.show = true
|
||||
|
||||
let req = this.parse.data.request.toLowerCase()
|
||||
if (this.parse.data.request.startsWith('lightning:')) {
|
||||
this.parse.data.request = this.parse.data.request.slice(10)
|
||||
} else if (this.parse.data.request.startsWith('lnurl:')) {
|
||||
this.parse.data.request = this.parse.data.request.slice(6)
|
||||
} else if (this.parse.data.request.indexOf('lightning=lnurl1') !== -1) {
|
||||
} else if (req.indexOf('lightning=lnurl1') !== -1) {
|
||||
this.parse.data.request = this.parse.data.request
|
||||
.split('lightning=')[1]
|
||||
.split('&')[0]
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
<code
|
||||
>curl -X POST {{ request.base_url }}api/v1/payments -d '{"out": false,
|
||||
"amount": <int>, "memo": <string>, "webhook":
|
||||
<url:string>}' -H "X-Api-Key: <i>{{ wallet.inkey }}</i>" -H
|
||||
<url:string>, "unit": <string>}' -H "X-Api-Key: <i>{{ wallet.inkey }}</i>" -H
|
||||
"Content-type: application/json"</code
|
||||
>
|
||||
</q-card-section>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
color="primary"
|
||||
@click="processing"
|
||||
type="a"
|
||||
href="{{ url_for('core.lnurlwallet', lightning=lnurl) }}"
|
||||
href="{{ url_for('core.lnurlwallet') }}?lightning={{ lnurl }}"
|
||||
>
|
||||
Press to claim bitcoin
|
||||
</q-btn>
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
</q-btn>
|
||||
</h3>
|
||||
</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">
|
||||
<q-btn
|
||||
unelevated
|
||||
@@ -273,426 +273,470 @@
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</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>
|
||||
{% include "core/_api_docs.html" %}
|
||||
{% if HIDE_API %}
|
||||
<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>
|
||||
|
||||
{% if wallet.lnurlwithdraw_full %}
|
||||
<q-expansion-item group="extras" icon="crop_free" 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}}">
|
||||
<q-list>
|
||||
{% include "core/_api_docs.html" %}
|
||||
<q-separator></q-separator>
|
||||
|
||||
{% if wallet.lnurlwithdraw_full %}
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="crop_free"
|
||||
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
|
||||
value="{{wallet.lnurlwithdraw_full}}"
|
||||
:value="'{{request.base_url}}' +'wallet?usr={{user.id}}&wal={{wallet.id}}'"
|
||||
: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
|
||||
:value="'{{request.base_url}}' +'wallet?usr={{user.id}}&wal={{wallet.id}}'"
|
||||
:options="{width:240}"
|
||||
></qrcode>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-separator></q-separator>
|
||||
<q-expansion-item group="extras" icon="edit" label="Rename wallet">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="" style="max-width: 320px">
|
||||
<q-input
|
||||
filled
|
||||
v-model.trim="newName"
|
||||
label="Label"
|
||||
dense="dense"
|
||||
@update:model-value="(e) => console.log(e)"
|
||||
/>
|
||||
</div>
|
||||
<q-btn
|
||||
:disable="!newName.length"
|
||||
unelevated
|
||||
class="q-mt-sm"
|
||||
color="primary"
|
||||
@click="updateWalletName()"
|
||||
>Update name</q-btn
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-separator></q-separator>
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="remove_circle"
|
||||
label="Delete wallet"
|
||||
>
|
||||
<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>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-separator></q-separator>
|
||||
<q-expansion-item group="extras" icon="edit" label="Rename wallet">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="" style="max-width: 320px">
|
||||
<q-input
|
||||
filled
|
||||
v-model.trim="newName"
|
||||
label="Label"
|
||||
dense="dense"
|
||||
@update:model-value="(e) => console.log(e)"
|
||||
/>
|
||||
</div>
|
||||
<q-btn
|
||||
:disable="!newName.length"
|
||||
unelevated
|
||||
class="q-mt-sm"
|
||||
color="primary"
|
||||
@click="updateWalletName()"
|
||||
>Update name</q-btn
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-separator></q-separator>
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="remove_circle"
|
||||
label="Delete wallet"
|
||||
>
|
||||
<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>
|
||||
{% endif %} {% if AD_SPACE %} {% for ADS in AD_SPACE %} {% set AD =
|
||||
ADS.split(';') %}
|
||||
<q-card>
|
||||
<a href="{{ AD[0] }}"
|
||||
><img width="100%" src="{{ AD[1] }}"
|
||||
/></a> </q-card
|
||||
>{% endfor %} {% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="receive.show" @hide="closeReceiveDialog">
|
||||
{% raw %}
|
||||
<q-card
|
||||
v-if="!receive.paymentReq"
|
||||
class="q-pa-lg q-pt-xl lnbits__dialog-card"
|
||||
>
|
||||
<q-form @submit="createInvoice" class="q-gutter-md">
|
||||
<p v-if="receive.lnurl" class="text-h6 text-center q-my-none">
|
||||
<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>?
|
||||
<q-dialog v-model="receive.show" @hide="closeReceiveDialog">
|
||||
{% raw %}
|
||||
<q-card
|
||||
v-if="!receive.paymentReq"
|
||||
class="q-pa-lg q-pt-xl lnbits__dialog-card"
|
||||
>
|
||||
<q-form @submit="createInvoice" class="q-gutter-md">
|
||||
<p v-if="receive.lnurl" class="text-h6 text-center q-my-none">
|
||||
<b>{{receive.lnurl.domain}}</b> is requesting an invoice:
|
||||
</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>
|
||||
<q-form
|
||||
v-if="!parse.camera.show"
|
||||
@submit="decodeRequest"
|
||||
class="q-gutter-md"
|
||||
>
|
||||
{% endraw %} {% if LNBITS_DENOMINATION != 'sats' %}
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="parse.data.request"
|
||||
type="textarea"
|
||||
label="Paste an invoice, payment request or lnurl code *"
|
||||
>
|
||||
</q-input>
|
||||
<div class="row q-mt-lg">
|
||||
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"
|
||||
: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
|
||||
unelevated
|
||||
color="primary"
|
||||
:disable="parse.data.request == ''"
|
||||
:disable="receive.data.amount == null || receive.data.amount <= 0"
|
||||
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"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</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>
|
||||
<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>
|
||||
<q-form
|
||||
v-if="!parse.camera.show"
|
||||
@submit="decodeRequest"
|
||||
class="q-gutter-md"
|
||||
>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="parse.data.request"
|
||||
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>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="parse.camera.show">
|
||||
<q-card class="q-pa-lg q-pt-xl">
|
||||
<div class="text-center q-mb-lg">
|
||||
<qrcode-stream @decode="decodeQR" class="rounded-borders"></qrcode-stream>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn @click="closeCamera" flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
<q-dialog v-model="parse.camera.show">
|
||||
<q-card class="q-pa-lg q-pt-xl">
|
||||
<div class="text-center q-mb-lg">
|
||||
<qrcode-stream
|
||||
@decode="decodeQR"
|
||||
class="rounded-borders"
|
||||
></qrcode-stream>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn @click="closeCamera" flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="paymentsChart.show">
|
||||
<q-card class="q-pa-sm" style="width: 800px; max-width: unset">
|
||||
<q-card-section>
|
||||
<canvas ref="canvas" width="600" height="400"></canvas>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
<q-dialog v-model="paymentsChart.show">
|
||||
<q-card class="q-pa-sm" style="width: 800px; max-width: unset">
|
||||
<q-card-section>
|
||||
<canvas ref="canvas" width="600" height="400"></canvas>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</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 %}
|
||||
<div ref="disclaimer"></div>
|
||||
<q-dialog v-model="disclaimerDialog.show">
|
||||
<q-card class="q-pa-lg">
|
||||
<h6 class="q-my-md text-deep-purple">Warning</h6>
|
||||
<p>
|
||||
Login functionality to be released in v0.2, for now,
|
||||
<strong
|
||||
>make sure you bookmark this page for future access to your
|
||||
wallet</strong
|
||||
>!
|
||||
</p>
|
||||
<p>
|
||||
This service is in BETA, and we hold no responsibility for people losing
|
||||
access to funds. To encourage you to run your own LNbits installation, any
|
||||
balance on {% raw %}{{ disclaimerDialog.location.host }}{% endraw %} will
|
||||
incur a charge of <strong>{{ service_fee }}% service fee</strong> per
|
||||
week.
|
||||
</p>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
@click="copyText(disclaimerDialog.location.href)"
|
||||
>Copy wallet URL</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||
>I understand</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
{% endif %} {% endblock %}
|
||||
<q-tab icon="photo_camera" label="Scan" @click="showCamera"> </q-tab>
|
||||
</q-tabs>
|
||||
{% if service_fee > 0 %}
|
||||
<div ref="disclaimer"></div>
|
||||
<q-dialog v-model="disclaimerDialog.show">
|
||||
<q-card class="q-pa-lg">
|
||||
<h6 class="q-my-md text-deep-purple">Warning</h6>
|
||||
<p>
|
||||
Login functionality to be released in v0.2, for now,
|
||||
<strong
|
||||
>make sure you bookmark this page for future access to your
|
||||
wallet</strong
|
||||
>!
|
||||
</p>
|
||||
<p>
|
||||
This service is in BETA, and we hold no responsibility for people losing
|
||||
access to funds. To encourage you to run your own LNbits installation,
|
||||
any balance on {% raw %}{{ disclaimerDialog.location.host }}{% endraw %}
|
||||
will incur a charge of
|
||||
<strong>{{ service_fee }}% service fee</strong> per week.
|
||||
</p>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
@click="copyText(disclaimerDialog.location.href)"
|
||||
>Copy wallet URL</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||
>I understand</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
{% endif %} {% endblock %}
|
||||
</div>
|
||||
|
||||
+41
-23
@@ -7,7 +7,7 @@ from typing import Dict, List, Optional, Union
|
||||
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
|
||||
|
||||
import httpx
|
||||
from fastapi import Query, Request
|
||||
from fastapi import Query, Request, Header
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi.param_functions import Depends
|
||||
from fastapi.params import Body
|
||||
@@ -23,9 +23,11 @@ from lnbits.decorators import (
|
||||
WalletInvoiceKeyChecker,
|
||||
WalletTypeInfo,
|
||||
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.settings import LNBITS_ADMIN_USERS, LNBITS_SITE_TITLE
|
||||
from lnbits.utils.exchange_rates import (
|
||||
currencies,
|
||||
fiat_amount_as_satoshis,
|
||||
@@ -34,13 +36,14 @@ from lnbits.utils.exchange_rates import (
|
||||
|
||||
from .. import core_app, db
|
||||
from ..crud import (
|
||||
create_payment,
|
||||
get_payments,
|
||||
get_standalone_payment,
|
||||
save_balance_check,
|
||||
update_wallet,
|
||||
create_payment,
|
||||
get_wallet,
|
||||
get_wallet_for_key,
|
||||
save_balance_check,
|
||||
update_payment_status,
|
||||
update_wallet,
|
||||
)
|
||||
from ..services import (
|
||||
InvoiceFailure,
|
||||
@@ -51,8 +54,6 @@ from ..services import (
|
||||
perform_lnurlauth,
|
||||
)
|
||||
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")
|
||||
@@ -71,7 +72,7 @@ async def api_wallet(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
async def api_update_balance(
|
||||
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(
|
||||
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):
|
||||
out: Optional[bool] = True
|
||||
amount: int = Query(None, ge=1)
|
||||
memo: str = None
|
||||
amount: float = Query(None, ge=0)
|
||||
memo: Optional[str] = None
|
||||
unit: Optional[str] = "sat"
|
||||
description_hash: Optional[str] = None
|
||||
lnurl_callback: Optional[str] = None
|
||||
@@ -140,9 +141,9 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
|
||||
memo = ""
|
||||
else:
|
||||
description_hash = b""
|
||||
memo = data.memo
|
||||
memo = data.memo or LNBITS_SITE_TITLE
|
||||
if data.unit == "sat":
|
||||
amount = data.amount
|
||||
amount = int(data.amount)
|
||||
else:
|
||||
price_in_sats = await fiat_amount_as_satoshis(data.amount, data.unit)
|
||||
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}.",
|
||||
)
|
||||
|
||||
# if invoice.description_hash != data.description_hash:
|
||||
# raise HTTPException(
|
||||
# status_code=HTTPStatus.BAD_REQUEST,
|
||||
# detail=f"{domain} returned an invalid invoice. Expected description_hash == {data.description_hash}, got {invoice.description_hash}.",
|
||||
# )
|
||||
# if invoice.description_hash != data.description_hash:
|
||||
# raise HTTPException(
|
||||
# status_code=HTTPStatus.BAD_REQUEST,
|
||||
# detail=f"{domain} returned an invalid invoice. Expected description_hash == {data.description_hash}, got {invoice.description_hash}.",
|
||||
# )
|
||||
|
||||
extra = {}
|
||||
|
||||
@@ -363,7 +364,13 @@ async def api_payments_sse(
|
||||
|
||||
|
||||
@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)
|
||||
await check_invoice_status(payment.wallet_id, 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."
|
||||
)
|
||||
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}
|
||||
|
||||
try:
|
||||
await payment.check_pending()
|
||||
except Exception:
|
||||
if wallet and wallet.id == payment.wallet_id:
|
||||
return {"paid": False, "details": payment}
|
||||
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}
|
||||
|
||||
|
||||
@@ -500,14 +513,19 @@ async def api_lnurlscan(code: str):
|
||||
return params
|
||||
|
||||
|
||||
class DecodePayment(BaseModel):
|
||||
data: str
|
||||
|
||||
|
||||
@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:
|
||||
if data["data"][:5] == "LNURL":
|
||||
url = lnurl.decode(data["data"])
|
||||
if payment_str[:5] == "LNURL":
|
||||
url = lnurl.decode(payment_str)
|
||||
return {"domain": url}
|
||||
else:
|
||||
invoice = bolt11.decode(data["data"])
|
||||
invoice = bolt11.decode(payment_str)
|
||||
return {
|
||||
"payment_hash": invoice.payment_hash,
|
||||
"amount_msat": invoice.amount_msat,
|
||||
@@ -559,6 +577,6 @@ async def api_fiat_as_sats(data: ConversionData):
|
||||
return output
|
||||
else:
|
||||
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
|
||||
return output
|
||||
|
||||
@@ -15,8 +15,8 @@ from lnbits.core.models import User
|
||||
from lnbits.decorators import check_user_exists
|
||||
from lnbits.helpers import template_renderer, url_for
|
||||
from lnbits.settings import (
|
||||
LNBITS_ALLOWED_USERS,
|
||||
LNBITS_ADMIN_USERS,
|
||||
LNBITS_ALLOWED_USERS,
|
||||
LNBITS_SITE_TITLE,
|
||||
SERVICE_FEE,
|
||||
)
|
||||
@@ -226,7 +226,7 @@ async def lnurl_balance_notify(request: Request, service: str):
|
||||
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 with db.connect() as conn:
|
||||
account = await create_account(conn=conn)
|
||||
|
||||
+9
-3
@@ -130,9 +130,15 @@ class Database(Compat):
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.path = os.path.join(LNBITS_DATA_FOLDER, f"{self.name}.sqlite3")
|
||||
database_uri = f"sqlite:///{self.path}"
|
||||
self.type = SQLITE
|
||||
if os.path.isdir(LNBITS_DATA_FOLDER):
|
||||
self.path = os.path.join(LNBITS_DATA_FOLDER, f"{self.name}.sqlite3")
|
||||
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
|
||||
if self.name.startswith("ext_"):
|
||||
|
||||
+10
-3
@@ -13,7 +13,7 @@ from starlette.requests import Request
|
||||
from lnbits.core.crud import get_user, get_wallet_for_key
|
||||
from lnbits.core.models import User, Wallet
|
||||
from lnbits.requestvars import g
|
||||
from lnbits.settings import LNBITS_ALLOWED_USERS, LNBITS_ADMIN_USERS
|
||||
from lnbits.settings import LNBITS_ALLOWED_USERS, LNBITS_ADMIN_USERS, LNBITS_ADMIN_EXTENSIONS
|
||||
|
||||
|
||||
class KeyChecker(SecurityBase):
|
||||
@@ -122,6 +122,7 @@ async def get_key_type(
|
||||
# 0: admin
|
||||
# 1: invoice
|
||||
# 2: invalid
|
||||
pathname = r['path'].split('/')[1]
|
||||
|
||||
if not api_key_header and not api_key_query:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -131,7 +132,10 @@ async def get_key_type(
|
||||
try:
|
||||
checker = WalletAdminKeyChecker(api_key=token)
|
||||
await checker.__call__(r)
|
||||
return WalletTypeInfo(0, checker.wallet)
|
||||
wallet = WalletTypeInfo(0, checker.wallet)
|
||||
if (LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS) and (LNBITS_ADMIN_EXTENSIONS and pathname in LNBITS_ADMIN_EXTENSIONS):
|
||||
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized.")
|
||||
return wallet
|
||||
except HTTPException as e:
|
||||
if e.status_code == HTTPStatus.BAD_REQUEST:
|
||||
raise
|
||||
@@ -143,7 +147,10 @@ async def get_key_type(
|
||||
try:
|
||||
checker = WalletInvoiceKeyChecker(api_key=token)
|
||||
await checker.__call__(r)
|
||||
return WalletTypeInfo(1, checker.wallet)
|
||||
wallet = WalletTypeInfo(1, checker.wallet)
|
||||
if (LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS) and (LNBITS_ADMIN_EXTENSIONS and pathname in LNBITS_ADMIN_EXTENSIONS):
|
||||
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized.")
|
||||
return wallet
|
||||
except HTTPException as e:
|
||||
if e.status_code == HTTPStatus.BAD_REQUEST:
|
||||
raise
|
||||
|
||||
@@ -88,7 +88,7 @@ async def get_copilot(copilot_id: str) -> Copilots:
|
||||
|
||||
async def get_copilots(user: str) -> List[Copilots]:
|
||||
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]
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ async def delete_ticket(payment_hash: 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
|
||||
|
||||
@@ -12,7 +12,7 @@ async def create_jukebox(
|
||||
juke_id = urlsafe_short_hash()
|
||||
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
@@ -41,6 +41,7 @@ async def update_jukebox(
|
||||
q = ", ".join([f"{field[0]} = ?" for field in data])
|
||||
items = [f"{field[1]}" for field in data]
|
||||
items.append(juke_id)
|
||||
q = q.replace("user", '"user"', 1) # hack to make user be "user"!
|
||||
await db.execute(f"UPDATE jukebox.jukebox SET {q} WHERE id = ?", (items))
|
||||
row = await db.fetchone("SELECT * FROM jukebox.jukebox WHERE id = ?", (juke_id,))
|
||||
return Jukebox(**row) if row else None
|
||||
@@ -57,11 +58,11 @@ async def get_jukebox_by_user(user: str) -> Optional[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:
|
||||
if row.sp_playlists == None:
|
||||
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]
|
||||
|
||||
|
||||
@@ -75,7 +75,6 @@ async def api_check_credentials_check(
|
||||
juke_id: str = Query(None), wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
):
|
||||
jukebox = await get_jukebox(juke_id)
|
||||
|
||||
return jukebox
|
||||
|
||||
|
||||
@@ -442,7 +441,7 @@ async def api_get_jukebox_currently(
|
||||
token = await api_get_token(juke_id)
|
||||
if token == False:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN, detail="INvoice not paid"
|
||||
status_code=HTTPStatus.FORBIDDEN, detail="Invoice not paid"
|
||||
)
|
||||
elif retry:
|
||||
raise HTTPException(
|
||||
@@ -456,5 +455,5 @@ async def api_get_jukebox_currently(
|
||||
)
|
||||
except:
|
||||
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"
|
||||
)
|
||||
|
||||
@@ -30,6 +30,6 @@ from .views import * # noqa
|
||||
from .views_api import * # noqa
|
||||
|
||||
|
||||
def lnticket_start():
|
||||
def livestream_start():
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from http import HTTPStatus
|
||||
from mmap import MAP_DENYWRITE
|
||||
# from mmap import MAP_DENYWRITE
|
||||
|
||||
from fastapi.param_functions import Depends
|
||||
from fastapi.params import Query
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import time
|
||||
import asyncio
|
||||
|
||||
from base64 import urlsafe_b64encode
|
||||
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.services import create_invoice, pay_invoice
|
||||
from lnbits.decorators import WalletTypeInfo
|
||||
from lnbits.settings import WALLET
|
||||
from lnbits.settings import WALLET, LNBITS_SITE_TITLE
|
||||
|
||||
from . import lndhub_ext
|
||||
from .decorators import check_wallet, require_admin_key
|
||||
@@ -55,14 +57,13 @@ async def lndhub_addinvoice(
|
||||
_, pr = await create_invoice(
|
||||
wallet_id=wallet.wallet.id,
|
||||
amount=int(data.amt),
|
||||
memo=data.memo or "received sats",
|
||||
memo=data.memo or LNBITS_SITE_TITLE,
|
||||
extra={"tag": "lndhub"},
|
||||
)
|
||||
except:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Failed to create invoice"
|
||||
)
|
||||
|
||||
invoice = bolt11.decode(pr)
|
||||
return {
|
||||
"pay_req": pr,
|
||||
@@ -116,7 +117,9 @@ async def lndhub_balance(
|
||||
|
||||
@lndhub_ext.get("/ext/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(
|
||||
wallet_id=wallet.wallet.id,
|
||||
@@ -124,11 +127,15 @@ async def lndhub_gettxs(
|
||||
pending=True,
|
||||
outgoing=True,
|
||||
incoming=False,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
exclude_uncheckable=True,
|
||||
):
|
||||
await payment.set_pending(
|
||||
(await WALLET.get_payment_status(payment.checking_id)).pending
|
||||
)
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
return [
|
||||
{
|
||||
"payment_preimage": payment.preimage,
|
||||
@@ -148,28 +155,34 @@ async def lndhub_gettxs(
|
||||
complete=True,
|
||||
outgoing=True,
|
||||
incoming=False,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
)[:limit]
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@lndhub_ext.get("/ext/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(
|
||||
wallet_id=wallet.wallet.id,
|
||||
complete=False,
|
||||
pending=True,
|
||||
outgoing=False,
|
||||
incoming=True,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
exclude_uncheckable=True,
|
||||
):
|
||||
await invoice.set_pending(
|
||||
(await WALLET.get_invoice_status(invoice.checking_id)).pending
|
||||
)
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -192,8 +205,10 @@ async def lndhub_getuserinvoices(
|
||||
complete=True,
|
||||
incoming=True,
|
||||
outgoing=False,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
)[:limit]
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@@ -117,9 +117,10 @@
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<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">
|
||||
{{ col.label }}
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
@@ -136,9 +137,19 @@
|
||||
:href="'mailto:' + props.row.email"
|
||||
></q-btn>
|
||||
</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">
|
||||
{{ col.value }}
|
||||
{{ col.label == "Ticket" ? col.value.length > 20 ? `${col.value.substring(0, 20)}...` : col.value : col.value }}
|
||||
</q-td>
|
||||
|
||||
<q-td auto-width>
|
||||
@@ -249,6 +260,29 @@
|
||||
</q-form>
|
||||
</q-card>
|
||||
</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>
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script>
|
||||
@@ -318,6 +352,10 @@
|
||||
formDialog: {
|
||||
show: 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 () {
|
||||
LNbits.utils.exportCSV(this.ticketsTable.columns, this.tickets)
|
||||
},
|
||||
@@ -421,12 +469,13 @@
|
||||
},
|
||||
updateformDialog: function (formId) {
|
||||
var link = _.findWhere(this.forms, {id: formId})
|
||||
console.log("LINK", link)
|
||||
|
||||
this.formDialog.data.id = link.id
|
||||
this.formDialog.data.wallet = link.wallet
|
||||
this.formDialog.data.name = link.name
|
||||
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.show = true
|
||||
},
|
||||
|
||||
@@ -103,6 +103,10 @@ async def api_ticket_make_ticket(data: CreateTicketData, form_id):
|
||||
raise HTTPException(
|
||||
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))
|
||||
|
||||
|
||||
@@ -76,6 +76,11 @@ async def api_link_create_or_update(
|
||||
link_id=None,
|
||||
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:
|
||||
raise HTTPException(
|
||||
detail="Min is greater than max.", status_code=HTTPStatus.BAD_REQUEST
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import asyncio
|
||||
import json
|
||||
from http import HTTPStatus
|
||||
|
||||
import httpx
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
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.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():
|
||||
@@ -25,16 +27,16 @@ async def on_invoice_paid(payment: Payment) -> None:
|
||||
try:
|
||||
# Check its got a payout associated with it
|
||||
lnurlpayout_link = await get_lnurlpayout_from_wallet(payment.wallet_id)
|
||||
print("LNURLpayout", lnurlpayout_link)
|
||||
if lnurlpayout_link:
|
||||
|
||||
# Check the wallet balance is more than the threshold
|
||||
|
||||
wallet = await get_wallet(lnurlpayout_link.wallet)
|
||||
if wallet.balance < lnurlpayout_link.threshold + (
|
||||
lnurlpayout_link.threshold * 0.02
|
||||
):
|
||||
return
|
||||
threshold = lnurlpayout_link.threshold + (lnurlpayout_link.threshold * 0.02)
|
||||
|
||||
if wallet.balance < threshold:
|
||||
return
|
||||
# Get the invoice from the LNURL to pay
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
@@ -43,6 +45,7 @@ async def on_invoice_paid(payment: Payment) -> None:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN, detail="LNURL broken"
|
||||
)
|
||||
|
||||
try:
|
||||
r = await client.get(str(url["domain"]), timeout=40)
|
||||
res = r.json()
|
||||
@@ -56,6 +59,12 @@ async def on_invoice_paid(payment: Payment) -> None:
|
||||
timeout=40,
|
||||
)
|
||||
res = r.json()
|
||||
|
||||
if hasattr(res, "status") and res["status"] == "ERROR":
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN,
|
||||
detail=res["reason"],
|
||||
)
|
||||
try:
|
||||
await pay_invoice(
|
||||
wallet_id=payment.wallet_id,
|
||||
@@ -65,7 +74,9 @@ async def on_invoice_paid(payment: Payment) -> None:
|
||||
return
|
||||
except:
|
||||
pass
|
||||
except:
|
||||
|
||||
except Exception as e:
|
||||
print("ERROR", str(e))
|
||||
return
|
||||
except (httpx.ConnectError, httpx.RequestError):
|
||||
return
|
||||
|
||||
@@ -4,7 +4,8 @@ from fastapi import Query
|
||||
from fastapi.params import Depends
|
||||
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.views.api import api_payment, api_payments_decode
|
||||
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
||||
@@ -14,10 +15,10 @@ from .crud import (
|
||||
create_lnurlpayout,
|
||||
delete_lnurlpayout,
|
||||
get_lnurlpayout,
|
||||
get_lnurlpayouts,
|
||||
get_lnurlpayout_from_wallet,
|
||||
get_lnurlpayouts,
|
||||
)
|
||||
from .models import lnurlpayout, CreateLnurlPayoutData
|
||||
from .models import CreateLnurlPayoutData, lnurlpayout
|
||||
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 = await get_lnurlpayout(lnurlpayout_id)
|
||||
payments = await get_payments(
|
||||
wallet_id=lnurlpayout.wallet,
|
||||
complete=True,
|
||||
## THIS
|
||||
mock_payment = Payment(
|
||||
checking_id="mock",
|
||||
pending=False,
|
||||
outgoing=True,
|
||||
incoming=True,
|
||||
amount=1,
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -77,8 +77,8 @@
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
userAmount: {{ paywall.amount }},
|
||||
paywallAmount: {{ paywall.amount }},
|
||||
userAmount: '{{ paywall.amount }}',
|
||||
paywallAmount: '{{ paywall.amount }}',
|
||||
paymentReq: null,
|
||||
redirectUrl: null,
|
||||
paymentDialog: {
|
||||
@@ -89,7 +89,9 @@
|
||||
},
|
||||
computed: {
|
||||
amount: function () {
|
||||
return (this.paywallAmount > this.userAmount) ? this.paywallAmount : this.userAmount
|
||||
return this.paywallAmount > this.userAmount
|
||||
? this.paywallAmount
|
||||
: this.userAmount
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -102,48 +104,55 @@
|
||||
},
|
||||
createInvoice: function () {
|
||||
var self = this
|
||||
console.log(this.amount)
|
||||
axios
|
||||
.post(
|
||||
'/paywall/api/v1/paywalls/{{ paywall.id }}/invoice',
|
||||
{amount: self.amount}
|
||||
LNbits.api
|
||||
.request(
|
||||
'POST',
|
||||
'/paywall/api/v1/paywalls/invoice/{{ paywall.id }}',
|
||||
'filler',
|
||||
{
|
||||
amount: self.amount
|
||||
}
|
||||
)
|
||||
.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({
|
||||
timeout: 0,
|
||||
message: 'Waiting for payment...'
|
||||
})
|
||||
|
||||
self.paymentDialog.checker = setInterval(function () {
|
||||
axios
|
||||
.post(
|
||||
'/paywall/api/v1/paywalls/{{ paywall.id }}/check_invoice',
|
||||
{payment_hash: response.data.payment_hash}
|
||||
)
|
||||
.then(function (res) {
|
||||
if (res.data.paid) {
|
||||
self.cancelPayment()
|
||||
self.redirectUrl = res.data.url
|
||||
if (res.data.remembers) {
|
||||
self.$q.localStorage.set(
|
||||
'lnbits.paywall.{{ paywall.id }}',
|
||||
res.data.url
|
||||
)
|
||||
self.paymentDialog.checker = setInterval(function () {
|
||||
LNbits.api
|
||||
.request(
|
||||
'POST',
|
||||
'/paywall/api/v1/paywalls/check_invoice/{{ paywall.id }}',
|
||||
'filler',
|
||||
{payment_hash: response.data.payment_hash}
|
||||
)
|
||||
.then(function (response) {
|
||||
if (response.data) {
|
||||
if (response.data.paid) {
|
||||
self.cancelPayment()
|
||||
self.redirectUrl = response.data.url
|
||||
if (response.data.remembers) {
|
||||
self.$q.localStorage.set(
|
||||
'lnbits.paywall.{{ paywall.id }}',
|
||||
response.data.url
|
||||
)
|
||||
}
|
||||
self.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Payment received!',
|
||||
icon: null
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
self.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Payment received!',
|
||||
icon: null
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
}, 2000)
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
|
||||
@@ -52,20 +52,17 @@ async def api_paywall_delete(
|
||||
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(
|
||||
paywall_id,
|
||||
data: CreatePaywallInvoice,
|
||||
wallet: WalletTypeInfo = Depends(get_key_type),
|
||||
paywall_id: str = Query(None)
|
||||
):
|
||||
paywall = await get_paywall(paywall_id)
|
||||
|
||||
if data.amount < paywall.amount:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=f"Minimum amount is {paywall.amount} sat.",
|
||||
)
|
||||
|
||||
try:
|
||||
amount = data.amount if data.amount > paywall.amount else paywall.amount
|
||||
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}
|
||||
|
||||
|
||||
@paywall_ext.post("/api/v1/paywalls/{paywall_id}/check_invoice")
|
||||
async def api_paywal_check_invoice(data: CheckPaywallInvoice, paywall_id):
|
||||
@paywall_ext.post("/api/v1/paywalls/check_invoice/{paywall_id}")
|
||||
async def api_paywal_check_invoice(data: CheckPaywallInvoice, paywall_id: str = Query(None)):
|
||||
paywall = await get_paywall(paywall_id)
|
||||
payment_hash = data.payment_hash
|
||||
if not paywall:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Paywall does not exist."
|
||||
)
|
||||
|
||||
try:
|
||||
status = await check_invoice_status(paywall.wallet, payment_hash)
|
||||
is_paid = not status.pending
|
||||
@@ -101,5 +97,4 @@ async def api_paywal_check_invoice(data: CheckPaywallInvoice, paywall_id):
|
||||
await payment.set_pending(False)
|
||||
|
||||
return {"paid": True, "url": paywall.url, "remembers": paywall.remembers}
|
||||
|
||||
return {"paid": False}
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
label="lightning⚡"
|
||||
>
|
||||
<q-tooltip>
|
||||
bitcoin onchain payment method not available
|
||||
bitcoin lightning payment method not available
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
@@ -86,7 +86,7 @@
|
||||
label="onchain⛓️"
|
||||
>
|
||||
<q-tooltip>
|
||||
bitcoin lightning payment method not available
|
||||
bitcoin onchain payment method not available
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
@@ -243,7 +243,7 @@
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
startPaymentNotifier(){
|
||||
startPaymentNotifier() {
|
||||
this.cancelListener()
|
||||
|
||||
this.cancelListener = LNbits.event.onInvoicePaid(
|
||||
|
||||
@@ -60,7 +60,7 @@ class Service(BaseModel):
|
||||
onchain: Optional[str]
|
||||
servicename: str # Currently, this will just always be "Streamlabs"
|
||||
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
|
||||
def from_row(cls, row: Row) -> "Service":
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
donationDialog: {
|
||||
show: false,
|
||||
data: {
|
||||
name: '',
|
||||
name: null,
|
||||
sats: '',
|
||||
message: ''
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ from starlette.responses import RedirectResponse
|
||||
|
||||
from lnbits.core.crud import get_user
|
||||
from lnbits.decorators import WalletTypeInfo, get_key_type
|
||||
from lnbits.extensions.satspay.models import CreateCharge
|
||||
from lnbits.extensions.streamalerts.models import (
|
||||
CreateDonation,
|
||||
CreateService,
|
||||
@@ -113,17 +114,18 @@ async def api_create_donation(data: CreateDonation, request: Request):
|
||||
service_id = data.service
|
||||
service = await get_service(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}"
|
||||
charge = await create_charge(
|
||||
create_charge_data = CreateCharge(
|
||||
amount=sats,
|
||||
completelink=f"https://twitch.tv/{service.twitchuser}",
|
||||
completelinktext="Back to Stream!",
|
||||
webhook=webhook_base + "/streamalerts/api/v1/postdonation",
|
||||
description=description,
|
||||
**charge_details,
|
||||
**charge_details
|
||||
)
|
||||
charge = await create_charge(user=charge_details["user"], data=create_charge_data)
|
||||
await create_donation(
|
||||
id=charge.id,
|
||||
wallet=service.wallet,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from sqlite3 import Row
|
||||
from typing import NamedTuple, Optional
|
||||
from typing import Optional
|
||||
|
||||
from fastapi.param_functions import Query
|
||||
from pydantic import BaseModel
|
||||
@@ -26,7 +26,7 @@ class createTip(BaseModel):
|
||||
message: str = ""
|
||||
|
||||
|
||||
class Tip(NamedTuple):
|
||||
class Tip(BaseModel):
|
||||
"""A Tip represents a single donation"""
|
||||
|
||||
id: str # This ID always corresponds to a satspay charge ID
|
||||
@@ -55,7 +55,7 @@ class createTips(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
class TipJar(NamedTuple):
|
||||
class TipJar(BaseModel):
|
||||
"""A TipJar represents a user's tip jar"""
|
||||
|
||||
id: int
|
||||
|
||||
@@ -222,6 +222,7 @@
|
||||
'INR',
|
||||
'IQD',
|
||||
'IRR',
|
||||
'IRT',
|
||||
'ISK',
|
||||
'JEP',
|
||||
'JMD',
|
||||
|
||||
@@ -112,7 +112,7 @@ async def api_get_addresses(wallet_id, w: WalletTypeInfo = Depends(get_key_type)
|
||||
async def api_update_mempool(
|
||||
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()
|
||||
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<code>[<withdraw_link_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<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 }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
@@ -49,7 +49,7 @@
|
||||
<code>{"lnurl": <string>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.base_url }}api/v1/links/<withdraw_id> -H
|
||||
>curl -X GET {{ request.base_url }}withdraw/api/v1/links/<withdraw_id> -H
|
||||
"X-Api-Key: {{ user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
@@ -78,7 +78,7 @@
|
||||
<code>{"lnurl": <string>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X POST {{ request.base_url }}api/v1/links -d '{"title":
|
||||
>curl -X POST {{ request.base_url }}withdraw/api/v1/links -d '{"title":
|
||||
<string>, "min_withdrawable": <integer>,
|
||||
"max_withdrawable": <integer>, "uses": <integer>,
|
||||
"wait_time": <integer>, "is_unique": <boolean>}' -H
|
||||
@@ -114,7 +114,7 @@
|
||||
<code>{"lnurl": <string>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X PUT {{ request.base_url }}api/v1/links/<withdraw_id> -d
|
||||
>curl -X PUT {{ request.base_url }}withdraw/api/v1/links/<withdraw_id> -d
|
||||
'{"title": <string>, "min_withdrawable": <integer>,
|
||||
"max_withdrawable": <integer>, "uses": <integer>,
|
||||
"wait_time": <integer>, "is_unique": <boolean>}' -H
|
||||
@@ -142,7 +142,7 @@
|
||||
<code></code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X DELETE {{ request.base_url }}api/v1/links/<withdraw_id>
|
||||
>curl -X DELETE {{ request.base_url }}withdraw/api/v1/links/<withdraw_id>
|
||||
-H "X-Api-Key: {{ user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
@@ -170,7 +170,7 @@
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.base_url
|
||||
}}api/v1/links/<the_hash>/<lnurl_id> -H "X-Api-Key: {{
|
||||
}}withdraw/api/v1/links/<the_hash>/<lnurl_id> -H "X-Api-Key: {{
|
||||
user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
@@ -191,7 +191,7 @@
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.base_url }}/withdraw/img/<lnurl_id>"
|
||||
>curl -X GET {{ request.base_url }}withdraw/img/<lnurl_id>"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
@@ -71,6 +71,11 @@ async def api_link_create_or_update(
|
||||
link_id: str = None,
|
||||
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:
|
||||
raise HTTPException(
|
||||
detail="`max_withdrawable` needs to be at least `min_withdrawable`.",
|
||||
|
||||
@@ -15,6 +15,7 @@ import lnbits.settings as settings
|
||||
class Extension(NamedTuple):
|
||||
code: str
|
||||
is_valid: bool
|
||||
is_admin_only: bool
|
||||
name: Optional[str] = None
|
||||
short_description: Optional[str] = None
|
||||
icon: Optional[str] = None
|
||||
@@ -25,6 +26,7 @@ class Extension(NamedTuple):
|
||||
class ExtensionManager:
|
||||
def __init__(self):
|
||||
self._disabled: List[str] = settings.LNBITS_DISABLED_EXTENSIONS
|
||||
self._admin_only: List[str] = [x.strip(' ') for x in settings.LNBITS_ADMIN_EXTENSIONS]
|
||||
self._extension_folders: List[str] = [
|
||||
x[1] for x in os.walk(os.path.join(settings.LNBITS_PATH, "extensions"))
|
||||
][0]
|
||||
@@ -47,14 +49,17 @@ class ExtensionManager:
|
||||
) as json_file:
|
||||
config = json.load(json_file)
|
||||
is_valid = True
|
||||
is_admin_only = True if extension in self._admin_only else False
|
||||
except Exception:
|
||||
config = {}
|
||||
is_valid = False
|
||||
is_admin_only = False
|
||||
|
||||
output.append(
|
||||
Extension(
|
||||
extension,
|
||||
is_valid,
|
||||
is_admin_only,
|
||||
config.get("name"),
|
||||
config.get("short_description"),
|
||||
config.get("icon"),
|
||||
@@ -155,6 +160,9 @@ def template_renderer(additional_folders: List = []) -> Jinja2Templates:
|
||||
["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["LNBITS_DENOMINATION"] = settings.LNBITS_DENOMINATION
|
||||
t.env.globals["SITE_TAGLINE"] = settings.LNBITS_SITE_TAGLINE
|
||||
|
||||
@@ -29,10 +29,13 @@ LNBITS_ALLOWED_USERS: List[str] = env.list(
|
||||
"LNBITS_ALLOWED_USERS", default=[], subcast=str
|
||||
)
|
||||
LNBITS_ADMIN_USERS: List[str] = env.list("LNBITS_ADMIN_USERS", default=[], subcast=str)
|
||||
LNBITS_ADMIN_EXTENSIONS: List[str] = env.list("LNBITS_ADMIN_EXTENSIONS", default=[], subcast=str)
|
||||
LNBITS_DISABLED_EXTENSIONS: List[str] = env.list(
|
||||
"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_DENOMINATION = env.str("LNBITS_DENOMINATION", default="sats")
|
||||
LNBITS_SITE_TAGLINE = env.str(
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 7.7 KiB |
+37
-11
@@ -111,7 +111,7 @@ window.LNbits = {
|
||||
'/wallet?' + (userId ? 'usr=' + userId + '&' : '') + 'nme=' + walletName
|
||||
},
|
||||
updateWallet: function (walletName, userId, walletId) {
|
||||
window.location.href = `/wallet?usr=${userId}&wal=${walletId}&nme=${walletName}`
|
||||
window.location.href = `/wallet?usr=${userId}&wal=${walletId}&nme=${walletName}`
|
||||
},
|
||||
deleteWallet: function (walletId, userId) {
|
||||
window.location.href = '/deletewallet?usr=' + userId + '&wal=' + walletId
|
||||
@@ -123,6 +123,7 @@ window.LNbits = {
|
||||
[
|
||||
'code',
|
||||
'isValid',
|
||||
'isAdminOnly',
|
||||
'name',
|
||||
'shortDescription',
|
||||
'icon',
|
||||
@@ -135,7 +136,12 @@ window.LNbits = {
|
||||
return obj
|
||||
},
|
||||
user: function (data) {
|
||||
var obj = {id: data.id, email: data.email, extensions: data.extensions, wallets: data.wallets}
|
||||
var obj = {
|
||||
id: data.id,
|
||||
email: data.email,
|
||||
extensions: data.extensions,
|
||||
wallets: data.wallets
|
||||
}
|
||||
var mapWallet = this.wallet
|
||||
obj.wallets = obj.wallets
|
||||
.map(function (obj) {
|
||||
@@ -153,16 +159,23 @@ window.LNbits = {
|
||||
return obj
|
||||
},
|
||||
wallet: function (data) {
|
||||
newWallet = {id: data.id, name: data.name, adminkey: data.adminkey, inkey: data.inkey}
|
||||
newWallet = {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
adminkey: data.adminkey,
|
||||
inkey: data.inkey
|
||||
}
|
||||
newWallet.msat = data.balance_msat
|
||||
newWallet.sat = Math.round(data.balance_msat / 1000)
|
||||
newWallet.fsat = new Intl.NumberFormat(window.LOCALE).format(newWallet.sat)
|
||||
newWallet.fsat = new Intl.NumberFormat(window.LOCALE).format(
|
||||
newWallet.sat
|
||||
)
|
||||
newWallet.url = ['/wallet?usr=', data.user, '&wal=', data.id].join('')
|
||||
return newWallet
|
||||
},
|
||||
payment: function (data) {
|
||||
obj = {
|
||||
checking_id:data.id,
|
||||
checking_id: data.id,
|
||||
pending: data.pending,
|
||||
amount: data.amount,
|
||||
fee: data.fee,
|
||||
@@ -174,8 +187,8 @@ window.LNbits = {
|
||||
extra: data.extra,
|
||||
wallet_id: data.wallet_id,
|
||||
webhook: data.webhook,
|
||||
webhook_status: data.webhook_status,
|
||||
}
|
||||
webhook_status: data.webhook_status
|
||||
}
|
||||
|
||||
obj.date = Quasar.utils.date.formatDate(
|
||||
new Date(obj.time * 1000),
|
||||
@@ -225,7 +238,8 @@ window.LNbits = {
|
||||
Quasar.plugins.Notify.create({
|
||||
timeout: 5000,
|
||||
type: types[error.response.status] || 'warning',
|
||||
message: error.response.data.message || error.response.data.detail || null,
|
||||
message:
|
||||
error.response.data.message || error.response.data.detail || null,
|
||||
caption:
|
||||
[error.response.status, ' ', error.response.statusText]
|
||||
.join('')
|
||||
@@ -331,17 +345,25 @@ window.windowMixin = {
|
||||
}
|
||||
},
|
||||
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
|
||||
if (!this.$q.localStorage.getItem('lnbits.theme')){
|
||||
this.changeColor(this.g.allowedThemes[0])
|
||||
}
|
||||
if (
|
||||
this.$q.localStorage.getItem('lnbits.theme') &&
|
||||
!this.g.allowedThemes.includes(
|
||||
this.$q.localStorage.getItem('lnbits.theme')
|
||||
)
|
||||
) {
|
||||
console.log('allowedThemes changed by Admin', this.g.allowedThemes[0])
|
||||
this.changeColor(this.g.allowedThemes[0])
|
||||
}
|
||||
|
||||
@@ -368,6 +390,10 @@ window.windowMixin = {
|
||||
.filter(function (obj) {
|
||||
return !obj.hidden
|
||||
})
|
||||
.filter(function (obj) {
|
||||
if (window.user.admin) return obj
|
||||
return !obj.isAdminOnly
|
||||
})
|
||||
.map(function (obj) {
|
||||
if (user) {
|
||||
obj.isEnabled = user.extensions.indexOf(obj.code) !== -1
|
||||
|
||||
@@ -7,6 +7,14 @@ $themes: (
|
||||
marginal-bg: #1f2234,
|
||||
marginal-text: #fff
|
||||
),
|
||||
'bitcoin': (
|
||||
primary: #ff9853,
|
||||
secondary: #ff7353,
|
||||
dark: #2d293b,
|
||||
info: #333646,
|
||||
marginal-bg: #2d293b,
|
||||
marginal-text: #fff
|
||||
),
|
||||
'mint': (
|
||||
primary: #3ab77d,
|
||||
secondary: #27b065,
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
{% block head_scripts %}{% endblock %}
|
||||
</head>
|
||||
|
||||
<body data-theme="classic">
|
||||
<body data-theme="bitcoin">
|
||||
<q-layout id="vue" view="hHh lpR lfr" v-cloak>
|
||||
<q-header bordered class="bg-marginal-bg">
|
||||
<q-toolbar>
|
||||
@@ -67,8 +67,19 @@
|
||||
icon="format_color_fill"
|
||||
color="deep-purple"
|
||||
size="md"
|
||||
><q-tooltip>classic</q-tooltip> </q-btn
|
||||
><q-btn
|
||||
><q-tooltip>classic</q-tooltip>
|
||||
</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')"
|
||||
dense
|
||||
flat
|
||||
@@ -151,11 +162,12 @@
|
||||
</q-page>
|
||||
</q-page-container>
|
||||
{% endblock %} {% block footer %}
|
||||
|
||||
<q-footer
|
||||
class="bg-transparent q-px-lg q-py-md"
|
||||
:class="{'text-dark': !$q.dark.isActive}"
|
||||
>
|
||||
<q-toolbar>
|
||||
<q-toolbar class="gt-sm">
|
||||
<q-toolbar-title class="text-caption">
|
||||
{{ SITE_TITLE }}, {{SITE_TAGLINE}}
|
||||
<br />
|
||||
@@ -179,6 +191,7 @@
|
||||
</q-btn>
|
||||
</q-toolbar>
|
||||
</q-footer>
|
||||
|
||||
{% endblock %}
|
||||
</q-layout>
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ currencies = {
|
||||
"IMP": "Isle of Man Pound",
|
||||
"INR": "Indian Rupee",
|
||||
"IQD": "Iraqi Dinar",
|
||||
"IRT": "Iranian Toman",
|
||||
"ISK": "Icelandic Króna",
|
||||
"JEP": "Jersey Pound",
|
||||
"JMD": "Jamaican Dollar",
|
||||
@@ -179,6 +180,12 @@ class Provider(NamedTuple):
|
||||
|
||||
|
||||
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",
|
||||
"bitfinex.com",
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from .void import VoidWallet
|
||||
from .clightning import CLightningWallet
|
||||
from .lndgrpc import LndWallet
|
||||
from .lntxbot import LntxbotWallet
|
||||
from .opennode import OpenNodeWallet
|
||||
from .lnpay import LNPayWallet
|
||||
|
||||
@@ -60,7 +60,9 @@ class Wallet(ABC):
|
||||
pass
|
||||
|
||||
@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
|
||||
|
||||
@abstractmethod
|
||||
|
||||
@@ -18,6 +18,7 @@ from .base import (
|
||||
Unsupported,
|
||||
Wallet,
|
||||
)
|
||||
from lnbits import bolt11 as lnbits_bolt11
|
||||
|
||||
|
||||
def async_wrap(func):
|
||||
@@ -31,8 +32,8 @@ def async_wrap(func):
|
||||
return run
|
||||
|
||||
|
||||
def _pay_invoice(ln, bolt11):
|
||||
return ln.pay(bolt11)
|
||||
def _pay_invoice(ln, payload):
|
||||
return ln.call("pay", payload)
|
||||
|
||||
|
||||
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}'."
|
||||
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:
|
||||
wrapped = async_wrap(_pay_invoice)
|
||||
r = await wrapped(self.ln, bolt11)
|
||||
r = await wrapped(self.ln, payload)
|
||||
except RpcError as exc:
|
||||
return PaymentResponse(False, None, 0, None, str(exc))
|
||||
|
||||
|
||||
+22
-9
@@ -36,7 +36,13 @@ class FakeWallet(Wallet):
|
||||
"out": False,
|
||||
"amount": amount,
|
||||
"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,
|
||||
"description_hash": None,
|
||||
"description": "",
|
||||
@@ -53,22 +59,29 @@ class FakeWallet(Wallet):
|
||||
data["tags_set"] = ["d"]
|
||||
data["memo"] = memo
|
||||
data["description"] = memo
|
||||
randomHash = data["privkey"][:6] + hashlib.sha256(
|
||||
str(random.getrandbits(256)).encode("utf-8")
|
||||
).hexdigest()[6:]
|
||||
randomHash = (
|
||||
data["privkey"][:6]
|
||||
+ hashlib.sha256(str(random.getrandbits(256)).encode("utf-8")).hexdigest()[
|
||||
6:
|
||||
]
|
||||
)
|
||||
data["paymenthash"] = randomHash
|
||||
payment_request = encode(data)
|
||||
checking_id = randomHash
|
||||
|
||||
return InvoiceResponse(True, checking_id, payment_request)
|
||||
|
||||
async def pay_invoice(self, bolt11: str) -> PaymentResponse:
|
||||
invoice = decode(bolt11)
|
||||
if hasattr(invoice, 'checking_id') and invoice.checking_id[6:] == data["privkey"][:6]:
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
invoice = decode(bolt11)
|
||||
if (
|
||||
hasattr(invoice, "checking_id")
|
||||
and invoice.checking_id[6:] == data["privkey"][:6]
|
||||
):
|
||||
return PaymentResponse(True, invoice.payment_hash, 0)
|
||||
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:
|
||||
return PaymentStatus(False)
|
||||
|
||||
@@ -80,7 +80,7 @@ class LNbitsWallet(Wallet):
|
||||
|
||||
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:
|
||||
r = await client.post(
|
||||
url=f"{self.endpoint}/api/v1/payments",
|
||||
|
||||
+12
-15
@@ -11,6 +11,7 @@ import base64
|
||||
import hashlib
|
||||
from os import environ, error, getenv
|
||||
from typing import Optional, Dict, AsyncGenerator
|
||||
from .macaroon import load_macaroon, AESCipher
|
||||
|
||||
if imports_ok:
|
||||
import lnbits.wallets.lnd_grpc_files.lightning_pb2 as ln
|
||||
@@ -58,12 +59,6 @@ def get_ssl_context(cert_path: str):
|
||||
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:
|
||||
return base64.b64decode(checking_id.replace("_", "/"))
|
||||
|
||||
@@ -90,7 +85,7 @@ class LndWallet(Wallet):
|
||||
self.port = int(getenv("LND_GRPC_PORT"))
|
||||
self.cert_path = getenv("LND_GRPC_CERT") or getenv("LND_CERT")
|
||||
|
||||
macaroon_path = (
|
||||
macaroon = (
|
||||
getenv("LND_GRPC_MACAROON")
|
||||
or getenv("LND_GRPC_ADMIN_MACAROON")
|
||||
or getenv("LND_ADMIN_MACAROON")
|
||||
@@ -98,10 +93,12 @@ class LndWallet(Wallet):
|
||||
or getenv("LND_INVOICE_MACAROON")
|
||||
)
|
||||
|
||||
if macaroon_path.split(".")[-1] == "macaroon":
|
||||
self.macaroon = load_macaroon(macaroon_path)
|
||||
else:
|
||||
self.macaroon = macaroon_path
|
||||
encrypted_macaroon = getenv("LND_GRPC_MACAROON_ENCRYPTED")
|
||||
if encrypted_macaroon:
|
||||
macaroon = AESCipher(description="macaroon decryption").decrypt(
|
||||
encrypted_macaroon
|
||||
)
|
||||
self.macaroon = load_macaroon(macaroon)
|
||||
|
||||
cert = open(self.cert_path, "rb").read()
|
||||
creds = grpc.ssl_channel_credentials(cert)
|
||||
@@ -147,10 +144,10 @@ class LndWallet(Wallet):
|
||||
payment_request = str(resp.payment_request)
|
||||
return InvoiceResponse(True, checking_id, payment_request, None)
|
||||
|
||||
async def pay_invoice(self, bolt11: str) -> PaymentResponse:
|
||||
resp = await self.rpc.SendPayment(
|
||||
lnrpc.SendPaymentRequest(payment_request=bolt11)
|
||||
)
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
fee_limit_fixed = ln.FeeLimit(fixed=fee_limit_msat // 1000)
|
||||
req = ln.SendRequest(payment_request=bolt11, fee_limit=fee_limit_fixed)
|
||||
resp = await self.rpc.SendPaymentSync(req)
|
||||
|
||||
if resp.payment_error:
|
||||
return PaymentResponse(False, "", 0, None, resp.payment_error)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
from pydoc import describe
|
||||
import httpx
|
||||
import json
|
||||
import base64
|
||||
@@ -6,6 +7,7 @@ from os import getenv
|
||||
from typing import Optional, Dict, AsyncGenerator
|
||||
|
||||
from lnbits import bolt11 as lnbits_bolt11
|
||||
from .macaroon import load_macaroon, AESCipher
|
||||
|
||||
from .base import (
|
||||
StatusResponse,
|
||||
@@ -34,8 +36,16 @@ class LndRestWallet(Wallet):
|
||||
or getenv("LND_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:
|
||||
try:
|
||||
@@ -89,15 +99,11 @@ class LndRestWallet(Wallet):
|
||||
|
||||
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:
|
||||
# set the fee limit for the payment
|
||||
invoice = lnbits_bolt11.decode(bolt11)
|
||||
lnrpcFeeLimit = dict()
|
||||
if invoice.amount_msat > 1000_000:
|
||||
lnrpcFeeLimit["percent"] = "1" # in percent
|
||||
else:
|
||||
lnrpcFeeLimit["fixed"] = "10" # in sat
|
||||
lnrpcFeeLimit["fixed_msat"] = "{}".format(fee_limit_msat)
|
||||
|
||||
r = await client.post(
|
||||
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
|
||||
# returned here are in hex, lnd is weird
|
||||
checking_id = checking_id.replace("_", "/")
|
||||
checking_id = base64.b64decode(checking_id).hex()
|
||||
|
||||
for p in r.json()["payments"]:
|
||||
|
||||
@@ -76,7 +76,7 @@ class LNPayWallet(Wallet):
|
||||
|
||||
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:
|
||||
r = await client.post(
|
||||
f"{self.endpoint}/wallet/{self.wallet_key}/withdraw",
|
||||
|
||||
@@ -74,7 +74,7 @@ class LntxbotWallet(Wallet):
|
||||
data = r.json()
|
||||
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:
|
||||
r = await client.post(
|
||||
f"{self.endpoint}/payinvoice",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from .macaroon import load_macaroon, AESCipher
|
||||
@@ -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)
|
||||
@@ -77,7 +77,7 @@ class OpenNodeWallet(Wallet):
|
||||
payment_request = data["lightning_invoice"]["payreq"]
|
||||
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:
|
||||
r = await client.post(
|
||||
f"{self.endpoint}/v2/withdrawals",
|
||||
|
||||
@@ -107,7 +107,7 @@ class SparkWallet(Wallet):
|
||||
|
||||
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:
|
||||
r = await self.pay(bolt11)
|
||||
except (SparkError, UnknownError) as exc:
|
||||
|
||||
@@ -25,7 +25,7 @@ class VoidWallet(Wallet):
|
||||
)
|
||||
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("")
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
|
||||
+2
-1
@@ -25,6 +25,7 @@ markupsafe==2.0.1
|
||||
marshmallow==3.13.0
|
||||
outcome==1.1.0
|
||||
psycopg2-binary==2.9.1
|
||||
pycryptodomex==3.14.1
|
||||
pydantic==1.8.2
|
||||
pypng==0.0.21
|
||||
pyqrcode==1.2.1
|
||||
@@ -46,4 +47,4 @@ uvicorn==0.15.0
|
||||
uvloop==0.16.0
|
||||
watchgod==0.7
|
||||
websockets==10.0
|
||||
zipp==3.5.0
|
||||
zipp==3.5.0
|
||||
@@ -3,24 +3,30 @@ import secrets
|
||||
from lnbits.core.crud import get_wallet
|
||||
from lnbits.settings import HOST, PORT
|
||||
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.helpers import credit_wallet
|
||||
from tests.extensions.bleskomat.conftest import bleskomat, lnurl
|
||||
from tests.mocks import WALLET
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bleskomat_lnurl_api_missing_secret(client):
|
||||
response = await client.get("/bleskomat/u")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"status": "ERROR", "reason": "Missing secret"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bleskomat_lnurl_api_invalid_secret(client):
|
||||
response = await client.get("/bleskomat/u?k1=invalid-secret")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"status": "ERROR", "reason": "Invalid secret"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bleskomat_lnurl_api_unknown_api_key(client):
|
||||
query = {
|
||||
@@ -33,11 +39,12 @@ async def test_bleskomat_lnurl_api_unknown_api_key(client):
|
||||
"f": "EUR",
|
||||
}
|
||||
payload = query_to_signing_payload(query)
|
||||
signature = "xxx"# not checked, so doesn't matter
|
||||
response = await client.get(f'/bleskomat/u?{payload}&signature={signature}')
|
||||
signature = "xxx" # not checked, so doesn't matter
|
||||
response = await client.get(f"/bleskomat/u?{payload}&signature={signature}")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"status": "ERROR", "reason": "Unknown API key"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bleskomat_lnurl_api_invalid_signature(client, bleskomat):
|
||||
query = {
|
||||
@@ -51,10 +58,11 @@ async def test_bleskomat_lnurl_api_invalid_signature(client, bleskomat):
|
||||
}
|
||||
payload = query_to_signing_payload(query)
|
||||
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.json() == {"status": "ERROR", "reason": "Invalid API key signature"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bleskomat_lnurl_api_valid_signature(client, bleskomat):
|
||||
query = {
|
||||
@@ -64,41 +72,46 @@ async def test_bleskomat_lnurl_api_valid_signature(client, bleskomat):
|
||||
"minWithdrawable": "1",
|
||||
"maxWithdrawable": "1",
|
||||
"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)
|
||||
signature = generate_bleskomat_lnurl_signature(
|
||||
payload=payload,
|
||||
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
|
||||
data = response.json()
|
||||
assert data["tag"] == "withdrawRequest"
|
||||
assert data["minWithdrawable"] == 1000
|
||||
assert data["maxWithdrawable"] == 1000
|
||||
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"]
|
||||
lnurl = await get_bleskomat_lnurl(secret=k1)
|
||||
assert lnurl
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bleskomat_lnurl_api_action_insufficient_balance(client, lnurl):
|
||||
bleskomat = lnurl["bleskomat"]
|
||||
secret = lnurl["secret"]
|
||||
pr = "lntb500n1pseq44upp5xqd38rgad72lnlh4gl339njlrsl3ykep82j6gj4g02dkule7k54qdqqcqzpgxqyz5vqsp5h0zgewuxdxcl2rnlumh6g520t4fr05rgudakpxm789xgjekha75s9qyyssq5vhwsy9knhfeqg0wn6hcnppwmum8fs3g3jxkgw45havgfl6evchjsz3s8e8kr6eyacz02szdhs7v5lg0m7wehd5rpf6yg8480cddjlqpae52xu"
|
||||
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.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)
|
||||
assert wallet.balance_msat == 0
|
||||
bleskomat_lnurl = await get_bleskomat_lnurl(secret)
|
||||
assert bleskomat_lnurl.has_uses_remaining() == True
|
||||
WALLET.pay_invoice.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bleskomat_lnurl_api_action_success(client, lnurl):
|
||||
bleskomat = lnurl["bleskomat"]
|
||||
@@ -111,10 +124,10 @@ async def test_bleskomat_lnurl_api_action_success(client, lnurl):
|
||||
wallet = await get_wallet(bleskomat.wallet)
|
||||
assert wallet.balance_msat == 100000
|
||||
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"}
|
||||
wallet = await get_wallet(bleskomat.wallet)
|
||||
assert wallet.balance_msat == 50000
|
||||
bleskomat_lnurl = await get_bleskomat_lnurl(secret)
|
||||
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
@@ -9,28 +9,42 @@ from lnbits.wallets.base import (
|
||||
)
|
||||
from lnbits.settings import WALLET
|
||||
|
||||
WALLET.status = AsyncMock(return_value=StatusResponse(
|
||||
"",# no error
|
||||
1000000,# msats
|
||||
))
|
||||
WALLET.create_invoice = AsyncMock(return_value=InvoiceResponse(
|
||||
True,# ok
|
||||
"6621aafbdd7709ca6eea6203f362d64bd7cb2911baa91311a176b3ecaf2274bd",# checking_id (i.e. payment_hash)
|
||||
"lntb1u1psezhgspp5vcs6477awuyu5mh2vgplxckkf0tuk2g3h253xydpw6e7etezwj7sdqqcqzpgxqyz5vqsp5dxpw8zs77hw5pla4wz4mfujllyxtlpu443auur2uxqdrs8q2h56q9qyyssq65zk30ylmygvv5y4tuwalnf3ttnqjn57ef6rmcqg0s53akem560jh8ptemjcmytn3lrlatw4hv9smg88exv3v4f4lqnp96s0psdrhxsp6pp75q",# payment_request
|
||||
"",# no error
|
||||
))
|
||||
def pay_invoice_side_effect(payment_request: str):
|
||||
WALLET.status = AsyncMock(
|
||||
return_value=StatusResponse(
|
||||
"", # no error
|
||||
1000000, # msats
|
||||
)
|
||||
)
|
||||
WALLET.create_invoice = AsyncMock(
|
||||
return_value=InvoiceResponse(
|
||||
True, # ok
|
||||
"6621aafbdd7709ca6eea6203f362d64bd7cb2911baa91311a176b3ecaf2274bd", # checking_id (i.e. payment_hash)
|
||||
"lntb1u1psezhgspp5vcs6477awuyu5mh2vgplxckkf0tuk2g3h253xydpw6e7etezwj7sdqqcqzpgxqyz5vqsp5dxpw8zs77hw5pla4wz4mfujllyxtlpu443auur2uxqdrs8q2h56q9qyyssq65zk30ylmygvv5y4tuwalnf3ttnqjn57ef6rmcqg0s53akem560jh8ptemjcmytn3lrlatw4hv9smg88exv3v4f4lqnp96s0psdrhxsp6pp75q", # payment_request
|
||||
"", # no error
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def pay_invoice_side_effect(
|
||||
payment_request: str, fee_limit_msat: int
|
||||
) -> PaymentResponse:
|
||||
invoice = bolt11.decode(payment_request)
|
||||
return PaymentResponse(
|
||||
True,# ok
|
||||
invoice.payment_hash,# checking_id (i.e. payment_hash)
|
||||
0,# fee_msat
|
||||
"",# no error
|
||||
True, # ok
|
||||
invoice.payment_hash, # checking_id (i.e. payment_hash)
|
||||
0, # fee_msat
|
||||
"", # no error
|
||||
)
|
||||
|
||||
|
||||
WALLET.pay_invoice = AsyncMock(side_effect=pay_invoice_side_effect)
|
||||
WALLET.get_invoice_status = AsyncMock(return_value=PaymentStatus(
|
||||
True,# paid
|
||||
))
|
||||
WALLET.get_payment_status = AsyncMock(return_value=PaymentStatus(
|
||||
True,# paid
|
||||
))
|
||||
WALLET.get_invoice_status = AsyncMock(
|
||||
return_value=PaymentStatus(
|
||||
True, # paid
|
||||
)
|
||||
)
|
||||
WALLET.get_payment_status = AsyncMock(
|
||||
return_value=PaymentStatus(
|
||||
True, # paid
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user