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 | ||
|
|
a7c65d5d60 | ||
|
|
50fcb15725 | ||
|
|
18ae455824 | ||
|
|
8db19f33e3 | ||
|
|
8a43611232 | ||
|
|
75e78d2ab6 | ||
|
|
568a1be91f | ||
|
|
b10a23dcc4 | ||
|
|
dab943baad | ||
|
|
db734d4acd | ||
|
|
0415f3d7cc | ||
|
|
3927545e32 | ||
|
|
3f0a855526 | ||
|
|
615abffeb4 | ||
|
|
31e300e05b | ||
|
|
d2879aa80c | ||
|
|
d1b390b929 | ||
|
|
d1a78ab81b | ||
|
|
34b3188f96 | ||
|
|
bdf5dd55f6 | ||
|
|
3ae6ef25a1 | ||
|
|
382622d5ac | ||
|
|
0680669e59 | ||
|
|
dbab181759 | ||
|
|
f5069f9699 | ||
|
|
e97e20889e | ||
|
|
68ba219096 | ||
|
|
649776951c | ||
|
|
7c3b3ac200 | ||
|
|
a147a07bab | ||
|
|
228717195b | ||
|
|
f0d9870143 | ||
|
|
d6f145eee9 | ||
|
|
47300e477e | ||
|
|
c96e7068e5 | ||
|
|
b4d00a490b | ||
|
|
9505958306 | ||
|
|
b00a39a7ce | ||
|
|
b4dc07170f | ||
|
|
57eb27fcee | ||
|
|
9e13b8eb70 | ||
|
|
09bfa35cd5 | ||
|
|
918e9be7ed | ||
|
|
fc021b3736 | ||
|
|
6718ed678d | ||
|
|
ab62014bf0 | ||
|
|
1940b8762b | ||
|
|
46838ca66f | ||
|
|
5fe9df48dd | ||
|
|
5bacb5f38a | ||
|
|
0f70b346ca | ||
|
|
f162d14e41 | ||
|
|
c87aa3dd2b | ||
|
|
7812c4aa48 | ||
|
|
fefa8ba56e | ||
|
|
1dd8b01f5a | ||
|
|
81dc9303f6 | ||
|
|
10ccee3480 | ||
|
|
8ca8d173e8 | ||
|
|
46e682bca3 | ||
|
|
11bf2029be | ||
|
|
a44aba6fcf | ||
|
|
d1a7d684b5 | ||
|
|
4e0e5b9956 | ||
|
|
25b502e5d5 | ||
|
|
6af8899cd1 | ||
|
|
7407be944c | ||
|
|
a00e46f629 | ||
|
|
aba206c87e | ||
|
|
db666fd103 | ||
|
|
57a2dffe52 | ||
|
|
d5617b9462 | ||
|
|
79ab444f59 | ||
|
|
480546ded7 | ||
|
|
6645b156e7 | ||
|
|
17b13bb652 | ||
|
|
6ba88d262b | ||
|
|
f76ef6b975 | ||
|
|
12bd930334 | ||
|
|
b9104bf63b | ||
|
|
f798ab9f57 | ||
|
|
e3c6d7aebe | ||
|
|
87105b22bb | ||
|
|
1fd2ccc54e | ||
|
|
aa8557ada1 | ||
|
|
cdeccec01b | ||
|
|
2991cbce1f | ||
|
|
10255ed1ce | ||
|
|
df2345bda9 | ||
|
|
adcfc43f56 | ||
|
|
aa58775c90 | ||
|
|
0ef0188462 | ||
|
|
e42fdde2ac | ||
|
|
32ea1106f9 | ||
|
|
93e58f4c80 | ||
|
|
6b5aaa442d | ||
|
|
40aadbfec7 | ||
|
|
a1ae7aca9e |
+19
-13
@@ -5,10 +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://...
|
||||
@@ -17,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"
|
||||
|
||||
@@ -27,10 +33,10 @@ 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),
|
||||
# LndRestWallet, CLightningWallet, LNbitsWallet, SparkWallet
|
||||
# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet,
|
||||
# LndRestWallet, CLightningWallet, LNbitsWallet, SparkWallet, FakeWallet
|
||||
LNBITS_BACKEND_WALLET_CLASS=VoidWallet
|
||||
# VoidWallet is just a fallback that works without any actual Lightning capabilities,
|
||||
# just so you can see the UI before dealing with this file.
|
||||
@@ -44,19 +50,15 @@ SPARK_TOKEN=myaccesstoken
|
||||
CLIGHTNING_RPC="/home/bob/.lightning/bitcoin/lightning-rpc"
|
||||
|
||||
# LnbitsWallet
|
||||
LNBITS_ENDPOINT=http://127.0.0.1:5000
|
||||
LNBITS_ENDPOINT=https://legend.lnbits.com
|
||||
LNBITS_KEY=LNBITS_ADMIN_KEY
|
||||
|
||||
# LndWallet
|
||||
LND_GRPC_ENDPOINT=127.0.0.1
|
||||
LND_GRPC_PORT=11009
|
||||
LND_GRPC_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert"
|
||||
LND_GRPC_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/admin.macaroon 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/
|
||||
@@ -72,3 +74,7 @@ LNTXBOT_KEY=LNTXBOT_ADMIN_KEY
|
||||
# OpenNodeWallet
|
||||
OPENNODE_API_ENDPOINT=https://api.opennode.com/
|
||||
OPENNODE_KEY=OPENNODE_ADMIN_KEY
|
||||
|
||||
# FakeWallet
|
||||
FAKE_WALLET_SECRET="ToTheMoon1"
|
||||
LNBITS_DENOMINATION=sats
|
||||
@@ -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"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Arc
|
||||
Copyright (c) 2022 Arc
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -29,6 +29,8 @@ uvicorn = {extras = ["standard"], version = "*"}
|
||||
sse-starlette = "*"
|
||||
jinja2 = "3.0.1"
|
||||
pyngrok = "*"
|
||||
secp256k1 = "*"
|
||||
pycryptodomex = "*"
|
||||
|
||||
[dev-packages]
|
||||
black = "==20.8b1"
|
||||
|
||||
Generated
+553
-323
File diff suppressed because it is too large
Load Diff
@@ -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:
|
||||
@@ -34,11 +33,7 @@ LNbits is inspired by all the great work of [opennode.com](https://www.opennode.
|
||||
|
||||
## Running LNbits
|
||||
|
||||
See the [install guide](docs/guide/installation.md) for details on installation and setup.
|
||||
|
||||
### Contributing to LNbits
|
||||
|
||||
There's a [slightly different setup](docs/devs/installation.md) if you want to contribute to LNbits, but if your changes don't require adding or removing any package dependencies you don't have to bother with that, just follow the [normal installation](docs/guide/installation.md) steps.
|
||||
See the [install guide](docs/devs/installation.md) for details on installation and setup.
|
||||
|
||||
## LNbits as an account system
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
## Python script to migrate an LNbits SQLite DB to Postgres
|
||||
## All credits to @Fritz446 for the awesome work
|
||||
|
||||
## pip install psycopg2 OR psycopg2-binary
|
||||
|
||||
import os
|
||||
import sqlite3
|
||||
|
||||
import psycopg2
|
||||
import sqlite3
|
||||
import os
|
||||
# Python script to migrate an LNbits SQLite DB to Postgres
|
||||
# All credits to @Fritz446 for the awesome work
|
||||
|
||||
|
||||
# pip install psycopg2 OR psycopg2-binary
|
||||
|
||||
|
||||
# Change these values as needed
|
||||
|
||||
@@ -34,7 +34,8 @@ def get_postgres_cursor():
|
||||
def check_db_versions(sqdb):
|
||||
sqlite = get_sqlite_cursor(sqdb)
|
||||
dblite = dict(sqlite.execute("SELECT * FROM dbversions;").fetchall())
|
||||
del dblite["lnurlpos"] # wrongly added?
|
||||
if "lnurlpos" in dblite:
|
||||
del dblite["lnurlpos"]
|
||||
sqlite.close()
|
||||
|
||||
postgres = get_postgres_cursor()
|
||||
@@ -42,7 +43,7 @@ def check_db_versions(sqdb):
|
||||
dbpost = dict(postgres.fetchall())
|
||||
|
||||
for key in dblite.keys():
|
||||
if dblite[key] != dbpost[key]:
|
||||
if key in dblite and key in dbpost and dblite[key] != dbpost[key]:
|
||||
raise Exception(
|
||||
f"sqlite database version ({dblite[key]}) of {key} doesn't match postgres database version {dbpost[key]}"
|
||||
)
|
||||
@@ -76,7 +77,10 @@ def insert_to_pg(query, data):
|
||||
connection = cursor.connection
|
||||
|
||||
for d in data:
|
||||
cursor.execute(query, d)
|
||||
try:
|
||||
cursor.execute(query, d)
|
||||
except:
|
||||
raise ValueError(f"Failed to insert {d}")
|
||||
connection.commit()
|
||||
|
||||
cursor.close()
|
||||
|
||||
+28
-41
@@ -7,59 +7,46 @@ nav_order: 1
|
||||
|
||||
# Installation
|
||||
|
||||
Download the latest stable release https://github.com/lnbits/lnbits/releases
|
||||
LNbits uses [Pipenv][pipenv] to manage Python packages.
|
||||
|
||||
## Application dependencies
|
||||
```sh
|
||||
git clone https://github.com/lnbits/lnbits-legend.git
|
||||
cd lnbits-legend/
|
||||
|
||||
The application uses [Pipenv][pipenv] to manage Python packages.
|
||||
While in development, you will need to install all dependencies:
|
||||
sudo apt-get install pipenv
|
||||
pipenv shell
|
||||
# pipenv --python 3.9 shell (if you wish to use a version of Python higher than 3.7)
|
||||
pipenv install --dev
|
||||
# pipenv --python 3.9 install --dev (if you wish to use a version of Python higher than 3.7)
|
||||
|
||||
git clone https://github.com/lnbits/lnbits-legend.git
|
||||
cd lnbits-legend/
|
||||
# for now you'll need to `git checkout FastAPI`
|
||||
pipenv shell
|
||||
pipenv install --dev
|
||||
# If any of the modules fails to install, try checking and upgrading your setupTool module
|
||||
# pip install -U setuptools
|
||||
|
||||
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
|
||||
|
||||
If you wish to use a version of Python higher than 3.7:
|
||||
Create the data folder and edit the .env file:
|
||||
|
||||
pipenv --python 3.8 install --dev
|
||||
mkdir data
|
||||
cp .env.example .env
|
||||
sudo nano .env
|
||||
|
||||
You will need to copy `.env.example` to `.env`, then set variables there.
|
||||
To then run the server for development purposes (includes hot-reload), use:
|
||||
|
||||

|
||||
pipenv run python -m uvicorn lnbits.__main__:app --host 0.0.0.0 --reload
|
||||
|
||||
For production, use:
|
||||
|
||||
pipenv run python -m uvicorn lnbits.__main__:app --host 0.0.0.0
|
||||
|
||||
You might also need to install additional packages, depending on the [backend wallet](../guide/wallets.md) you use.
|
||||
E.g. when you want to use LND you have to `pipenv run pip install lndgrpc` and `pipenv run pip install purerpc`.
|
||||
|
||||
Take a look at [Polar][polar] for an excellent way of spinning up a Lightning Network dev environment.
|
||||
|
||||
## Running the server
|
||||
**Notes**:
|
||||
|
||||
LNbits uses [FastAPI][fastapi] as an application server.
|
||||
Before running the server for the first time, make sure to create the data folder and edit the .env file:
|
||||
|
||||
mkdir data
|
||||
cp .env.example .env
|
||||
sudo nano .env
|
||||
|
||||
To then run the server, use:
|
||||
|
||||
pipenv run python -m uvicorn lnbits.__main__:app --reload
|
||||
|
||||
**Note**: You'll need to use _https_ for some endpoints and/or extensions. You can use [ngrok](https://ngrok.com/) for that. Follow the installation instructions on the website and when it's all set you can run:
|
||||
|
||||
./nrok http 5000
|
||||
|
||||
this will give you an _https_ tunnel for the _localhost_, use that URL for navigating to LNBits.
|
||||
|
||||
## Frontend
|
||||
|
||||
The frontend uses [Vue.js and Quasar][quasar].
|
||||
|
||||
[quart]: https://pgjones.gitlab.io/
|
||||
[pipenv]: https://pipenv.pypa.io/
|
||||
[polar]: https://lightningpolar.com/
|
||||
[quasar]: https://quasar.dev/start/how-to-use-vue
|
||||
* We reccomend using <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.
|
||||
|
||||
@@ -6,6 +6,7 @@ nav_order: 2
|
||||
|
||||
# Basic installation
|
||||
Install Postgres and setup a database for LNbits:
|
||||
|
||||
```sh
|
||||
# on debian/ubuntu 'sudo apt-get -y install postgresql'
|
||||
# or follow instructions at https://www.postgresql.org/download/linux/
|
||||
@@ -25,14 +26,13 @@ Download this repo and install the dependencies:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/lnbits/lnbits-legend.git
|
||||
cd lnbits/
|
||||
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
|
||||
# add the database connection string to .env 'nano .env' LNBITS_DATABASE_URL=
|
||||
# postgres://<user>:<password>@<host>/<database> - alter line bellow with your user, password and db name
|
||||
# postgres://<user>:<myPassword>@<host>/<lnbits> - alter line bellow with your user, password and db name
|
||||
LNBITS_DATABASE_URL="postgres://postgres:postgres@localhost/lnbits"
|
||||
# save and exit
|
||||
./venv/bin/uvicorn lnbits.__main__:app --port 5000
|
||||
@@ -50,6 +50,7 @@ You might also need to install additional packages or perform additional setup s
|
||||
If you already have LNbits installed and running, on an SQLite database, we **HIGHLY** recommend you migrate to postgres!
|
||||
|
||||
There's a script included that can do the migration easy. You should have Postgres already installed and there should be a password for the user, check the guide above.
|
||||
|
||||
```sh
|
||||
# STOP LNbits
|
||||
# on the LNBits folder, locate and edit 'conv.py' with the relevant credentials
|
||||
@@ -67,6 +68,39 @@ Hopefully, everything works and get migrated... Launch LNbits again and check if
|
||||
|
||||
# Additional guides
|
||||
|
||||
### LNbits as a systemd service
|
||||
|
||||
Systemd is great for taking care of your LNbits instance. It will start it on boot and restart it in case it crashes. If you want to run LNbits as a systemd service on your Debian/Ubuntu/Raspbian server, create a file at `/etc/systemd/system/lnbits.service` with the following content:
|
||||
|
||||
```
|
||||
# Systemd unit for lnbits
|
||||
# /etc/systemd/system/lnbits.service
|
||||
|
||||
[Unit]
|
||||
Description=LNbits
|
||||
#Wants=lnd.service # you can uncomment these lines if you know what you're doing
|
||||
#After=lnd.service # it will make sure that lnbits starts after lnd (replace with your own backend service)
|
||||
|
||||
[Service]
|
||||
WorkingDirectory=/home/bitcoin/lnbits # replace with the absolute path of your lnbits installation
|
||||
ExecStart=/home/bitcoin/lnbits/venv/bin/uvicorn lnbits.__main__:app --port 5000 # same here
|
||||
User=bitcoin # replace with the user that you're running lnbits on
|
||||
Restart=always
|
||||
TimeoutSec=120
|
||||
RestartSec=30
|
||||
Environment=PYTHONUNBUFFERED=1 # this makes sure that you receive logs in real time
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Save the file and run the following commands:
|
||||
|
||||
```sh
|
||||
sudo systemctl enable lnbits.service
|
||||
sudo systemctl start lnbits.service
|
||||
```
|
||||
|
||||
### LNbits running on Umbrel behind Tor
|
||||
|
||||
If you want to run LNbits on your Umbrel but want it to be reached through clearnet, _Uxellodunum_ made an extensive [guide](https://community.getumbrel.com/t/guide-lnbits-without-tor/604) on how to do it.
|
||||
|
||||
+11
-2
@@ -17,6 +17,7 @@ A backend wallet can be configured using the following LNbits environment variab
|
||||
### CLightning
|
||||
|
||||
Using this wallet requires the installation of the `pylightning` Python package.
|
||||
If you want to use LNURLp you should use SparkWallet because of an issue with description_hash and CLightning.
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **CLightningWallet**
|
||||
- `CLIGHTNING_RPC`: /file/path/lightning-rpc
|
||||
@@ -35,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
|
||||
|
||||
|
||||
+12
-15
@@ -1,6 +1,5 @@
|
||||
import asyncio
|
||||
import importlib
|
||||
from lnbits.core.tasks import register_task_listeners
|
||||
import sys
|
||||
import traceback
|
||||
import warnings
|
||||
@@ -12,6 +11,7 @@ from fastapi.middleware.gzip import GZipMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
import lnbits.settings
|
||||
from lnbits.core.tasks import register_task_listeners
|
||||
|
||||
from .commands import db_migrate, handle_assets
|
||||
from .core import core_app
|
||||
@@ -45,14 +45,10 @@ def create_app(config_object="lnbits.settings") -> FastAPI:
|
||||
"/core/static", StaticFiles(directory="lnbits/core/static"), name="core_static"
|
||||
)
|
||||
|
||||
origins = ["http://localhost", "http://localhost:5000"]
|
||||
origins = ["*"]
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
CORSMiddleware, allow_origins=origins, allow_methods=["*"], allow_headers=["*"]
|
||||
)
|
||||
|
||||
g().config = lnbits.settings
|
||||
@@ -88,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:
|
||||
|
||||
+205
-4
@@ -2,10 +2,14 @@ import bitstring # type: ignore
|
||||
import re
|
||||
import hashlib
|
||||
from typing import List, NamedTuple, Optional
|
||||
from bech32 import bech32_decode, CHARSET # type: ignore
|
||||
from bech32 import bech32_encode, bech32_decode, CHARSET
|
||||
from ecdsa import SECP256k1, VerifyingKey # type: ignore
|
||||
from ecdsa.util import sigdecode_string # type: ignore
|
||||
from binascii import unhexlify
|
||||
import time
|
||||
from decimal import Decimal
|
||||
import embit
|
||||
import secp256k1
|
||||
|
||||
|
||||
class Route(NamedTuple):
|
||||
@@ -116,6 +120,166 @@ def decode(pr: str) -> Invoice:
|
||||
return invoice
|
||||
|
||||
|
||||
def encode(options):
|
||||
"""Convert options into LnAddr and pass it to the encoder"""
|
||||
addr = LnAddr()
|
||||
addr.currency = options["currency"]
|
||||
addr.fallback = options["fallback"] if options["fallback"] else None
|
||||
if options["amount"]:
|
||||
addr.amount = options["amount"]
|
||||
if options["timestamp"]:
|
||||
addr.date = int(options["timestamp"])
|
||||
|
||||
addr.paymenthash = unhexlify(options["paymenthash"])
|
||||
|
||||
if options["description"]:
|
||||
addr.tags.append(("d", options["description"]))
|
||||
if options["description_hash"]:
|
||||
addr.tags.append(("h", options["description_hash"]))
|
||||
if options["expires"]:
|
||||
addr.tags.append(("x", options["expires"]))
|
||||
|
||||
if options["fallback"]:
|
||||
addr.tags.append(("f", options["fallback"]))
|
||||
if options["route"]:
|
||||
for r in options["route"]:
|
||||
splits = r.split("/")
|
||||
route = []
|
||||
while len(splits) >= 5:
|
||||
route.append(
|
||||
(
|
||||
unhexlify(splits[0]),
|
||||
unhexlify(splits[1]),
|
||||
int(splits[2]),
|
||||
int(splits[3]),
|
||||
int(splits[4]),
|
||||
)
|
||||
)
|
||||
splits = splits[5:]
|
||||
assert len(splits) == 0
|
||||
addr.tags.append(("r", route))
|
||||
return lnencode(addr, options["privkey"])
|
||||
|
||||
|
||||
def lnencode(addr, privkey):
|
||||
if addr.amount:
|
||||
amount = Decimal(str(addr.amount))
|
||||
# We can only send down to millisatoshi.
|
||||
if amount * 10 ** 12 % 10:
|
||||
raise ValueError(
|
||||
"Cannot encode {}: too many decimal places".format(addr.amount)
|
||||
)
|
||||
|
||||
amount = addr.currency + shorten_amount(amount)
|
||||
else:
|
||||
amount = addr.currency if addr.currency else ""
|
||||
|
||||
hrp = "ln" + amount + "0n"
|
||||
|
||||
# Start with the timestamp
|
||||
data = bitstring.pack("uint:35", addr.date)
|
||||
|
||||
# Payment hash
|
||||
data += tagged_bytes("p", addr.paymenthash)
|
||||
tags_set = set()
|
||||
|
||||
for k, v in addr.tags:
|
||||
|
||||
# BOLT #11:
|
||||
#
|
||||
# A writer MUST NOT include more than one `d`, `h`, `n` or `x` fields,
|
||||
if k in ("d", "h", "n", "x"):
|
||||
if k in tags_set:
|
||||
raise ValueError("Duplicate '{}' tag".format(k))
|
||||
|
||||
if k == "r":
|
||||
route = bitstring.BitArray()
|
||||
for step in v:
|
||||
pubkey, channel, feebase, feerate, cltv = step
|
||||
route.append(
|
||||
bitstring.BitArray(pubkey)
|
||||
+ bitstring.BitArray(channel)
|
||||
+ bitstring.pack("intbe:32", feebase)
|
||||
+ bitstring.pack("intbe:32", feerate)
|
||||
+ bitstring.pack("intbe:16", cltv)
|
||||
)
|
||||
data += tagged("r", route)
|
||||
elif k == "f":
|
||||
data += encode_fallback(v, addr.currency)
|
||||
elif k == "d":
|
||||
data += tagged_bytes("d", v.encode())
|
||||
elif k == "x":
|
||||
# Get minimal length by trimming leading 5 bits at a time.
|
||||
expirybits = bitstring.pack("intbe:64", v)[4:64]
|
||||
while expirybits.startswith("0b00000"):
|
||||
expirybits = expirybits[5:]
|
||||
data += tagged("x", expirybits)
|
||||
elif k == "h":
|
||||
data += tagged_bytes("h", hashlib.sha256(v.encode("utf-8")).digest())
|
||||
elif k == "n":
|
||||
data += tagged_bytes("n", v)
|
||||
else:
|
||||
# FIXME: Support unknown tags?
|
||||
raise ValueError("Unknown tag {}".format(k))
|
||||
|
||||
tags_set.add(k)
|
||||
|
||||
# BOLT #11:
|
||||
#
|
||||
# A writer MUST include either a `d` or `h` field, and MUST NOT include
|
||||
# both.
|
||||
if "d" in tags_set and "h" in tags_set:
|
||||
raise ValueError("Cannot include both 'd' and 'h'")
|
||||
if not "d" in tags_set and not "h" in tags_set:
|
||||
raise ValueError("Must include either 'd' or 'h'")
|
||||
|
||||
# We actually sign the hrp, then data (padded to 8 bits with zeroes).
|
||||
privkey = secp256k1.PrivateKey(bytes(unhexlify(privkey)))
|
||||
sig = privkey.ecdsa_sign_recoverable(
|
||||
bytearray([ord(c) for c in hrp]) + data.tobytes()
|
||||
)
|
||||
# This doesn't actually serialize, but returns a pair of values :(
|
||||
sig, recid = privkey.ecdsa_recoverable_serialize(sig)
|
||||
data += bytes(sig) + bytes([recid])
|
||||
|
||||
return bech32_encode(hrp, bitarray_to_u5(data))
|
||||
|
||||
|
||||
class LnAddr(object):
|
||||
def __init__(
|
||||
self, paymenthash=None, amount=None, currency="bc", tags=None, date=None
|
||||
):
|
||||
self.date = int(time.time()) if not date else int(date)
|
||||
self.tags = [] if not tags else tags
|
||||
self.unknown_tags = []
|
||||
self.paymenthash = paymenthash
|
||||
self.signature = None
|
||||
self.pubkey = None
|
||||
self.currency = currency
|
||||
self.amount = amount
|
||||
|
||||
def __str__(self):
|
||||
return "LnAddr[{}, amount={}{} tags=[{}]]".format(
|
||||
hexlify(self.pubkey.serialize()).decode("utf-8"),
|
||||
self.amount,
|
||||
self.currency,
|
||||
", ".join([k + "=" + str(v) for k, v in self.tags]),
|
||||
)
|
||||
|
||||
|
||||
def shorten_amount(amount):
|
||||
"""Given an amount in bitcoin, shorten it"""
|
||||
# Convert to pico initially
|
||||
amount = int(amount * 10 ** 12)
|
||||
units = ["p", "n", "u", "m", ""]
|
||||
for unit in units:
|
||||
if amount % 1000 == 0:
|
||||
amount //= 1000
|
||||
else:
|
||||
break
|
||||
return str(amount) + unit
|
||||
|
||||
|
||||
def _unshorten_amount(amount: str) -> int:
|
||||
"""Given a shortened amount, return millisatoshis"""
|
||||
# BOLT #11:
|
||||
@@ -146,6 +310,34 @@ def _pull_tagged(stream):
|
||||
return (CHARSET[tag], stream.read(length * 5), stream)
|
||||
|
||||
|
||||
def is_p2pkh(currency, prefix):
|
||||
return prefix == base58_prefix_map[currency][0]
|
||||
|
||||
|
||||
def is_p2sh(currency, prefix):
|
||||
return prefix == base58_prefix_map[currency][1]
|
||||
|
||||
|
||||
# Tagged field containing BitArray
|
||||
def tagged(char, l):
|
||||
# Tagged fields need to be zero-padded to 5 bits.
|
||||
while l.len % 5 != 0:
|
||||
l.append("0b0")
|
||||
return (
|
||||
bitstring.pack(
|
||||
"uint:5, uint:5, uint:5",
|
||||
CHARSET.find(char),
|
||||
(l.len / 5) / 32,
|
||||
(l.len / 5) % 32,
|
||||
)
|
||||
+ l
|
||||
)
|
||||
|
||||
|
||||
def tagged_bytes(char, l):
|
||||
return tagged(char, bitstring.BitArray(l))
|
||||
|
||||
|
||||
def _trim_to_bytes(barr):
|
||||
# Adds a byte if necessary.
|
||||
b = barr.tobytes()
|
||||
@@ -156,9 +348,9 @@ def _trim_to_bytes(barr):
|
||||
|
||||
def _readable_scid(short_channel_id: int) -> str:
|
||||
return "{blockheight}x{transactionindex}x{outputindex}".format(
|
||||
blockheight=((short_channel_id >> 40) & 0xFFFFFF),
|
||||
transactionindex=((short_channel_id >> 16) & 0xFFFFFF),
|
||||
outputindex=(short_channel_id & 0xFFFF),
|
||||
blockheight=((short_channel_id >> 40) & 0xffffff),
|
||||
transactionindex=((short_channel_id >> 16) & 0xffffff),
|
||||
outputindex=(short_channel_id & 0xffff),
|
||||
)
|
||||
|
||||
|
||||
@@ -167,3 +359,12 @@ def _u5_to_bitarray(arr: List[int]) -> bitstring.BitArray:
|
||||
for a in arr:
|
||||
ret += bitstring.pack("uint:5", a)
|
||||
return ret
|
||||
|
||||
|
||||
def bitarray_to_u5(barr):
|
||||
assert barr.len % 5 == 0
|
||||
ret = []
|
||||
s = bitstring.ConstBitStream(barr)
|
||||
while s.pos != s.len:
|
||||
ret.append(s.read(5).uint)
|
||||
return ret
|
||||
|
||||
+19
-4
@@ -6,12 +6,11 @@ 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
|
||||
|
||||
|
||||
# accounts
|
||||
# --------
|
||||
|
||||
@@ -62,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,
|
||||
)
|
||||
|
||||
|
||||
@@ -217,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]:
|
||||
"""
|
||||
@@ -261,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)}"
|
||||
@@ -271,14 +284,16 @@ 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]
|
||||
|
||||
|
||||
async def delete_expired_invoices(conn: Optional[Connection] = None,) -> None:
|
||||
async def delete_expired_invoices(
|
||||
conn: Optional[Connection] = None,
|
||||
) -> None:
|
||||
# first we delete all invoices older than one month
|
||||
await (conn or db).execute(
|
||||
f"""
|
||||
|
||||
@@ -57,6 +57,7 @@ class User(BaseModel):
|
||||
extensions: List[str] = []
|
||||
wallets: List[Wallet] = []
|
||||
password: Optional[str] = None
|
||||
admin: bool = False
|
||||
|
||||
@property
|
||||
def wallet_ids(self) -> List[str]:
|
||||
|
||||
+16
-13
@@ -85,20 +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)
|
||||
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.")
|
||||
|
||||
# put all parameters that don't change here
|
||||
PaymentKwargs = TypedDict(
|
||||
"PaymentKwargs",
|
||||
@@ -136,7 +133,7 @@ async def pay_invoice(
|
||||
# the balance is enough in the next step
|
||||
await create_payment(
|
||||
checking_id=temp_id,
|
||||
fee=-fee_reserve(invoice.amount_msat),
|
||||
fee=-fee_reserve_msat,
|
||||
conn=conn,
|
||||
**payment_kwargs,
|
||||
)
|
||||
@@ -145,6 +142,10 @@ async def pay_invoice(
|
||||
wallet = await get_wallet(wallet_id, conn=conn)
|
||||
assert wallet
|
||||
if wallet.balance_msat < 0:
|
||||
if not internal_checking_id and wallet.balance_msat > -fee_reserve_msat:
|
||||
raise PaymentFailure(
|
||||
f"You must reserve at least 1% ({round(fee_reserve_msat/1000)} sat) to cover potential routing fees."
|
||||
)
|
||||
raise PermissionError("Insufficient balance.")
|
||||
|
||||
if internal_checking_id:
|
||||
@@ -163,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(
|
||||
@@ -278,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)
|
||||
|
||||
@@ -322,8 +325,7 @@ async def check_invoice_status(
|
||||
if not payment.pending:
|
||||
return status
|
||||
if payment.is_out and status.failed:
|
||||
print(
|
||||
f" - deleting outgoing failed payment {payment.checking_id}: {status}")
|
||||
print(f" - deleting outgoing failed payment {payment.checking_id}: {status}")
|
||||
await payment.delete()
|
||||
elif not status.pending:
|
||||
print(
|
||||
@@ -333,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))
|
||||
|
||||
@@ -161,14 +161,14 @@ new Vue({
|
||||
{
|
||||
name: 'sat',
|
||||
align: 'right',
|
||||
label: 'Amount (sat)',
|
||||
label: 'Amount (' + LNBITS_DENOMINATION + ')',
|
||||
field: 'sat',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'fee',
|
||||
align: 'right',
|
||||
label: 'Fee (msat)',
|
||||
label: 'Fee (m' + LNBITS_DENOMINATION + ')',
|
||||
field: 'fee'
|
||||
}
|
||||
],
|
||||
@@ -185,12 +185,17 @@ new Vue({
|
||||
location: window.location
|
||||
},
|
||||
balance: 0,
|
||||
credit: 0,
|
||||
newName: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
formattedBalance: function () {
|
||||
return LNbits.utils.formatSat(this.balance || this.g.wallet.sat)
|
||||
if (LNBITS_DENOMINATION != 'sats') {
|
||||
return this.balance / 100
|
||||
} else {
|
||||
return LNbits.utils.formatSat(this.balance || this.g.wallet.sat)
|
||||
}
|
||||
},
|
||||
filteredPayments: function () {
|
||||
var q = this.paymentsTable.filter
|
||||
@@ -249,6 +254,28 @@ new Vue({
|
||||
this.parse.data.paymentChecker = null
|
||||
this.parse.camera.show = false
|
||||
},
|
||||
updateBalance: function (credit) {
|
||||
if (LNBITS_DENOMINATION != 'sats') {
|
||||
credit = credit * 100
|
||||
}
|
||||
LNbits.api
|
||||
.request('PUT', '/api/v1/wallet/balance/' + credit, this.g.wallet.inkey)
|
||||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
.then(response => {
|
||||
let data = response.data
|
||||
if (data.status === 'ERROR') {
|
||||
this.$q.notify({
|
||||
timeout: 5000,
|
||||
type: 'warning',
|
||||
message: `Failed to update.`
|
||||
})
|
||||
return
|
||||
}
|
||||
this.balance = this.balance + data.balance
|
||||
})
|
||||
},
|
||||
closeReceiveDialog: function () {
|
||||
setTimeout(() => {
|
||||
clearInterval(this.receive.paymentChecker)
|
||||
@@ -271,7 +298,9 @@ new Vue({
|
||||
},
|
||||
createInvoice: function () {
|
||||
this.receive.status = 'loading'
|
||||
|
||||
if (LNBITS_DENOMINATION != 'sats') {
|
||||
this.receive.data.amount = this.receive.data.amount * 100
|
||||
}
|
||||
LNbits.api
|
||||
.createInvoice(
|
||||
this.g.wallet,
|
||||
@@ -335,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]
|
||||
|
||||
@@ -50,7 +50,7 @@ async def dispatch_invoice_listener(payment: Payment):
|
||||
|
||||
async def dispatch_webhook(payment: Payment):
|
||||
async with httpx.AsyncClient() as client:
|
||||
data = payment._asdict()
|
||||
data = payment.dict()
|
||||
try:
|
||||
r = await client.post(payment.webhook, json=data, timeout=40)
|
||||
await mark_webhook_sent(payment, r.status_code)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -15,10 +15,54 @@
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h3 class="q-my-none">
|
||||
<strong>{% raw %}{{ formattedBalance }}{% endraw %}</strong> sat
|
||||
<strong>{% raw %}{{ formattedBalance }} {% endraw %}</strong>
|
||||
{{LNBITS_DENOMINATION}}
|
||||
<q-btn
|
||||
v-if="'{{user.admin}}' == 'True'"
|
||||
flat
|
||||
round
|
||||
color="primary"
|
||||
icon="add"
|
||||
size="md"
|
||||
>
|
||||
<q-popup-edit
|
||||
class="bg-accent text-white"
|
||||
v-slot="scope"
|
||||
v-model="credit"
|
||||
>
|
||||
<q-input
|
||||
v-if="'{{LNBITS_DENOMINATION}}' != 'sats'"
|
||||
label="Amount to credit account"
|
||||
v-model="scope.value"
|
||||
dense
|
||||
autofocus
|
||||
mask="#.##"
|
||||
fill-mask="0"
|
||||
reverse-fill-mask
|
||||
@keyup.enter="updateBalance(scope.value)"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<q-icon name="edit" />
|
||||
</template>
|
||||
</q-input>
|
||||
<q-input
|
||||
v-else
|
||||
type="number"
|
||||
label="Amount to credit account"
|
||||
v-model="scope.value"
|
||||
dense
|
||||
autofocus
|
||||
@keyup.enter="updateBalance(scope.value)"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<q-icon name="edit" />
|
||||
</template>
|
||||
</q-input>
|
||||
</q-popup-edit>
|
||||
</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
|
||||
@@ -141,7 +185,17 @@
|
||||
<q-tooltip>{{ props.row.date }}</q-tooltip>
|
||||
{{ props.row.dateFrom }}
|
||||
</q-td>
|
||||
<q-td auto-width key="sat" :props="props">
|
||||
{% endraw %}
|
||||
<q-td
|
||||
auto-width
|
||||
key="sat"
|
||||
v-if="'{{LNBITS_DENOMINATION}}' != 'sats'"
|
||||
:props="props"
|
||||
>{% raw %} {{ parseFloat(String(props.row.fsat).replaceAll(",",
|
||||
"")) / 100 }}
|
||||
</q-td>
|
||||
|
||||
<q-td auto-width key="sat" v-else :props="props">
|
||||
{{ props.row.fsat }}
|
||||
</q-td>
|
||||
<q-td auto-width key="fee" :props="props">
|
||||
@@ -219,401 +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>
|
||||
|
||||
<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 (${receive.unit}) *`"
|
||||
:step="receive.unit != 'sat' ? '0.001' : '1'"
|
||||
:min="receive.minMax[0]"
|
||||
:max="receive.minMax[1]"
|
||||
:readonly="receive.lnurl && receive.lnurl.fixed"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="receive.data.memo"
|
||||
label="Memo *"
|
||||
placeholder="LNbits invoice"
|
||||
></q-input>
|
||||
<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">
|
||||
{% raw %}
|
||||
<h6 class="q-my-none">{{ parse.invoice.fsat }} sat</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 }} sat
|
||||
<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> sat
|
||||
<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">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.number="parse.data.amount"
|
||||
type="number"
|
||||
label="Amount (sat) *"
|
||||
:min="parse.lnurlpay.minSendable / 1000"
|
||||
:max="parse.lnurlpay.maxSendable / 1000"
|
||||
:readonly="parse.lnurlpay.fixed"
|
||||
></q-input>
|
||||
</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 satoshis</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>
|
||||
|
||||
+110
-31
@@ -3,15 +3,16 @@ import hashlib
|
||||
import json
|
||||
from binascii import unhexlify
|
||||
from http import HTTPStatus
|
||||
from typing import Dict, Optional, Union
|
||||
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
|
||||
from pydantic import BaseModel
|
||||
from pydantic.fields import Field
|
||||
from sse_starlette.sse import EventSourceResponse
|
||||
|
||||
from lnbits import bolt11, lnurl
|
||||
@@ -22,16 +23,26 @@ 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.utils.exchange_rates import currencies, fiat_amount_as_satoshis
|
||||
from lnbits.settings import LNBITS_ADMIN_USERS, LNBITS_SITE_TITLE
|
||||
from lnbits.utils.exchange_rates import (
|
||||
currencies,
|
||||
fiat_amount_as_satoshis,
|
||||
satoshis_amount_as_fiat,
|
||||
)
|
||||
|
||||
from .. import core_app, db
|
||||
from ..crud import (
|
||||
create_payment,
|
||||
get_payments,
|
||||
get_standalone_payment,
|
||||
get_wallet,
|
||||
get_wallet_for_key,
|
||||
save_balance_check,
|
||||
update_payment_status,
|
||||
update_wallet,
|
||||
)
|
||||
from ..services import (
|
||||
@@ -57,6 +68,35 @@ async def api_wallet(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
return {"name": wallet.wallet.name, "balance": wallet.wallet.balance_msat}
|
||||
|
||||
|
||||
@core_app.put("/api/v1/wallet/balance/{amount}")
|
||||
async def api_update_balance(
|
||||
amount: int, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
):
|
||||
if wallet.wallet.user not in LNBITS_ADMIN_USERS:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN, detail="Not an admin user"
|
||||
)
|
||||
|
||||
payHash = urlsafe_short_hash()
|
||||
await create_payment(
|
||||
wallet_id=wallet.wallet.id,
|
||||
checking_id=payHash,
|
||||
payment_request="selfPay",
|
||||
payment_hash=payHash,
|
||||
amount=amount * 1000,
|
||||
memo="selfPay",
|
||||
fee=0,
|
||||
)
|
||||
await update_payment_status(checking_id=payHash, pending=False)
|
||||
updatedWallet = await get_wallet(wallet.wallet.id)
|
||||
|
||||
return {
|
||||
"id": wallet.wallet.id,
|
||||
"name": wallet.wallet.name,
|
||||
"balance": amount,
|
||||
}
|
||||
|
||||
|
||||
@core_app.put("/api/v1/wallet/{new_name}")
|
||||
async def api_update_wallet(
|
||||
new_name: str, wallet: WalletTypeInfo = Depends(WalletAdminKeyChecker())
|
||||
@@ -72,7 +112,9 @@ async def api_update_wallet(
|
||||
@core_app.get("/api/v1/payments")
|
||||
async def api_payments(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
await get_payments(wallet_id=wallet.wallet.id, pending=True, complete=True)
|
||||
pendingPayments = await get_payments(wallet_id=wallet.wallet.id, pending=True, exclude_uncheckable=True)
|
||||
pendingPayments = await get_payments(
|
||||
wallet_id=wallet.wallet.id, pending=True, exclude_uncheckable=True
|
||||
)
|
||||
for payment in pendingPayments:
|
||||
await check_invoice_status(
|
||||
wallet_id=payment.wallet_id, payment_hash=payment.payment_hash
|
||||
@@ -82,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
|
||||
@@ -99,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
|
||||
@@ -126,8 +168,8 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
|
||||
|
||||
lnurl_response: Union[None, bool, str] = None
|
||||
if data.lnurl_callback:
|
||||
if "lnurl_balance_check" in g().data:
|
||||
save_balance_check(g().wallet.id, data.lnurl_balance_check)
|
||||
if "lnurl_balance_check" in data:
|
||||
save_balance_check(wallet.id, data.lnurl_balance_check)
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
@@ -138,7 +180,7 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
|
||||
"balanceNotify": url_for(
|
||||
f"/withdraw/notify/{urlparse(data.lnurl_callback).netloc}",
|
||||
external=True,
|
||||
wal=g().wallet.id,
|
||||
wal=wallet.id,
|
||||
),
|
||||
},
|
||||
timeout=10,
|
||||
@@ -193,8 +235,7 @@ async def api_payments_create(
|
||||
invoiceData: CreateInvoiceData = Body(...),
|
||||
):
|
||||
if wallet.wallet_type < 0 or wallet.wallet_type > 2:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST, detail="Key is invalid")
|
||||
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="Key is invalid")
|
||||
|
||||
if invoiceData.out is True and wallet.wallet_type == 0:
|
||||
if not invoiceData.bolt11:
|
||||
@@ -249,14 +290,14 @@ async def api_payments_pay_lnurl(
|
||||
if invoice.amount_msat != data.amount:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=f"{domain} returned an invalid invoice. Expected {data['amount']} msat, got {invoice.amount_msat}.",
|
||||
detail=f"{domain} returned an invalid invoice. Expected {data.amount} msat, got {invoice.amount_msat}.",
|
||||
)
|
||||
|
||||
if invoice.description_hash != data.description_hash:
|
||||
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 = {}
|
||||
|
||||
@@ -323,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)
|
||||
@@ -332,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}
|
||||
|
||||
|
||||
@@ -374,16 +427,14 @@ async def api_lnurlscan(code: str):
|
||||
params.update(callback=url) # with k1 already in it
|
||||
|
||||
lnurlauth_key = g().wallet.lnurlauth_key(domain)
|
||||
params.update(
|
||||
pubkey=lnurlauth_key.verifying_key.to_string("compressed").hex())
|
||||
params.update(pubkey=lnurlauth_key.verifying_key.to_string("compressed").hex())
|
||||
else:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.get(url, timeout=5)
|
||||
if r.is_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
|
||||
detail={"domain": domain,
|
||||
"message": "failed to get parameters"},
|
||||
detail={"domain": domain, "message": "failed to get parameters"},
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -413,8 +464,7 @@ async def api_lnurlscan(code: str):
|
||||
|
||||
if tag == "withdrawRequest":
|
||||
params.update(kind="withdraw")
|
||||
params.update(fixed=data["minWithdrawable"]
|
||||
== data["maxWithdrawable"])
|
||||
params.update(fixed=data["minWithdrawable"] == data["maxWithdrawable"])
|
||||
|
||||
# callback with k1 already in it
|
||||
parsed_callback: ParseResult = urlparse(data["callback"])
|
||||
@@ -463,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,
|
||||
@@ -501,3 +556,27 @@ async def api_perform_lnurlauth(callback: str):
|
||||
@core_app.get("/api/v1/currencies")
|
||||
async def api_list_currencies_available():
|
||||
return list(currencies.keys())
|
||||
|
||||
|
||||
class ConversionData(BaseModel):
|
||||
from_: str = Field("sat", alias="from")
|
||||
amount: float
|
||||
to: str = Query("usd")
|
||||
|
||||
|
||||
@core_app.post("/api/v1/conversion")
|
||||
async def api_fiat_as_sats(data: ConversionData):
|
||||
output = {}
|
||||
if data.from_ == "sat":
|
||||
output["sats"] = int(data.amount)
|
||||
output["BTC"] = data.amount / 100000000
|
||||
for currency in data.to.split(","):
|
||||
output[currency.strip().upper()] = await satoshis_amount_as_fiat(
|
||||
data.amount, currency.strip()
|
||||
)
|
||||
return output
|
||||
else:
|
||||
output[data.from_.upper()] = data.amount
|
||||
output["sats"] = await fiat_amount_as_satoshis(data.amount, data.from_)
|
||||
output["BTC"] = output["sats"] / 100000000
|
||||
return output
|
||||
|
||||
@@ -14,7 +14,12 @@ from lnbits.core import db
|
||||
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_SITE_TITLE, SERVICE_FEE
|
||||
from lnbits.settings import (
|
||||
LNBITS_ADMIN_USERS,
|
||||
LNBITS_ALLOWED_USERS,
|
||||
LNBITS_SITE_TITLE,
|
||||
SERVICE_FEE,
|
||||
)
|
||||
|
||||
from ..crud import (
|
||||
create_account,
|
||||
@@ -43,9 +48,7 @@ async def home(request: Request, lightning: str = None):
|
||||
|
||||
|
||||
@core_html_routes.get(
|
||||
"/extensions",
|
||||
name="core.extensions",
|
||||
response_class=HTMLResponse,
|
||||
"/extensions", name="core.extensions", response_class=HTMLResponse
|
||||
)
|
||||
async def extensions(
|
||||
request: Request,
|
||||
@@ -115,6 +118,8 @@ async def wallet(
|
||||
return template_renderer().TemplateResponse(
|
||||
"error.html", {"request": request, "err": "User not authorized."}
|
||||
)
|
||||
if LNBITS_ADMIN_USERS and user_id in LNBITS_ADMIN_USERS:
|
||||
user.admin = True
|
||||
if not wallet_id:
|
||||
if user.wallets and not wallet_name:
|
||||
wallet = user.wallets[0]
|
||||
@@ -221,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_"):
|
||||
|
||||
+33
-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
|
||||
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
|
||||
@@ -172,6 +179,26 @@ async def require_admin_key(
|
||||
return wallet
|
||||
|
||||
|
||||
async def require_invoice_key(
|
||||
r: Request,
|
||||
api_key_header: str = Security(api_key_header),
|
||||
api_key_query: str = Security(api_key_query),
|
||||
):
|
||||
token = api_key_header if api_key_header else api_key_query
|
||||
|
||||
wallet = await get_key_type(r, token)
|
||||
|
||||
if wallet.wallet_type > 1:
|
||||
# If wallet type is not invoice then return the unauthorized status
|
||||
# This also covers when the user passes an invalid key type
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invoice (or Admin) key required.",
|
||||
)
|
||||
else:
|
||||
return wallet
|
||||
|
||||
|
||||
async def check_user_exists(usr: UUID4) -> User:
|
||||
g().user = await get_user(usr.hex)
|
||||
if not g().user:
|
||||
@@ -184,4 +211,7 @@ async def check_user_exists(usr: UUID4) -> User:
|
||||
status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized."
|
||||
)
|
||||
|
||||
if LNBITS_ADMIN_USERS and g().user.id in LNBITS_ADMIN_USERS:
|
||||
g().user.admin = True
|
||||
|
||||
return g().user
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
from . import db
|
||||
from .models import Copilots, CreateCopilotData
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
###############COPILOTS##########################
|
||||
|
||||
@@ -15,7 +16,7 @@ async def create_copilot(
|
||||
"""
|
||||
INSERT INTO copilot.newer_copilots (
|
||||
id,
|
||||
user,
|
||||
"user",
|
||||
lnurl_toggle,
|
||||
wallet,
|
||||
title,
|
||||
@@ -87,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
|
||||
|
||||
@@ -160,7 +160,7 @@ async def set_address_renewed(address_id: str, duration: int):
|
||||
|
||||
|
||||
async def check_address_available(username: str, domain: str):
|
||||
row, = await db.fetchone(
|
||||
(row,) = await db.fetchone(
|
||||
"SELECT COUNT(username) FROM lnaddress.address WHERE username = ? AND domain = ?",
|
||||
(username, domain),
|
||||
)
|
||||
|
||||
@@ -52,7 +52,7 @@ async def lnurl_callback(address_id, amount: int = Query(...)):
|
||||
amount_received = amount
|
||||
|
||||
domain = await get_domain(address.domain)
|
||||
|
||||
|
||||
base_url = (
|
||||
address.wallet_endpoint[:-1]
|
||||
if address.wallet_endpoint.endswith("/")
|
||||
@@ -71,7 +71,9 @@ async def lnurl_callback(address_id, amount: int = Query(...)):
|
||||
"out": False,
|
||||
"amount": int(amount_received / 1000),
|
||||
"description_hash": hashlib.sha256(
|
||||
(await address.lnurlpay_metadata(domain=domain.domain)).encode("utf-8")
|
||||
(await address.lnurlpay_metadata(domain=domain.domain)).encode(
|
||||
"utf-8"
|
||||
)
|
||||
).hexdigest(),
|
||||
"extra": {"tag": f"Payment to {address.username}@{domain.domain}"},
|
||||
},
|
||||
|
||||
@@ -53,5 +53,5 @@ class Addresses(BaseModel):
|
||||
text = f"Payment to {self.username}"
|
||||
identifier = f"{self.username}@{domain}"
|
||||
metadata = [["text/plain", text], ["text/identifier", identifier]]
|
||||
|
||||
|
||||
return LnurlPayMetadata(json.dumps(metadata))
|
||||
|
||||
@@ -36,7 +36,7 @@ async def display(domain_id, request: Request):
|
||||
|
||||
wallet = await get_wallet(domain.wallet)
|
||||
url = urlparse(str(request.url))
|
||||
|
||||
|
||||
return lnaddress_renderer().TemplateResponse(
|
||||
"lnaddress/display.html",
|
||||
{
|
||||
@@ -45,6 +45,6 @@ async def display(domain_id, request: Request):
|
||||
"domain_domain": domain.domain,
|
||||
"domain_cost": domain.cost,
|
||||
"domain_wallet_inkey": wallet.inkey,
|
||||
"root_url": f"{url.scheme}://{url.netloc}"
|
||||
"root_url": f"{url.scheme}://{url.netloc}",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
@@ -108,13 +109,17 @@ async def lndhub_payinvoice(
|
||||
|
||||
|
||||
@lndhub_ext.get("/ext/balance")
|
||||
async def lndhub_balance(wallet: WalletTypeInfo = Depends(check_wallet),):
|
||||
async def lndhub_balance(
|
||||
wallet: WalletTypeInfo = Depends(check_wallet),
|
||||
):
|
||||
return {"BTC": {"AvailableBalance": wallet.wallet.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,
|
||||
@@ -122,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,
|
||||
@@ -146,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 [
|
||||
{
|
||||
@@ -190,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))
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# LNURLDevice
|
||||
|
||||
For offline LNURL devices
|
||||
@@ -0,0 +1,17 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from lnbits.db import Database
|
||||
from lnbits.helpers import template_renderer
|
||||
|
||||
db = Database("ext_lnurldevice")
|
||||
|
||||
lnurldevice_ext: APIRouter = APIRouter(prefix="/lnurldevice", tags=["lnurldevice"])
|
||||
|
||||
|
||||
def lnurldevice_renderer():
|
||||
return template_renderer(["lnbits/extensions/lnurldevice/templates"])
|
||||
|
||||
|
||||
from .lnurl import * # noqa
|
||||
from .views import * # noqa
|
||||
from .views_api import * # noqa
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "LNURLDevice",
|
||||
"short_description": "For offline LNURL devices",
|
||||
"icon": "point_of_sale",
|
||||
"contributors": ["arcbtc"]
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
from . import db
|
||||
from .models import createLnurldevice, lnurldevicepayment, lnurldevices
|
||||
|
||||
###############lnurldeviceS##########################
|
||||
|
||||
|
||||
async def create_lnurldevice(
|
||||
data: createLnurldevice,
|
||||
) -> lnurldevices:
|
||||
lnurldevice_id = urlsafe_short_hash()
|
||||
lnurldevice_key = urlsafe_short_hash()
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO lnurldevice.lnurldevices (
|
||||
id,
|
||||
key,
|
||||
title,
|
||||
wallet,
|
||||
currency,
|
||||
device,
|
||||
profit
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
lnurldevice_id,
|
||||
lnurldevice_key,
|
||||
data.title,
|
||||
data.wallet,
|
||||
data.currency,
|
||||
data.device,
|
||||
data.profit,
|
||||
),
|
||||
)
|
||||
return await get_lnurldevice(lnurldevice_id)
|
||||
|
||||
|
||||
async def update_lnurldevice(lnurldevice_id: str, **kwargs) -> Optional[lnurldevices]:
|
||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||
await db.execute(
|
||||
f"UPDATE lnurldevice.lnurldevices SET {q} WHERE id = ?",
|
||||
(*kwargs.values(), lnurldevice_id),
|
||||
)
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM lnurldevice.lnurldevices WHERE id = ?", (lnurldevice_id,)
|
||||
)
|
||||
return lnurldevices(**row) if row else None
|
||||
|
||||
|
||||
async def get_lnurldevice(lnurldevice_id: str) -> lnurldevices:
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM lnurldevice.lnurldevices WHERE id = ?", (lnurldevice_id,)
|
||||
)
|
||||
return lnurldevices(**row) if row else None
|
||||
|
||||
|
||||
async def get_lnurldevices(wallet_ids: Union[str, List[str]]) -> List[lnurldevices]:
|
||||
wallet_ids = [wallet_ids]
|
||||
q = ",".join(["?"] * len(wallet_ids[0]))
|
||||
rows = await db.fetchall(
|
||||
f"""
|
||||
SELECT * FROM lnurldevice.lnurldevices WHERE wallet IN ({q})
|
||||
ORDER BY id
|
||||
""",
|
||||
(*wallet_ids,),
|
||||
)
|
||||
|
||||
return [lnurldevices(**row) if row else None for row in rows]
|
||||
|
||||
|
||||
async def delete_lnurldevice(lnurldevice_id: str) -> None:
|
||||
await db.execute(
|
||||
"DELETE FROM lnurldevice.lnurldevices WHERE id = ?", (lnurldevice_id,)
|
||||
)
|
||||
|
||||
########################lnuldevice payments###########################
|
||||
|
||||
|
||||
async def create_lnurldevicepayment(
|
||||
deviceid: str,
|
||||
payload: Optional[str] = None,
|
||||
pin: Optional[str] = None,
|
||||
payhash: Optional[str] = None,
|
||||
sats: Optional[int] = 0,
|
||||
) -> lnurldevicepayment:
|
||||
lnurldevicepayment_id = urlsafe_short_hash()
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO lnurldevice.lnurldevicepayment (
|
||||
id,
|
||||
deviceid,
|
||||
payload,
|
||||
pin,
|
||||
payhash,
|
||||
sats
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(lnurldevicepayment_id, deviceid, payload, pin, payhash, sats),
|
||||
)
|
||||
return await get_lnurldevicepayment(lnurldevicepayment_id)
|
||||
|
||||
|
||||
async def update_lnurldevicepayment(
|
||||
lnurldevicepayment_id: str, **kwargs
|
||||
) -> Optional[lnurldevicepayment]:
|
||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||
await db.execute(
|
||||
f"UPDATE lnurldevice.lnurldevicepayment SET {q} WHERE id = ?",
|
||||
(*kwargs.values(), lnurldevicepayment_id),
|
||||
)
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM lnurldevice.lnurldevicepayment WHERE id = ?",
|
||||
(lnurldevicepayment_id,),
|
||||
)
|
||||
return lnurldevicepayment(**row) if row else None
|
||||
|
||||
|
||||
async def get_lnurldevicepayment(lnurldevicepayment_id: str) -> lnurldevicepayment:
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM lnurldevice.lnurldevicepayment WHERE id = ?",
|
||||
(lnurldevicepayment_id,),
|
||||
)
|
||||
return lnurldevicepayment(**row) if row else None
|
||||
|
||||
|
||||
async def get_lnurlpayload(lnurldevicepayment_payload: str) -> lnurldevicepayment:
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM lnurldevice.lnurldevicepayment WHERE payload = ?",
|
||||
(lnurldevicepayment_payload,),
|
||||
)
|
||||
return lnurldevicepayment(**row) if row else None
|
||||
@@ -0,0 +1,231 @@
|
||||
import base64
|
||||
import hashlib
|
||||
from http import HTTPStatus
|
||||
from typing import Optional
|
||||
|
||||
from embit import bech32
|
||||
from embit import compact
|
||||
import base64
|
||||
from io import BytesIO
|
||||
import hmac
|
||||
|
||||
from fastapi import Request
|
||||
from fastapi.param_functions import Query
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from lnbits.core.services import create_invoice
|
||||
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
|
||||
from lnbits.core.views.api import pay_invoice
|
||||
|
||||
|
||||
from . import lnurldevice_ext
|
||||
from .crud import (
|
||||
create_lnurldevicepayment,
|
||||
get_lnurldevice,
|
||||
get_lnurldevicepayment,
|
||||
update_lnurldevicepayment,
|
||||
get_lnurlpayload,
|
||||
)
|
||||
|
||||
|
||||
def bech32_decode(bech):
|
||||
"""tweaked version of bech32_decode that ignores length limitations"""
|
||||
if (any(ord(x) < 33 or ord(x) > 126 for x in bech)) or (
|
||||
bech.lower() != bech and bech.upper() != bech
|
||||
):
|
||||
return
|
||||
bech = bech.lower()
|
||||
device = bech.rfind("1")
|
||||
if device < 1 or device + 7 > len(bech):
|
||||
return
|
||||
if not all(x in bech32.CHARSET for x in bech[device + 1 :]):
|
||||
return
|
||||
hrp = bech[:device]
|
||||
data = [bech32.CHARSET.find(x) for x in bech[device + 1 :]]
|
||||
encoding = bech32.bech32_verify_checksum(hrp, data)
|
||||
if encoding is None:
|
||||
return
|
||||
return bytes(bech32.convertbits(data[:-6], 5, 8, False))
|
||||
|
||||
|
||||
def xor_decrypt(key, blob):
|
||||
s = BytesIO(blob)
|
||||
variant = s.read(1)[0]
|
||||
if variant != 1:
|
||||
raise RuntimeError("Not implemented")
|
||||
# reading nonce
|
||||
l = s.read(1)[0]
|
||||
nonce = s.read(l)
|
||||
if len(nonce) != l:
|
||||
raise RuntimeError("Missing nonce bytes")
|
||||
if l < 8:
|
||||
raise RuntimeError("Nonce is too short")
|
||||
# reading payload
|
||||
l = s.read(1)[0]
|
||||
payload = s.read(l)
|
||||
if len(payload) > 32:
|
||||
raise RuntimeError("Payload is too long for this encryption method")
|
||||
if len(payload) != l:
|
||||
raise RuntimeError("Missing payload bytes")
|
||||
hmacval = s.read()
|
||||
expected = hmac.new(
|
||||
key, b"Data:" + blob[: -len(hmacval)], digestmod="sha256"
|
||||
).digest()
|
||||
if len(hmacval) < 8:
|
||||
raise RuntimeError("HMAC is too short")
|
||||
if hmacval != expected[: len(hmacval)]:
|
||||
raise RuntimeError("HMAC is invalid")
|
||||
secret = hmac.new(key, b"Round secret:" + nonce, digestmod="sha256").digest()
|
||||
payload = bytearray(payload)
|
||||
for i in range(len(payload)):
|
||||
payload[i] = payload[i] ^ secret[i]
|
||||
s = BytesIO(payload)
|
||||
pin = compact.read_from(s)
|
||||
amount_in_cent = compact.read_from(s)
|
||||
return pin, amount_in_cent
|
||||
|
||||
|
||||
@lnurldevice_ext.get(
|
||||
"/api/v1/lnurl/{device_id}",
|
||||
status_code=HTTPStatus.OK,
|
||||
name="lnurldevice.lnurl_v1_params",
|
||||
)
|
||||
async def lnurl_v1_params(
|
||||
request: Request,
|
||||
device_id: str = Query(None),
|
||||
p: str = Query(None),
|
||||
atm: str = Query(None),
|
||||
):
|
||||
device = await get_lnurldevice(device_id)
|
||||
if not device:
|
||||
return {
|
||||
"status": "ERROR",
|
||||
"reason": f"lnurldevice {device_id} not found on this server",
|
||||
}
|
||||
paymentcheck = await get_lnurlpayload(p)
|
||||
if device.device == "atm":
|
||||
if paymentcheck:
|
||||
return {"status": "ERROR", "reason": f"Payment already claimed"}
|
||||
|
||||
if len(p) % 4 > 0:
|
||||
p += "=" * (4 - (len(p) % 4))
|
||||
|
||||
data = base64.urlsafe_b64decode(p)
|
||||
pin = 0
|
||||
amount_in_cent = 0
|
||||
try:
|
||||
result = xor_decrypt(device.key.encode(), data)
|
||||
pin = result[0]
|
||||
amount_in_cent = result[1]
|
||||
except Exception as exc:
|
||||
return {"status": "ERROR", "reason": str(exc)}
|
||||
|
||||
price_msat = (
|
||||
await fiat_amount_as_satoshis(float(amount_in_cent) / 100, device.currency)
|
||||
if device.currency != "sat"
|
||||
else amount_in_cent
|
||||
) * 1000
|
||||
|
||||
if atm:
|
||||
if device.device != "atm":
|
||||
return {"status": "ERROR", "reason": "Not ATM device."}
|
||||
price_msat = int(price_msat * (1 - (device.profit / 100)) / 1000)
|
||||
lnurldevicepayment = await create_lnurldevicepayment(
|
||||
deviceid=device.id,
|
||||
payload=p,
|
||||
sats=price_msat * 1000,
|
||||
pin=pin,
|
||||
payhash="payment_hash",
|
||||
)
|
||||
if not lnurldevicepayment:
|
||||
return {"status": "ERROR", "reason": "Could not create payment."}
|
||||
return {
|
||||
"tag": "withdrawRequest",
|
||||
"callback": request.url_for(
|
||||
"lnurldevice.lnurl_callback", paymentid=lnurldevicepayment.id
|
||||
),
|
||||
"k1": lnurldevicepayment.id,
|
||||
"minWithdrawable": price_msat * 1000,
|
||||
"maxWithdrawable": price_msat * 1000,
|
||||
"defaultDescription": device.title,
|
||||
}
|
||||
price_msat = int(price_msat * ((device.profit / 100) + 1) / 1000)
|
||||
print(price_msat)
|
||||
lnurldevicepayment = await create_lnurldevicepayment(
|
||||
deviceid=device.id,
|
||||
payload=p,
|
||||
sats=price_msat * 1000,
|
||||
pin=pin,
|
||||
payhash="payment_hash",
|
||||
)
|
||||
if not lnurldevicepayment:
|
||||
return {"status": "ERROR", "reason": "Could not create payment."}
|
||||
return {
|
||||
"tag": "payRequest",
|
||||
"callback": request.url_for(
|
||||
"lnurldevice.lnurl_callback", paymentid=lnurldevicepayment.id
|
||||
),
|
||||
"minSendable": price_msat * 1000,
|
||||
"maxSendable": price_msat * 1000,
|
||||
"metadata": await device.lnurlpay_metadata(),
|
||||
}
|
||||
|
||||
|
||||
@lnurldevice_ext.get(
|
||||
"/api/v1/lnurl/cb/{paymentid}",
|
||||
status_code=HTTPStatus.OK,
|
||||
name="lnurldevice.lnurl_callback",
|
||||
)
|
||||
async def lnurl_callback(
|
||||
request: Request,
|
||||
paymentid: str = Query(None),
|
||||
pr: str = Query(None),
|
||||
k1: str = Query(None),
|
||||
):
|
||||
lnurldevicepayment = await get_lnurldevicepayment(paymentid)
|
||||
device = await get_lnurldevice(lnurldevicepayment.deviceid)
|
||||
if not device:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN, detail="lnurldevice not found."
|
||||
)
|
||||
if pr:
|
||||
if lnurldevicepayment.id != k1:
|
||||
return {"status": "ERROR", "reason": "Bad K1"}
|
||||
if lnurldevicepayment.payhash != "payment_hash":
|
||||
return {"status": "ERROR", "reason": f"Payment already claimed"}
|
||||
lnurldevicepayment = await update_lnurldevicepayment(
|
||||
lnurldevicepayment_id=paymentid, payhash=lnurldevicepayment.payload
|
||||
)
|
||||
|
||||
await pay_invoice(
|
||||
wallet_id=device.wallet,
|
||||
payment_request=pr,
|
||||
max_sat=lnurldevicepayment.sats / 1000,
|
||||
extra={"tag": "withdraw"},
|
||||
)
|
||||
return {"status": "OK"}
|
||||
print(lnurldevicepayment.sats)
|
||||
payment_hash, payment_request = await create_invoice(
|
||||
wallet_id=device.wallet,
|
||||
amount=lnurldevicepayment.sats / 1000,
|
||||
memo=device.title,
|
||||
description_hash=hashlib.sha256(
|
||||
(await device.lnurlpay_metadata()).encode("utf-8")
|
||||
).digest(),
|
||||
extra={"tag": "PoS"},
|
||||
)
|
||||
lnurldevicepayment = await update_lnurldevicepayment(
|
||||
lnurldevicepayment_id=paymentid, payhash=payment_hash
|
||||
)
|
||||
|
||||
return {
|
||||
"pr": payment_request,
|
||||
"successAction": {
|
||||
"tag": "url",
|
||||
"description": "Check the attached link",
|
||||
"url": request.url_for("lnurldevice.displaypin", paymentid=paymentid),
|
||||
},
|
||||
"routes": [],
|
||||
}
|
||||
|
||||
return resp.dict()
|
||||
@@ -0,0 +1,81 @@
|
||||
from lnbits.db import Database
|
||||
|
||||
db2 = Database("ext_lnurlpos")
|
||||
|
||||
|
||||
async def m001_initial(db):
|
||||
"""
|
||||
Initial lnurldevice table.
|
||||
"""
|
||||
await db.execute(
|
||||
f"""
|
||||
CREATE TABLE lnurldevice.lnurldevices (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
key TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
wallet TEXT NOT NULL,
|
||||
currency TEXT NOT NULL,
|
||||
device TEXT NOT NULL,
|
||||
profit FLOAT NOT NULL,
|
||||
timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
||||
);
|
||||
"""
|
||||
)
|
||||
await db.execute(
|
||||
f"""
|
||||
CREATE TABLE lnurldevice.lnurldevicepayment (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
deviceid TEXT NOT NULL,
|
||||
payhash TEXT,
|
||||
payload TEXT NOT NULL,
|
||||
pin INT,
|
||||
sats INT,
|
||||
timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
async def m002_redux(db):
|
||||
"""
|
||||
Moves everything from lnurlpos to lnurldevices
|
||||
"""
|
||||
try:
|
||||
for row in [
|
||||
list(row) for row in await db2.fetchall("SELECT * FROM lnurlpos.lnurlposs")
|
||||
]:
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO lnurldevice.lnurldevices (
|
||||
id,
|
||||
key,
|
||||
title,
|
||||
wallet,
|
||||
currency,
|
||||
device,
|
||||
profit
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(row[0], row[1], row[2], row[3], row[4], "pos", 0),
|
||||
)
|
||||
for row in [
|
||||
list(row)
|
||||
for row in await db2.fetchall("SELECT * FROM lnurlpos.lnurlpospayment")
|
||||
]:
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO lnurldevice.lnurldevicepayment (
|
||||
id,
|
||||
deviceid,
|
||||
payhash,
|
||||
payload,
|
||||
pin,
|
||||
sats
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(row[0], row[1], row[3], row[4], row[5], row[6]),
|
||||
)
|
||||
except:
|
||||
return
|
||||
@@ -11,33 +11,40 @@ from pydantic import BaseModel
|
||||
from pydantic.main import BaseModel
|
||||
|
||||
|
||||
class createLnurlpos(BaseModel):
|
||||
class createLnurldevice(BaseModel):
|
||||
title: str
|
||||
wallet: str
|
||||
currency: str
|
||||
device: str
|
||||
profit: float
|
||||
|
||||
|
||||
class lnurlposs(BaseModel):
|
||||
class lnurldevices(BaseModel):
|
||||
id: str
|
||||
key: str
|
||||
title: str
|
||||
wallet: str
|
||||
currency: str
|
||||
device: str
|
||||
profit: float
|
||||
timestamp: str
|
||||
|
||||
def from_row(cls, row: Row) -> "lnurlposs":
|
||||
def from_row(cls, row: Row) -> "lnurldevices":
|
||||
return cls(**dict(row))
|
||||
|
||||
def lnurl(self, req: Request) -> Lnurl:
|
||||
url = req.url_for("lnurlpos.lnurl_response", pos_id=self.id, _external=True)
|
||||
url = req.url_for(
|
||||
"lnurldevice.lnurl_response", device_id=self.id, _external=True
|
||||
)
|
||||
return lnurl_encode(url)
|
||||
|
||||
async def lnurlpay_metadata(self) -> LnurlPayMetadata:
|
||||
return LnurlPayMetadata(json.dumps([["text/plain", self.title]]))
|
||||
|
||||
class lnurlpospayment(BaseModel):
|
||||
|
||||
class lnurldevicepayment(BaseModel):
|
||||
id: str
|
||||
posid: str
|
||||
deviceid: str
|
||||
payhash: str
|
||||
payload: str
|
||||
pin: int
|
||||
@@ -45,5 +52,5 @@ class lnurlpospayment(BaseModel):
|
||||
timestamp: str
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "lnurlpospayment":
|
||||
def from_row(cls, row: Row) -> "lnurldevicepayment":
|
||||
return cls(**dict(row))
|
||||
+35
-24
@@ -1,10 +1,10 @@
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<p>
|
||||
Register LNURLPoS devices to receive payments in your LNbits wallet.<br />
|
||||
Register LNURLDevice devices to receive payments in your LNbits wallet.<br />
|
||||
Build your own here
|
||||
<a href="https://github.com/arcbtc/LNURLPoS"
|
||||
>https://github.com/arcbtc/LNURLPoS</a
|
||||
<a href="https://github.com/arcbtc/bitcoinpos"
|
||||
>https://github.com/arcbtc/bitcoinpos</a
|
||||
><br />
|
||||
<small>
|
||||
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
|
||||
@@ -21,12 +21,12 @@
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Create lnurlpos"
|
||||
label="Create lnurldevice"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-blue">POST</span> /lnurlpos/api/v1/lnurlpos</code
|
||||
><span class="text-blue"></span> /lnurldevice/api/v1/lnurlpos</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||
@@ -36,10 +36,10 @@
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
<code>[<lnurlpos_object>, ...]</code>
|
||||
<code>[<lnurldevice_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X POST {{ request.base_url }}api/v1/lnurlpos -d '{"title":
|
||||
>curl -X POST {{ request.base_url }}api/v1/lnurldevice -d '{"title":
|
||||
<string>, "message":<string>, "currency":
|
||||
<integer>}' -H "Content-type: application/json" -H "X-Api-Key:
|
||||
{{user.wallets[0].adminkey }}"
|
||||
@@ -51,13 +51,13 @@
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Update lnurlpos"
|
||||
label="Update lnurldevice"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-blue">PUT</span>
|
||||
/lnurlpos/api/v1/lnurlpos/<lnurlpos_id></code
|
||||
/lnurldevice/api/v1/lnurlpos/<lnurldevice_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||
@@ -67,25 +67,30 @@
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
<code>[<lnurlpos_object>, ...]</code>
|
||||
<code>[<lnurldevice_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X POST {{ request.base_url
|
||||
}}api/v1/lnurlpos/<lnurlpos_id> -d ''{"title": <string>,
|
||||
"message":<string>, "currency": <integer>} -H
|
||||
"Content-type: application/json" -H "X-Api-Key:
|
||||
}}api/v1/lnurlpos/<lnurldevice_id> -d ''{"title":
|
||||
<string>, "message":<string>, "currency":
|
||||
<integer>} -H "Content-type: application/json" -H "X-Api-Key:
|
||||
{{user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
|
||||
<q-expansion-item group="api" dense expand-separator label="Get lnurlpos">
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Get lnurldevice"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-blue">GET</span>
|
||||
/lnurlpos/api/v1/lnurlpos/<lnurlpos_id></code
|
||||
/lnurldevice/api/v1/lnurlpos/<lnurldevice_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
||||
@@ -95,21 +100,27 @@
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
<code>[<lnurlpos_object>, ...]</code>
|
||||
<code>[<lnurldevice_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.base_url
|
||||
}}api/v1/lnurlpos/<lnurlpos_id> -H "X-Api-Key: {{
|
||||
}}api/v1/lnurlpos/<lnurldevice_id> -H "X-Api-Key: {{
|
||||
user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="Get lnurlposs">
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Get lnurldevices"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-blue">GET</span> /lnurlpos/api/v1/lnurlposs</code
|
||||
><span class="text-blue">GET</span>
|
||||
/lnurldevice/api/v1/lnurlposs</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
||||
@@ -119,11 +130,11 @@
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
<code>[<lnurlpos_object>, ...]</code>
|
||||
<code>[<lnurldevice_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.base_url }}api/v1/lnurlposs -H "X-Api-Key:
|
||||
{{ user.wallets[0].inkey }}"
|
||||
>curl -X GET {{ request.base_url }}api/v1/lnurldevices -H
|
||||
"X-Api-Key: {{ user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
@@ -139,7 +150,7 @@
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-pink">DELETE</span>
|
||||
/lnurlpos/api/v1/lnurlpos/<lnurlpos_id></code
|
||||
/lnurldevice/api/v1/lnurlpos/<lnurldevice_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||
@@ -148,7 +159,7 @@
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X DELETE {{ request.base_url
|
||||
}}api/v1/lnurlpos/<lnurlpos_id> -H "X-Api-Key: {{
|
||||
}}api/v1/lnurlpos/<lnurldevice_id> -H "X-Api-Key: {{
|
||||
user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
+148
-88
@@ -8,8 +8,8 @@
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
@click="formDialoglnurlpos.show = true"
|
||||
>New LNURLPoS instance
|
||||
@click="formDialoglnurldevice.show = true"
|
||||
>New LNURLDevice instance
|
||||
</q-btn>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
@@ -18,7 +18,7 @@
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none">lNURLPoS</h5>
|
||||
<h5 class="text-subtitle1 q-my-none">lNURLdevice</h5>
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
@@ -33,7 +33,7 @@
|
||||
<q-icon name="search"></q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
<q-btn flat color="grey" @click="exportlnurlposCSV"
|
||||
<q-btn flat color="grey" @click="exportlnurldeviceCSV"
|
||||
>Export to CSV</q-btn
|
||||
>
|
||||
</div>
|
||||
@@ -41,10 +41,10 @@
|
||||
<q-table
|
||||
flat
|
||||
dense
|
||||
:data="lnurlposLinks"
|
||||
:data="lnurldeviceLinks"
|
||||
row-key="id"
|
||||
:columns="lnurlpossTable.columns"
|
||||
:pagination.sync="lnurlpossTable.pagination"
|
||||
:columns="lnurldevicesTable.columns"
|
||||
:pagination.sync="lnurldevicesTable.pagination"
|
||||
:filter="filter"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
@@ -72,11 +72,11 @@
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="deletelnurlposLink(props.row.id)"
|
||||
@click="deletelnurldeviceLink(props.row.id)"
|
||||
icon="cancel"
|
||||
color="pink"
|
||||
>
|
||||
<q-tooltip> Delete LNURLPoS </q-tooltip>
|
||||
<q-tooltip> Delete LNURLDevice </q-tooltip>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
<q-td>
|
||||
@@ -84,11 +84,11 @@
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="openlnurlposSettings(props.row.id)"
|
||||
@click="openlnurldeviceSettings(props.row.id)"
|
||||
icon="perm_data_setting"
|
||||
color="primary"
|
||||
>
|
||||
<q-tooltip> LNURLPoS Settings </q-tooltip>
|
||||
<q-tooltip> LNURLDevice Settings </q-tooltip>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
<q-td
|
||||
@@ -112,46 +112,57 @@
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-my-none">
|
||||
{{SITE_TITLE}} LNURLPoS Extension
|
||||
{{SITE_TITLE}} LNURLDevice Extension
|
||||
</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list> {% include "lnurlpos/_api_docs.html" %} </q-list>
|
||||
<q-list> {% include "lnurldevice/_api_docs.html" %} </q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<q-dialog
|
||||
v-model="settingsDialog.show"
|
||||
position="top"
|
||||
deviceition="top"
|
||||
@hide="closeFormDialog"
|
||||
>
|
||||
<q-card
|
||||
style="width: 700px; max-width: 80vw"
|
||||
class="q-pa-lg q-pt-xl lnbits__dialog-card"
|
||||
>
|
||||
<div class="text-h6">Copy to LNURLPoS device</div>
|
||||
<div class="text-h6">LNURLDevice device string</div>
|
||||
<q-btn
|
||||
dense
|
||||
outline
|
||||
unelevated
|
||||
color="primary"
|
||||
size="md"
|
||||
@click="copyText(location + '/lnurldevice/api/v1/lnurl/' + settingsDialog.data.id + ',' +
|
||||
settingsDialog.data.key + ',' + settingsDialog.data.currency, 'Link copied to clipboard!')"
|
||||
>{% raw
|
||||
%}{{location}}/lnurldevice/api/v1/lnurl/{{settingsDialog.data.id}},
|
||||
{{settingsDialog.data.key}}, {{settingsDialog.data.currency}}{% endraw
|
||||
%}<q-tooltip> Click to copy URL </q-tooltip>
|
||||
</q-btn>
|
||||
|
||||
<div class="text-subtitle2">
|
||||
{% raw %} String baseURL =
|
||||
"{{location}}/lnurlpos/api/v1/lnurl/{{settingsDialog.data.id}}";<br />
|
||||
String key = "{{settingsDialog.data.key}}";<br />
|
||||
String currency = "{{settingsDialog.data.currency}}";{% endraw %}
|
||||
<small> </small>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog
|
||||
v-model="formDialoglnurlpos.show"
|
||||
position="top"
|
||||
v-model="formDialoglnurldevice.show"
|
||||
deviceition="top"
|
||||
@hide="closeFormDialog"
|
||||
>
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form @submit="sendFormDatalnurlpos" class="q-gutter-md">
|
||||
<q-form @submit="sendFormDatalnurldevice" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialoglnurlpos.data.title"
|
||||
v-model.trim="formDialoglnurldevice.data.title"
|
||||
type="text"
|
||||
label="Title"
|
||||
></q-input>
|
||||
@@ -160,7 +171,7 @@
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="formDialoglnurlpos.data.wallet"
|
||||
v-model="formDialoglnurldevice.data.wallet"
|
||||
:options="g.user.walletOptions"
|
||||
label="Wallet *"
|
||||
></q-select>
|
||||
@@ -168,32 +179,46 @@
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialoglnurlpos.data.currency"
|
||||
v-model.trim="formDialoglnurldevice.data.currency"
|
||||
type="text"
|
||||
label="Fiat currency for PoS"
|
||||
label="Fiat currency for device"
|
||||
:options="currency"
|
||||
></q-select>
|
||||
<q-option-group
|
||||
v-model.trim="formDialoglnurldevice.data.device"
|
||||
:options="devices"
|
||||
color="primary"
|
||||
label="Type of device"
|
||||
></q-option-group>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialoglnurldevice.data.profit"
|
||||
type="number"
|
||||
max="90"
|
||||
label="Profit margin (% added to invoices/deducted from faucets)"
|
||||
></q-input>
|
||||
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
v-if="formDialoglnurlpos.data.id"
|
||||
v-if="formDialoglnurldevice.data.id"
|
||||
unelevated
|
||||
color="primary"
|
||||
:disable="
|
||||
formDialoglnurlpos.data.title == ''"
|
||||
formDialoglnurldevice.data.title == ''"
|
||||
type="submit"
|
||||
>Update lnurlpos</q-btn
|
||||
>Update lnurldevice</q-btn
|
||||
>
|
||||
<q-btn
|
||||
v-else
|
||||
unelevated
|
||||
color="primary"
|
||||
:disable="
|
||||
formDialoglnurlpos.data.title == ''"
|
||||
formDialoglnurldevice.data.title == ''"
|
||||
type="submit"
|
||||
>Create lnurlpos</q-btn
|
||||
>Create lnurldevice</q-btn
|
||||
>
|
||||
<q-btn @click="cancellnurlpos" flat color="grey" class="q-ml-auto"
|
||||
<q-btn @click="cancellnurldevice" flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
@@ -206,7 +231,7 @@
|
||||
<script>
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
|
||||
var maplnurlpos = obj => {
|
||||
var maplnurldevice = obj => {
|
||||
obj._data = _.clone(obj)
|
||||
obj.theTime = obj.time * 60 - (Date.now() / 1000 - obj.timestamp)
|
||||
obj.time = obj.time + 'mins'
|
||||
@@ -230,9 +255,19 @@
|
||||
location: window.location.hostname,
|
||||
filter: '',
|
||||
currency: 'USD',
|
||||
lnurlposLinks: [],
|
||||
lnurlposLinksObj: [],
|
||||
lnurlpossTable: {
|
||||
lnurldeviceLinks: [],
|
||||
lnurldeviceLinksObj: [],
|
||||
devices: [
|
||||
{
|
||||
label: 'PoS',
|
||||
value: 'pos'
|
||||
},
|
||||
{
|
||||
label: 'ATM',
|
||||
value: 'atm'
|
||||
}
|
||||
],
|
||||
lnurldevicesTable: {
|
||||
columns: [
|
||||
{
|
||||
name: 'title',
|
||||
@@ -258,6 +293,18 @@
|
||||
label: 'wallet',
|
||||
field: 'wallet'
|
||||
},
|
||||
{
|
||||
name: 'device',
|
||||
align: 'left',
|
||||
label: 'device',
|
||||
field: 'device'
|
||||
},
|
||||
{
|
||||
name: 'profit',
|
||||
align: 'left',
|
||||
label: 'profit',
|
||||
field: 'profit'
|
||||
},
|
||||
{
|
||||
name: 'currency',
|
||||
align: 'left',
|
||||
@@ -269,7 +316,7 @@
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
passedlnurlpos: {},
|
||||
passedlnurldevice: {},
|
||||
settingsDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
@@ -278,13 +325,15 @@
|
||||
show: false,
|
||||
data: {}
|
||||
},
|
||||
formDialoglnurlpos: {
|
||||
formDialoglnurldevice: {
|
||||
show: false,
|
||||
data: {
|
||||
lnurl_toggle: false,
|
||||
show_message: false,
|
||||
show_ack: false,
|
||||
show_price: 'None',
|
||||
device: 'pos',
|
||||
profit: 2,
|
||||
title: ''
|
||||
}
|
||||
},
|
||||
@@ -295,33 +344,33 @@
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cancellnurlpos: function (data) {
|
||||
cancellnurldevice: function (data) {
|
||||
var self = this
|
||||
self.formDialoglnurlpos.show = false
|
||||
self.clearFormDialoglnurlpos()
|
||||
self.formDialoglnurldevice.show = false
|
||||
self.clearFormDialoglnurldevice()
|
||||
},
|
||||
closeFormDialog: function () {
|
||||
this.clearFormDialoglnurlpos()
|
||||
this.clearFormDialoglnurldevice()
|
||||
this.formDialog.data = {
|
||||
is_unique: false
|
||||
}
|
||||
},
|
||||
sendFormDatalnurlpos: function () {
|
||||
sendFormDatalnurldevice: function () {
|
||||
var self = this
|
||||
if (self.formDialoglnurlpos.data.id) {
|
||||
this.updatelnurlpos(
|
||||
if (self.formDialoglnurldevice.data.id) {
|
||||
this.updatelnurldevice(
|
||||
self.g.user.wallets[0].adminkey,
|
||||
self.formDialoglnurlpos.data
|
||||
self.formDialoglnurldevice.data
|
||||
)
|
||||
} else {
|
||||
this.createlnurlpos(
|
||||
this.createlnurldevice(
|
||||
self.g.user.wallets[0].adminkey,
|
||||
self.formDialoglnurlpos.data
|
||||
self.formDialoglnurldevice.data
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
createlnurlpos: function (wallet, data) {
|
||||
createlnurldevice: function (wallet, data) {
|
||||
var self = this
|
||||
var updatedData = {}
|
||||
for (const property in data) {
|
||||
@@ -330,86 +379,91 @@
|
||||
}
|
||||
}
|
||||
LNbits.api
|
||||
.request('POST', '/lnurlpos/api/v1/lnurlpos', wallet, updatedData)
|
||||
.request('POST', '/lnurldevice/api/v1/lnurlpos', wallet, updatedData)
|
||||
.then(function (response) {
|
||||
self.lnurlposLinks.push(maplnurlpos(response.data))
|
||||
self.formDialoglnurlpos.show = false
|
||||
self.clearFormDialoglnurlpos()
|
||||
self.lnurldeviceLinks.push(maplnurldevice(response.data))
|
||||
self.formDialoglnurldevice.show = false
|
||||
self.clearFormDialoglnurldevice()
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
getlnurlposs: function () {
|
||||
getlnurldevices: function () {
|
||||
var self = this
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/lnurlpos/api/v1/lnurlpos',
|
||||
'/lnurldevice/api/v1/lnurlpos',
|
||||
self.g.user.wallets[0].adminkey
|
||||
)
|
||||
.then(function (response) {
|
||||
if (response.data) {
|
||||
self.lnurlposLinks = response.data.map(maplnurlpos)
|
||||
self.lnurldeviceLinks = response.data.map(maplnurldevice)
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
getlnurlpos: function (lnurlpos_id) {
|
||||
getlnurldevice: function (lnurldevice_id) {
|
||||
var self = this
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/lnurlpos/api/v1/lnurlpos/' + lnurlpos_id,
|
||||
'/lnurldevice/api/v1/lnurlpos/' + lnurldevice_id,
|
||||
self.g.user.wallets[0].adminkey
|
||||
)
|
||||
.then(function (response) {
|
||||
localStorage.setItem('lnurlpos', JSON.stringify(response.data))
|
||||
localStorage.setItem('lnurldevice', JSON.stringify(response.data))
|
||||
localStorage.setItem('inkey', self.g.user.wallets[0].inkey)
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
deletelnurlposLink: function (lnurlposId) {
|
||||
deletelnurldeviceLink: function (lnurldeviceId) {
|
||||
var self = this
|
||||
var link = _.findWhere(this.lnurlposLinks, {id: lnurlposId})
|
||||
var link = _.findWhere(this.lnurldeviceLinks, {id: lnurldeviceId})
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this pay link?')
|
||||
.onOk(function () {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/lnurlpos/api/v1/lnurlpos/' + lnurlposId,
|
||||
'/lnurldevice/api/v1/lnurlpos/' + lnurldeviceId,
|
||||
self.g.user.wallets[0].adminkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.lnurlposLinks = _.reject(self.lnurlposLinks, function (
|
||||
obj
|
||||
) {
|
||||
return obj.id === lnurlposId
|
||||
})
|
||||
self.lnurldeviceLinks = _.reject(
|
||||
self.lnurldeviceLinks,
|
||||
function (obj) {
|
||||
return obj.id === lnurldeviceId
|
||||
}
|
||||
)
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
openUpdatelnurlposLink: function (lnurlposId) {
|
||||
openUpdatelnurldeviceLink: function (lnurldeviceId) {
|
||||
var self = this
|
||||
var lnurlpos = _.findWhere(this.lnurlposLinks, {id: lnurlposId})
|
||||
self.formDialoglnurlpos.data = _.clone(lnurlpos._data)
|
||||
self.formDialoglnurlpos.show = true
|
||||
var lnurldevice = _.findWhere(this.lnurldeviceLinks, {
|
||||
id: lnurldeviceId
|
||||
})
|
||||
self.formDialoglnurldevice.data = _.clone(lnurldevice._data)
|
||||
self.formDialoglnurldevice.show = true
|
||||
},
|
||||
openlnurlposSettings: function (lnurlposId) {
|
||||
openlnurldeviceSettings: function (lnurldeviceId) {
|
||||
var self = this
|
||||
var lnurlpos = _.findWhere(this.lnurlposLinks, {id: lnurlposId})
|
||||
self.settingsDialog.data = _.clone(lnurlpos._data)
|
||||
var lnurldevice = _.findWhere(this.lnurldeviceLinks, {
|
||||
id: lnurldeviceId
|
||||
})
|
||||
self.settingsDialog.data = _.clone(lnurldevice._data)
|
||||
self.settingsDialog.show = true
|
||||
},
|
||||
updatelnurlpos: function (wallet, data) {
|
||||
updatelnurldevice: function (wallet, data) {
|
||||
var self = this
|
||||
var updatedData = {}
|
||||
for (const property in data) {
|
||||
@@ -421,24 +475,27 @@
|
||||
LNbits.api
|
||||
.request(
|
||||
'PUT',
|
||||
'/lnurlpos/api/v1/lnurlpos/' + updatedData.id,
|
||||
'/lnurldevice/api/v1/lnurlpos/' + updatedData.id,
|
||||
wallet,
|
||||
updatedData
|
||||
)
|
||||
.then(function (response) {
|
||||
self.lnurlposLinks = _.reject(self.lnurlposLinks, function (obj) {
|
||||
return obj.id === updatedData.id
|
||||
})
|
||||
self.lnurlposLinks.push(maplnurlpos(response.data))
|
||||
self.formDialoglnurlpos.show = false
|
||||
self.clearFormDialoglnurlpos()
|
||||
self.lnurldeviceLinks = _.reject(
|
||||
self.lnurldeviceLinks,
|
||||
function (obj) {
|
||||
return obj.id === updatedData.id
|
||||
}
|
||||
)
|
||||
self.lnurldeviceLinks.push(maplnurldevice(response.data))
|
||||
self.formDialoglnurldevice.show = false
|
||||
self.clearFormDialoglnurldevice()
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
clearFormDialoglnurlpos() {
|
||||
this.formDialoglnurlpos.data = {
|
||||
clearFormDialoglnurldevice() {
|
||||
this.formDialoglnurldevice.data = {
|
||||
lnurl_toggle: false,
|
||||
show_message: false,
|
||||
show_ack: false,
|
||||
@@ -446,15 +503,18 @@
|
||||
title: ''
|
||||
}
|
||||
},
|
||||
exportlnurlposCSV: function () {
|
||||
exportlnurldeviceCSV: function () {
|
||||
var self = this
|
||||
LNbits.utils.exportCSV(self.lnurlpossTable.columns, this.lnurlposLinks)
|
||||
LNbits.utils.exportCSV(
|
||||
self.lnurldevicesTable.columns,
|
||||
this.lnurldeviceLinks
|
||||
)
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
var self = this
|
||||
var getlnurlposs = this.getlnurlposs
|
||||
getlnurlposs()
|
||||
var getlnurldevices = this.getlnurldevices
|
||||
getlnurldevices()
|
||||
self.location = [
|
||||
window.location.protocol,
|
||||
'//',
|
||||
@@ -0,0 +1,53 @@
|
||||
from http import HTTPStatus
|
||||
|
||||
from fastapi import Request
|
||||
from fastapi.param_functions import Query
|
||||
from fastapi.params import Depends
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.responses import HTMLResponse
|
||||
|
||||
from lnbits.core.crud import update_payment_status
|
||||
from lnbits.core.models import User
|
||||
from lnbits.core.views.api import api_payment
|
||||
from lnbits.decorators import check_user_exists
|
||||
|
||||
from . import lnurldevice_ext, lnurldevice_renderer
|
||||
from .crud import get_lnurldevice, get_lnurldevicepayment
|
||||
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
|
||||
@lnurldevice_ext.get("/", response_class=HTMLResponse)
|
||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||
return lnurldevice_renderer().TemplateResponse(
|
||||
"lnurldevice/index.html", {"request": request, "user": user.dict()}
|
||||
)
|
||||
|
||||
|
||||
@lnurldevice_ext.get(
|
||||
"/{paymentid}", name="lnurldevice.displaypin", response_class=HTMLResponse
|
||||
)
|
||||
async def displaypin(request: Request, paymentid: str = Query(None)):
|
||||
lnurldevicepayment = await get_lnurldevicepayment(paymentid)
|
||||
if not lnurldevicepayment:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="No lmurldevice payment"
|
||||
)
|
||||
device = await get_lnurldevice(lnurldevicepayment.deviceid)
|
||||
if not device:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="lnurldevice not found."
|
||||
)
|
||||
status = await api_payment(lnurldevicepayment.payhash)
|
||||
if status["paid"]:
|
||||
await update_payment_status(
|
||||
checking_id=lnurldevicepayment.payhash, pending=True
|
||||
)
|
||||
return lnurldevice_renderer().TemplateResponse(
|
||||
"lnurldevice/paid.html", {"request": request, "pin": lnurldevicepayment.pin}
|
||||
)
|
||||
return lnurldevice_renderer().TemplateResponse(
|
||||
"lnurldevice/error.html",
|
||||
{"request": request, "pin": "filler", "not_paid": True},
|
||||
)
|
||||
@@ -0,0 +1,88 @@
|
||||
from http import HTTPStatus
|
||||
|
||||
from fastapi import Request
|
||||
from fastapi.param_functions import Query
|
||||
from fastapi.params import Depends
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from lnbits.core.crud import get_user
|
||||
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
||||
from lnbits.extensions.lnurldevice import lnurldevice_ext
|
||||
from lnbits.utils.exchange_rates import currencies
|
||||
|
||||
from . import lnurldevice_ext
|
||||
from .crud import (
|
||||
create_lnurldevice,
|
||||
delete_lnurldevice,
|
||||
get_lnurldevice,
|
||||
get_lnurldevices,
|
||||
update_lnurldevice,
|
||||
)
|
||||
from .models import createLnurldevice
|
||||
|
||||
|
||||
@lnurldevice_ext.get("/api/v1/currencies")
|
||||
async def api_list_currencies_available():
|
||||
return list(currencies.keys())
|
||||
|
||||
|
||||
#######################lnurldevice##########################
|
||||
|
||||
|
||||
@lnurldevice_ext.post("/api/v1/lnurlpos")
|
||||
@lnurldevice_ext.put("/api/v1/lnurlpos/{lnurldevice_id}")
|
||||
async def api_lnurldevice_create_or_update(
|
||||
data: createLnurldevice,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
lnurldevice_id: str = Query(None),
|
||||
):
|
||||
if not lnurldevice_id:
|
||||
lnurldevice = await create_lnurldevice(data)
|
||||
return lnurldevice.dict()
|
||||
else:
|
||||
lnurldevice = await update_lnurldevice(data, lnurldevice_id=lnurldevice_id)
|
||||
return lnurldevice.dict()
|
||||
|
||||
|
||||
@lnurldevice_ext.get("/api/v1/lnurlpos")
|
||||
async def api_lnurldevices_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
|
||||
try:
|
||||
return [
|
||||
{**lnurldevice.dict()} for lnurldevice in await get_lnurldevices(wallet_ids)
|
||||
]
|
||||
except:
|
||||
return ""
|
||||
|
||||
|
||||
@lnurldevice_ext.get("/api/v1/lnurlpos/{lnurldevice_id}")
|
||||
async def api_lnurldevice_retrieve(
|
||||
request: Request,
|
||||
wallet: WalletTypeInfo = Depends(get_key_type),
|
||||
lnurldevice_id: str = Query(None),
|
||||
):
|
||||
lnurldevice = await get_lnurldevice(lnurldevice_id)
|
||||
if not lnurldevice:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="lnurldevice does not exist"
|
||||
)
|
||||
if not lnurldevice.lnurl_toggle:
|
||||
return {**lnurldevice.dict()}
|
||||
return {**lnurldevice.dict(), **{"lnurl": lnurldevice.lnurl(request=request)}}
|
||||
|
||||
|
||||
@lnurldevice_ext.delete("/api/v1/lnurlpos/{lnurldevice_id}")
|
||||
async def api_lnurldevice_delete(
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
lnurldevice_id: str = Query(None),
|
||||
):
|
||||
lnurldevice = await get_lnurldevice(lnurldevice_id)
|
||||
|
||||
if not lnurldevice:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Wallet link does not exist."
|
||||
)
|
||||
|
||||
await delete_lnurldevice(lnurldevice_id)
|
||||
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
@@ -66,7 +66,7 @@ async def api_link_retrieve(
|
||||
detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN
|
||||
)
|
||||
|
||||
return {**link._asdict(), **{"lnurl": link.lnurl(r)}}
|
||||
return {**link.dict(), **{"lnurl": link.lnurl(r)}}
|
||||
|
||||
|
||||
@lnurlp_ext.post("/api/v1/links", status_code=HTTPStatus.CREATED)
|
||||
@@ -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
|
||||
|
||||
@@ -13,10 +13,12 @@ lnurlpayout_ext: APIRouter = APIRouter(prefix="/lnurlpayout", tags=["lnurlpayout
|
||||
def lnurlpayout_renderer():
|
||||
return template_renderer(["lnbits/extensions/lnurlpayout/templates"])
|
||||
|
||||
|
||||
from .tasks import wait_for_paid_invoices
|
||||
from .views import * # noqa
|
||||
from .views_api import * # noqa
|
||||
|
||||
|
||||
def lnurlpayout_start():
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
|
||||
|
||||
@@ -6,14 +6,23 @@ from . import db
|
||||
from .models import lnurlpayout, CreateLnurlPayoutData
|
||||
|
||||
|
||||
async def create_lnurlpayout(wallet_id: str, admin_key: str, data: CreateLnurlPayoutData) -> lnurlpayout:
|
||||
async def create_lnurlpayout(
|
||||
wallet_id: str, admin_key: str, data: CreateLnurlPayoutData
|
||||
) -> lnurlpayout:
|
||||
lnurlpayout_id = urlsafe_short_hash()
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO lnurlpayout.lnurlpayouts (id, title, wallet, admin_key, lnurlpay, threshold)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(lnurlpayout_id, data.title, wallet_id, admin_key, data.lnurlpay, data.threshold),
|
||||
(
|
||||
lnurlpayout_id,
|
||||
data.title,
|
||||
wallet_id,
|
||||
admin_key,
|
||||
data.lnurlpay,
|
||||
data.threshold,
|
||||
),
|
||||
)
|
||||
|
||||
lnurlpayout = await get_lnurlpayout(lnurlpayout_id)
|
||||
@@ -22,13 +31,19 @@ async def create_lnurlpayout(wallet_id: str, admin_key: str, data: CreateLnurlPa
|
||||
|
||||
|
||||
async def get_lnurlpayout(lnurlpayout_id: str) -> Optional[lnurlpayout]:
|
||||
row = await db.fetchone("SELECT * FROM lnurlpayout.lnurlpayouts WHERE id = ?", (lnurlpayout_id,))
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM lnurlpayout.lnurlpayouts WHERE id = ?", (lnurlpayout_id,)
|
||||
)
|
||||
return lnurlpayout(**row) if row else None
|
||||
|
||||
|
||||
async def get_lnurlpayout_from_wallet(wallet_id: str) -> Optional[lnurlpayout]:
|
||||
row = await db.fetchone("SELECT * FROM lnurlpayout.lnurlpayouts WHERE wallet = ?", (wallet_id,))
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM lnurlpayout.lnurlpayouts WHERE wallet = ?", (wallet_id,)
|
||||
)
|
||||
return lnurlpayout(**row) if row else None
|
||||
|
||||
|
||||
async def get_lnurlpayouts(wallet_ids: Union[str, List[str]]) -> List[lnurlpayout]:
|
||||
if isinstance(wallet_ids, str):
|
||||
wallet_ids = [wallet_ids]
|
||||
@@ -42,4 +57,6 @@ async def get_lnurlpayouts(wallet_ids: Union[str, List[str]]) -> List[lnurlpayou
|
||||
|
||||
|
||||
async def delete_lnurlpayout(lnurlpayout_id: str) -> None:
|
||||
await db.execute("DELETE FROM lnurlpayout.lnurlpayouts WHERE id = ?", (lnurlpayout_id,))
|
||||
await db.execute(
|
||||
"DELETE FROM lnurlpayout.lnurlpayouts WHERE id = ?", (lnurlpayout_id,)
|
||||
)
|
||||
|
||||
@@ -2,11 +2,13 @@ from sqlite3 import Row
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class CreateLnurlPayoutData(BaseModel):
|
||||
title: str
|
||||
lnurlpay: str
|
||||
threshold: int
|
||||
|
||||
|
||||
class lnurlpayout(BaseModel):
|
||||
id: str
|
||||
title: str
|
||||
|
||||
@@ -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,32 +27,44 @@ 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):
|
||||
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:
|
||||
url = await api_payments_decode({"data": lnurlpayout_link.lnurlpay})
|
||||
if str(url["domain"])[0:4] != "http":
|
||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="LNURL broken")
|
||||
try:
|
||||
r = await client.get(
|
||||
str(url["domain"]),
|
||||
timeout=40,
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN, detail="LNURL broken"
|
||||
)
|
||||
|
||||
try:
|
||||
r = await client.get(str(url["domain"]), timeout=40)
|
||||
res = r.json()
|
||||
try:
|
||||
r = await client.get(
|
||||
res["callback"] + "?amount=" + str(int((wallet.balance - wallet.balance*0.02) * 1000)),
|
||||
res["callback"]
|
||||
+ "?amount="
|
||||
+ str(
|
||||
int((wallet.balance - wallet.balance * 0.02) * 1000)
|
||||
),
|
||||
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,
|
||||
@@ -60,11 +74,16 @@ 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
|
||||
except Exception:
|
||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Failed to save LNURLPayout")
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN,
|
||||
detail="Failed to save LNURLPayout",
|
||||
)
|
||||
except:
|
||||
return
|
||||
return
|
||||
|
||||
@@ -4,16 +4,24 @@ 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
|
||||
|
||||
from . import lnurlpayout_ext
|
||||
from .crud import create_lnurlpayout, delete_lnurlpayout, get_lnurlpayout, get_lnurlpayouts, get_lnurlpayout_from_wallet
|
||||
from .models import lnurlpayout, CreateLnurlPayoutData
|
||||
from .crud import (
|
||||
create_lnurlpayout,
|
||||
delete_lnurlpayout,
|
||||
get_lnurlpayout,
|
||||
get_lnurlpayout_from_wallet,
|
||||
get_lnurlpayouts,
|
||||
)
|
||||
from .models import CreateLnurlPayoutData, lnurlpayout
|
||||
from .tasks import on_invoice_paid
|
||||
|
||||
|
||||
@lnurlpayout_ext.get("/api/v1/lnurlpayouts", status_code=HTTPStatus.OK)
|
||||
async def api_lnurlpayouts(
|
||||
all_wallets: bool = Query(None), wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
@@ -28,23 +36,33 @@ async def api_lnurlpayouts(
|
||||
@lnurlpayout_ext.post("/api/v1/lnurlpayouts", status_code=HTTPStatus.CREATED)
|
||||
async def api_lnurlpayout_create(
|
||||
data: CreateLnurlPayoutData, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
):
|
||||
):
|
||||
if await get_lnurlpayout_from_wallet(wallet.wallet.id):
|
||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Wallet already has lnurlpayout set")
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN,
|
||||
detail="Wallet already has lnurlpayout set",
|
||||
)
|
||||
return
|
||||
url = await api_payments_decode({"data": data.lnurlpay})
|
||||
if "domain" not in url:
|
||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="LNURL could not be decoded")
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN, detail="LNURL could not be decoded"
|
||||
)
|
||||
return
|
||||
if str(url["domain"])[0:4] != "http":
|
||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not valid LNURL")
|
||||
return
|
||||
lnurlpayout = await create_lnurlpayout(wallet_id=wallet.wallet.id, admin_key=wallet.wallet.adminkey, data=data)
|
||||
lnurlpayout = await create_lnurlpayout(
|
||||
wallet_id=wallet.wallet.id, admin_key=wallet.wallet.adminkey, data=data
|
||||
)
|
||||
if not lnurlpayout:
|
||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Failed to save LNURLPayout")
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN, detail="Failed to save LNURLPayout"
|
||||
)
|
||||
return
|
||||
return lnurlpayout.dict()
|
||||
|
||||
|
||||
@lnurlpayout_ext.delete("/api/v1/lnurlpayouts/{lnurlpayout_id}")
|
||||
async def api_lnurlpayout_delete(
|
||||
lnurlpayout_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
@@ -57,26 +75,44 @@ async def api_lnurlpayout_delete(
|
||||
)
|
||||
|
||||
if lnurlpayout.wallet != wallet.wallet.id:
|
||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your lnurlpayout.")
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN, detail="Not your lnurlpayout."
|
||||
)
|
||||
|
||||
await delete_lnurlpayout(lnurlpayout_id)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
|
||||
|
||||
@lnurlpayout_ext.get("/api/v1/lnurlpayouts/{lnurlpayout_id}", status_code=HTTPStatus.OK)
|
||||
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, pending=False, outgoing=True, incoming=True
|
||||
## THIS
|
||||
mock_payment = Payment(
|
||||
checking_id="mock",
|
||||
pending=False,
|
||||
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
|
||||
|
||||
# get payouts func
|
||||
# lnurlpayouts = await get_lnurlpayouts(wallet_ids)
|
||||
# for lnurlpayout in lnurlpayouts:
|
||||
# payments = await get_payments(
|
||||
# wallet_id=lnurlpayout.wallet, complete=True, pending=False, outgoing=True, incoming=True
|
||||
# )
|
||||
# await on_invoice_paid(payments[0])
|
||||
|
||||
# get payouts func
|
||||
# lnurlpayouts = await get_lnurlpayouts(wallet_ids)
|
||||
# for lnurlpayout in lnurlpayouts:
|
||||
# payments = await get_payments(
|
||||
# wallet_id=lnurlpayout.wallet, complete=True, pending=False, outgoing=True, incoming=True
|
||||
# )
|
||||
# await on_invoice_paid(payments[0])
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# LNURLPoS
|
||||
|
||||
For offline LNURL PoS devices
|
||||
@@ -1,17 +0,0 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from lnbits.db import Database
|
||||
from lnbits.helpers import template_renderer
|
||||
|
||||
db = Database("ext_lnurlpos")
|
||||
|
||||
lnurlpos_ext: APIRouter = APIRouter(prefix="/lnurlpos", tags=["lnurlpos"])
|
||||
|
||||
|
||||
def lnurlpos_renderer():
|
||||
return template_renderer(["lnbits/extensions/lnurlpos/templates"])
|
||||
|
||||
|
||||
from .lnurl import * # noqa
|
||||
from .views import * # noqa
|
||||
from .views_api import * # noqa
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "LNURLPoS",
|
||||
"short_description": "For offline LNURL PoS systems",
|
||||
"icon": "point_of_sale",
|
||||
"contributors": ["arcbtc"]
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
from . import db
|
||||
from .models import createLnurlpos, lnurlpospayment, lnurlposs
|
||||
|
||||
###############lnurlposS##########################
|
||||
|
||||
|
||||
async def create_lnurlpos(data: createLnurlpos,) -> lnurlposs:
|
||||
lnurlpos_id = urlsafe_short_hash()
|
||||
lnurlpos_key = urlsafe_short_hash()
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO lnurlpos.lnurlposs (
|
||||
id,
|
||||
key,
|
||||
title,
|
||||
wallet,
|
||||
currency
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""",
|
||||
(lnurlpos_id, lnurlpos_key, data.title, data.wallet, data.currency),
|
||||
)
|
||||
return await get_lnurlpos(lnurlpos_id)
|
||||
|
||||
|
||||
async def update_lnurlpos(lnurlpos_id: str, **kwargs) -> Optional[lnurlposs]:
|
||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||
await db.execute(
|
||||
f"UPDATE lnurlpos.lnurlposs SET {q} WHERE id = ?",
|
||||
(*kwargs.values(), lnurlpos_id),
|
||||
)
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM lnurlpos.lnurlposs WHERE id = ?", (lnurlpos_id,)
|
||||
)
|
||||
return lnurlposs(**row) if row else None
|
||||
|
||||
|
||||
async def get_lnurlpos(lnurlpos_id: str) -> lnurlposs:
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM lnurlpos.lnurlposs WHERE id = ?", (lnurlpos_id,)
|
||||
)
|
||||
return lnurlposs(**row) if row else None
|
||||
|
||||
|
||||
async def get_lnurlposs(wallet_ids: Union[str, List[str]]) -> List[lnurlposs]:
|
||||
wallet_ids = [wallet_ids]
|
||||
q = ",".join(["?"] * len(wallet_ids[0]))
|
||||
rows = await db.fetchall(
|
||||
f"""
|
||||
SELECT * FROM lnurlpos.lnurlposs WHERE wallet IN ({q})
|
||||
ORDER BY id
|
||||
""",
|
||||
(*wallet_ids,),
|
||||
)
|
||||
|
||||
return [lnurlposs(**row) if row else None for row in rows]
|
||||
|
||||
|
||||
async def delete_lnurlpos(lnurlpos_id: str) -> None:
|
||||
await db.execute("DELETE FROM lnurlpos.lnurlposs WHERE id = ?", (lnurlpos_id,))
|
||||
|
||||
########################lnulpos payments###########################
|
||||
|
||||
|
||||
async def create_lnurlpospayment(
|
||||
posid: str,
|
||||
payload: Optional[str] = None,
|
||||
pin: Optional[str] = None,
|
||||
payhash: Optional[str] = None,
|
||||
sats: Optional[int] = 0,
|
||||
) -> lnurlpospayment:
|
||||
lnurlpospayment_id = urlsafe_short_hash()
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO lnurlpos.lnurlpospayment (
|
||||
id,
|
||||
posid,
|
||||
payload,
|
||||
pin,
|
||||
payhash,
|
||||
sats
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(lnurlpospayment_id, posid, payload, pin, payhash, sats),
|
||||
)
|
||||
return await get_lnurlpospayment(lnurlpospayment_id)
|
||||
|
||||
|
||||
async def update_lnurlpospayment(
|
||||
lnurlpospayment_id: str, **kwargs
|
||||
) -> Optional[lnurlpospayment]:
|
||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||
await db.execute(
|
||||
f"UPDATE lnurlpos.lnurlpospayment SET {q} WHERE id = ?",
|
||||
(*kwargs.values(), lnurlpospayment_id),
|
||||
)
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM lnurlpos.lnurlpospayment WHERE id = ?", (lnurlpospayment_id,)
|
||||
)
|
||||
return lnurlpospayment(**row) if row else None
|
||||
|
||||
|
||||
async def get_lnurlpospayment(lnurlpospayment_id: str) -> lnurlpospayment:
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM lnurlpos.lnurlpospayment WHERE id = ?", (lnurlpospayment_id,)
|
||||
)
|
||||
return lnurlpospayment(**row) if row else None
|
||||
@@ -1,169 +0,0 @@
|
||||
import base64
|
||||
import hashlib
|
||||
from http import HTTPStatus
|
||||
from typing import Optional
|
||||
|
||||
from embit import bech32
|
||||
from embit import compact
|
||||
import base64
|
||||
from io import BytesIO
|
||||
import hmac
|
||||
|
||||
from fastapi import Request
|
||||
from fastapi.param_functions import Query
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from lnbits.core.services import create_invoice
|
||||
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
|
||||
|
||||
from . import lnurlpos_ext
|
||||
from .crud import (
|
||||
create_lnurlpospayment,
|
||||
get_lnurlpos,
|
||||
get_lnurlpospayment,
|
||||
update_lnurlpospayment,
|
||||
)
|
||||
|
||||
def bech32_decode(bech):
|
||||
"""tweaked version of bech32_decode that ignores length limitations"""
|
||||
if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or
|
||||
(bech.lower() != bech and bech.upper() != bech)):
|
||||
return
|
||||
bech = bech.lower()
|
||||
pos = bech.rfind('1')
|
||||
if pos < 1 or pos + 7 > len(bech):
|
||||
return
|
||||
if not all(x in bech32.CHARSET for x in bech[pos+1:]):
|
||||
return
|
||||
hrp = bech[:pos]
|
||||
data = [bech32.CHARSET.find(x) for x in bech[pos+1:]]
|
||||
encoding = bech32.bech32_verify_checksum(hrp, data)
|
||||
if encoding is None:
|
||||
return
|
||||
return bytes(bech32.convertbits(data[:-6], 5, 8, False))
|
||||
|
||||
def xor_decrypt(key, blob):
|
||||
s = BytesIO(blob)
|
||||
variant = s.read(1)[0]
|
||||
if variant != 1:
|
||||
raise RuntimeError("Not implemented")
|
||||
# reading nonce
|
||||
l = s.read(1)[0]
|
||||
nonce = s.read(l)
|
||||
if len(nonce) != l:
|
||||
raise RuntimeError("Missing nonce bytes")
|
||||
if l < 8:
|
||||
raise RuntimeError("Nonce is too short")
|
||||
# reading payload
|
||||
l = s.read(1)[0]
|
||||
payload = s.read(l)
|
||||
if len(payload) > 32:
|
||||
raise RuntimeError("Payload is too long for this encryption method")
|
||||
if len(payload) != l:
|
||||
raise RuntimeError("Missing payload bytes")
|
||||
hmacval = s.read()
|
||||
expected = hmac.new(key, b"Data:" + blob[:-len(hmacval)], digestmod="sha256").digest()
|
||||
if len(hmacval) < 8:
|
||||
raise RuntimeError("HMAC is too short")
|
||||
if hmacval != expected[:len(hmacval)]:
|
||||
raise RuntimeError("HMAC is invalid")
|
||||
secret = hmac.new(key, b"Round secret:" + nonce, digestmod="sha256").digest()
|
||||
payload = bytearray(payload)
|
||||
for i in range(len(payload)):
|
||||
payload[i] = payload[i] ^ secret[i]
|
||||
s = BytesIO(payload)
|
||||
pin = compact.read_from(s)
|
||||
amount_in_cent = compact.read_from(s)
|
||||
return pin, amount_in_cent
|
||||
|
||||
@lnurlpos_ext.get(
|
||||
"/api/v1/lnurl/{pos_id}",
|
||||
status_code=HTTPStatus.OK,
|
||||
name="lnurlpos.lnurl_v1_params",
|
||||
)
|
||||
async def lnurl_v1_params(
|
||||
request: Request,
|
||||
pos_id: str = Query(None),
|
||||
p: str = Query(None),
|
||||
):
|
||||
return await handle_lnurl_firstrequest(request, pos_id, p)
|
||||
|
||||
|
||||
async def handle_lnurl_firstrequest(
|
||||
request: Request, pos_id: str, payload: str
|
||||
):
|
||||
pos = await get_lnurlpos(pos_id)
|
||||
if not pos:
|
||||
return {
|
||||
"status": "ERROR",
|
||||
"reason": f"lnurlpos {pos_id} not found on this server",
|
||||
}
|
||||
|
||||
if len(payload) % 4 > 0:
|
||||
payload += "="*(4-(len(payload)%4))
|
||||
|
||||
data = base64.urlsafe_b64decode(payload)
|
||||
pin, amount_in_cent = xor_decrypt(pos.key.encode(), data)
|
||||
price_msat = (
|
||||
await fiat_amount_as_satoshis(float(amount_in_cent) / 100, pos.currency)
|
||||
if pos.currency != "sat"
|
||||
else amount
|
||||
) * 1000
|
||||
|
||||
lnurlpospayment = await create_lnurlpospayment(
|
||||
posid=pos.id,
|
||||
payload=payload,
|
||||
sats=price_msat,
|
||||
pin=pin,
|
||||
payhash="payment_hash",
|
||||
)
|
||||
if not lnurlpospayment:
|
||||
return {"status": "ERROR", "reason": "Could not create payment."}
|
||||
|
||||
return {
|
||||
"tag": "payRequest",
|
||||
"callback": request.url_for(
|
||||
"lnurlpos.lnurl_callback", paymentid=lnurlpospayment.id
|
||||
),
|
||||
"minSendable": price_msat,
|
||||
"maxSendable": price_msat,
|
||||
"metadata": await pos.lnurlpay_metadata(),
|
||||
}
|
||||
|
||||
|
||||
@lnurlpos_ext.get(
|
||||
"/api/v1/lnurl/cb/{paymentid}",
|
||||
status_code=HTTPStatus.OK,
|
||||
name="lnurlpos.lnurl_callback",
|
||||
)
|
||||
async def lnurl_callback(request: Request, paymentid: str = Query(None)):
|
||||
lnurlpospayment = await get_lnurlpospayment(paymentid)
|
||||
pos = await get_lnurlpos(lnurlpospayment.posid)
|
||||
if not pos:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN, detail="lnurlpos not found."
|
||||
)
|
||||
payment_hash, payment_request = await create_invoice(
|
||||
wallet_id=pos.wallet,
|
||||
amount=int(lnurlpospayment.sats / 1000),
|
||||
memo=pos.title,
|
||||
description_hash=hashlib.sha256(
|
||||
(await pos.lnurlpay_metadata()).encode("utf-8")
|
||||
).digest(),
|
||||
extra={"tag": "lnurlpos"},
|
||||
)
|
||||
lnurlpospayment = await update_lnurlpospayment(
|
||||
lnurlpospayment_id=paymentid, payhash=payment_hash
|
||||
)
|
||||
|
||||
return {
|
||||
"pr": payment_request,
|
||||
"successAction": {
|
||||
"tag": "url",
|
||||
"description": "Check the attached link",
|
||||
"url": request.url_for("lnurlpos.displaypin", paymentid=paymentid),
|
||||
},
|
||||
"routes": [],
|
||||
}
|
||||
|
||||
return resp.dict()
|
||||
@@ -1,30 +0,0 @@
|
||||
async def m001_initial(db):
|
||||
"""
|
||||
Initial lnurlpos table.
|
||||
"""
|
||||
|
||||
await db.execute(
|
||||
f"""
|
||||
CREATE TABLE lnurlpos.lnurlposs (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
key TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
wallet TEXT NOT NULL,
|
||||
currency TEXT NOT NULL,
|
||||
timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
||||
);
|
||||
"""
|
||||
)
|
||||
await db.execute(
|
||||
f"""
|
||||
CREATE TABLE lnurlpos.lnurlpospayment (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
posid TEXT NOT NULL,
|
||||
payhash TEXT,
|
||||
payload TEXT NOT NULL,
|
||||
pin INT,
|
||||
sats INT,
|
||||
timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
||||
);
|
||||
"""
|
||||
)
|
||||
@@ -1,50 +0,0 @@
|
||||
from http import HTTPStatus
|
||||
|
||||
from fastapi import Request
|
||||
from fastapi.param_functions import Query
|
||||
from fastapi.params import Depends
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.responses import HTMLResponse
|
||||
|
||||
from lnbits.core.crud import update_payment_status
|
||||
from lnbits.core.models import User
|
||||
from lnbits.core.views.api import api_payment
|
||||
from lnbits.decorators import check_user_exists
|
||||
|
||||
from . import lnurlpos_ext, lnurlpos_renderer
|
||||
from .crud import get_lnurlpos, get_lnurlpospayment
|
||||
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
|
||||
@lnurlpos_ext.get("/", response_class=HTMLResponse)
|
||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||
return lnurlpos_renderer().TemplateResponse(
|
||||
"lnurlpos/index.html", {"request": request, "user": user.dict()}
|
||||
)
|
||||
|
||||
|
||||
@lnurlpos_ext.get(
|
||||
"/{paymentid}", name="lnurlpos.displaypin", response_class=HTMLResponse
|
||||
)
|
||||
async def displaypin(request: Request, paymentid: str = Query(None)):
|
||||
lnurlpospayment = await get_lnurlpospayment(paymentid)
|
||||
if not lnurlpospayment:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="No lmurlpos payment"
|
||||
)
|
||||
pos = await get_lnurlpos(lnurlpospayment.posid)
|
||||
if not pos:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="lnurlpos not found."
|
||||
)
|
||||
status = await api_payment(lnurlpospayment.payhash)
|
||||
if status["paid"]:
|
||||
await update_payment_status(checking_id=lnurlpospayment.payhash, pending=True)
|
||||
return lnurlpos_renderer().TemplateResponse(
|
||||
"lnurlpos/paid.html", {"request": request, "pin": lnurlpospayment.pin}
|
||||
)
|
||||
return lnurlpos_renderer().TemplateResponse(
|
||||
"lnurlpos/error.html", {"request": request, "pin": "filler", "not_paid": True}
|
||||
)
|
||||
@@ -1,85 +0,0 @@
|
||||
from http import HTTPStatus
|
||||
|
||||
from fastapi import Request
|
||||
from fastapi.param_functions import Query
|
||||
from fastapi.params import Depends
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from lnbits.core.crud import get_user
|
||||
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
||||
from lnbits.extensions.lnurlpos import lnurlpos_ext
|
||||
from lnbits.utils.exchange_rates import currencies
|
||||
|
||||
from . import lnurlpos_ext
|
||||
from .crud import (
|
||||
create_lnurlpos,
|
||||
delete_lnurlpos,
|
||||
get_lnurlpos,
|
||||
get_lnurlposs,
|
||||
update_lnurlpos,
|
||||
)
|
||||
from .models import createLnurlpos
|
||||
|
||||
|
||||
@lnurlpos_ext.get("/api/v1/currencies")
|
||||
async def api_list_currencies_available():
|
||||
return list(currencies.keys())
|
||||
|
||||
|
||||
#######################lnurlpos##########################
|
||||
|
||||
|
||||
@lnurlpos_ext.post("/api/v1/lnurlpos")
|
||||
@lnurlpos_ext.put("/api/v1/lnurlpos/{lnurlpos_id}")
|
||||
async def api_lnurlpos_create_or_update(
|
||||
data: createLnurlpos,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
lnurlpos_id: str = Query(None),
|
||||
):
|
||||
if not lnurlpos_id:
|
||||
lnurlpos = await create_lnurlpos(data)
|
||||
return lnurlpos.dict()
|
||||
else:
|
||||
lnurlpos = await update_lnurlpos(data, lnurlpos_id=lnurlpos_id)
|
||||
return lnurlpos.dict()
|
||||
|
||||
|
||||
@lnurlpos_ext.get("/api/v1/lnurlpos")
|
||||
async def api_lnurlposs_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
|
||||
try:
|
||||
return [{**lnurlpos.dict()} for lnurlpos in await get_lnurlposs(wallet_ids)]
|
||||
except:
|
||||
return ""
|
||||
|
||||
|
||||
@lnurlpos_ext.get("/api/v1/lnurlpos/{lnurlpos_id}")
|
||||
async def api_lnurlpos_retrieve(
|
||||
request: Request,
|
||||
wallet: WalletTypeInfo = Depends(get_key_type),
|
||||
lnurlpos_id: str = Query(None),
|
||||
):
|
||||
lnurlpos = await get_lnurlpos(lnurlpos_id)
|
||||
if not lnurlpos:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="lnurlpos does not exist"
|
||||
)
|
||||
if not lnurlpos.lnurl_toggle:
|
||||
return {**lnurlpos.dict()}
|
||||
return {**lnurlpos.dict(), **{"lnurl": lnurlpos.lnurl(request=request)}}
|
||||
|
||||
|
||||
@lnurlpos_ext.delete("/api/v1/lnurlpos/{lnurlpos_id}")
|
||||
async def api_lnurlpos_delete(
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key), lnurlpos_id: str = Query(None)
|
||||
):
|
||||
lnurlpos = await get_lnurlpos(lnurlpos_id)
|
||||
|
||||
if not lnurlpos:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Wallet link does not exist."
|
||||
)
|
||||
|
||||
await delete_lnurlpos(lnurlpos_id)
|
||||
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
@@ -8,8 +8,8 @@ def hotp(key, counter, digits=6, digest="sha1"):
|
||||
key = base64.b32decode(key.upper() + "=" * ((8 - len(key)) % 8))
|
||||
counter = struct.pack(">Q", counter)
|
||||
mac = hmac.new(key, counter, digest).digest()
|
||||
offset = mac[-1] & 0x0F
|
||||
binary = struct.unpack(">L", mac[offset : offset + 4])[0] & 0x7FFFFFFF
|
||||
offset = mac[-1] & 0x0f
|
||||
binary = struct.unpack(">L", mac[offset : offset + 4])[0] & 0x7fffffff
|
||||
return str(binary)[-digits:].zfill(digits)
|
||||
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import asyncio
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from lnbits.db import Database
|
||||
from lnbits.helpers import template_renderer
|
||||
from lnbits.tasks import catch_everything_and_restart
|
||||
|
||||
db = Database("ext_satspay")
|
||||
|
||||
@@ -13,5 +16,11 @@ def satspay_renderer():
|
||||
return template_renderer(["lnbits/extensions/satspay/templates"])
|
||||
|
||||
|
||||
from .tasks import wait_for_paid_invoices
|
||||
from .views import * # noqa
|
||||
from .views_api import * # noqa
|
||||
|
||||
|
||||
def satspay_start():
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
|
||||
|
||||
@@ -25,7 +25,10 @@ async def create_charge(user: str, data: CreateCharge) -> Charges:
|
||||
onchainaddress = None
|
||||
if data.lnbitswallet:
|
||||
payment_hash, payment_request = await create_invoice(
|
||||
wallet_id=data.lnbitswallet, amount=data.amount, memo=charge_id
|
||||
wallet_id=data.lnbitswallet,
|
||||
amount=data.amount,
|
||||
memo=charge_id,
|
||||
extra={"tag": "charge"},
|
||||
)
|
||||
else:
|
||||
payment_hash = None
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import time
|
||||
from sqlite3 import Row
|
||||
from typing import Optional
|
||||
|
||||
from fastapi.param_functions import Query
|
||||
from pydantic import BaseModel
|
||||
import time
|
||||
|
||||
|
||||
class CreateCharge(BaseModel):
|
||||
@@ -23,8 +24,8 @@ class Charges(BaseModel):
|
||||
onchainwallet: Optional[str]
|
||||
onchainaddress: Optional[str]
|
||||
lnbitswallet: Optional[str]
|
||||
payment_request: str
|
||||
payment_hash: str
|
||||
payment_request: Optional[str]
|
||||
payment_hash: Optional[str]
|
||||
webhook: Optional[str]
|
||||
completelink: Optional[str]
|
||||
completelinktext: Optional[str] = "Back to Merchant"
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import asyncio
|
||||
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.extensions.satspay.crud import check_address_balance, get_charge
|
||||
from lnbits.tasks import register_invoice_listener
|
||||
|
||||
# from .crud import get_ticket, set_ticket_paid
|
||||
|
||||
|
||||
async def wait_for_paid_invoices():
|
||||
invoice_queue = asyncio.Queue()
|
||||
register_invoice_listener(invoice_queue)
|
||||
|
||||
while True:
|
||||
payment = await invoice_queue.get()
|
||||
await on_invoice_paid(payment)
|
||||
|
||||
|
||||
async def on_invoice_paid(payment: Payment) -> None:
|
||||
if "charge" != payment.extra.get("tag"):
|
||||
# not a charge invoice
|
||||
return
|
||||
|
||||
charge = await get_charge(payment.memo)
|
||||
if not charge:
|
||||
print("this should never happen", payment)
|
||||
return
|
||||
|
||||
await payment.set_pending(False)
|
||||
await check_address_balance(charge_id=charge.id)
|
||||
@@ -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
|
||||
@@ -132,7 +132,7 @@
|
||||
lightning-network invoice</span
|
||||
>
|
||||
</center>
|
||||
<a href="lightning://{{ charge.payment_request }}">
|
||||
<a href="lightning:{{ charge.payment_request }}">
|
||||
<q-responsive :ratio="1" class="q-mx-md">
|
||||
<qrcode
|
||||
:value="'{{ charge.payment_request }}'"
|
||||
@@ -182,7 +182,7 @@
|
||||
to this onchain address</span
|
||||
>
|
||||
</center>
|
||||
<a href="bitcoin://{{ charge.onchainaddress }}">
|
||||
<a href="bitcoin:{{ charge.onchainaddress }}">
|
||||
<q-responsive :ratio="1" class="q-mx-md">
|
||||
<qrcode
|
||||
:value="'{{ charge.onchainaddress }}'"
|
||||
@@ -235,10 +235,24 @@
|
||||
charge_time_elapsed: '{{charge.time_elapsed}}',
|
||||
charge_amount: '{{charge.amount}}',
|
||||
charge_balance: '{{charge.balance}}',
|
||||
charge_paid: '{{charge.paid}}'
|
||||
charge_paid: '{{charge.paid}}',
|
||||
wallet: {
|
||||
inkey: ''
|
||||
},
|
||||
cancelListener: () => {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
startPaymentNotifier() {
|
||||
this.cancelListener()
|
||||
|
||||
this.cancelListener = LNbits.event.onInvoicePaid(
|
||||
this.wallet,
|
||||
payment => {
|
||||
this.checkBalance()
|
||||
}
|
||||
)
|
||||
},
|
||||
checkBalance: function () {
|
||||
var self = this
|
||||
LNbits.api
|
||||
@@ -307,12 +321,14 @@
|
||||
this.lnbtc = false
|
||||
this.onbtc = true
|
||||
}
|
||||
this.wallet.inkey = '{{ wallet_inkey }}'
|
||||
this.getTheTime()
|
||||
this.getThePercentage()
|
||||
var timerCount = this.timerCount
|
||||
if ('{{ charge.paid }}' == 'False') {
|
||||
timerCount()
|
||||
}
|
||||
this.startPaymentNotifier()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -475,7 +475,7 @@
|
||||
},
|
||||
sendFormDataCharge: function () {
|
||||
var self = this
|
||||
var wallet = this.g.user.wallets[0].adminkey
|
||||
var wallet = this.g.user.wallets[0].inkey
|
||||
var data = this.formDialogCharge.data
|
||||
data.amount = parseInt(data.amount)
|
||||
data.time = parseInt(data.time)
|
||||
|
||||
@@ -6,6 +6,7 @@ from starlette.exceptions import HTTPException
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import HTMLResponse
|
||||
|
||||
from lnbits.core.crud import get_wallet
|
||||
from lnbits.core.models import User
|
||||
from lnbits.decorators import check_user_exists
|
||||
|
||||
@@ -29,6 +30,8 @@ async def display(request: Request, charge_id):
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Charge link does not exist."
|
||||
)
|
||||
wallet = await get_wallet(charge.lnbitswallet)
|
||||
return satspay_renderer().TemplateResponse(
|
||||
"satspay/display.html", {"request": request, "charge": charge}
|
||||
"satspay/display.html",
|
||||
{"request": request, "charge": charge, "wallet_key": wallet.inkey},
|
||||
)
|
||||
|
||||
@@ -5,7 +5,12 @@ from fastapi import Query
|
||||
from fastapi.params import Depends
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
||||
from lnbits.decorators import (
|
||||
WalletTypeInfo,
|
||||
get_key_type,
|
||||
require_admin_key,
|
||||
require_invoice_key,
|
||||
)
|
||||
from lnbits.extensions.satspay import satspay_ext
|
||||
|
||||
from .crud import (
|
||||
@@ -22,18 +27,21 @@ from .models import CreateCharge
|
||||
|
||||
|
||||
@satspay_ext.post("/api/v1/charge")
|
||||
async def api_charge_create(
|
||||
data: CreateCharge, wallet: WalletTypeInfo = Depends(require_invoice_key)
|
||||
):
|
||||
charge = await create_charge(user=wallet.wallet.user, data=data)
|
||||
return charge.dict()
|
||||
|
||||
|
||||
@satspay_ext.put("/api/v1/charge/{charge_id}")
|
||||
async def api_charge_create_or_update(
|
||||
async def api_charge_update(
|
||||
data: CreateCharge,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
charge_id=None,
|
||||
):
|
||||
if not charge_id:
|
||||
charge = await create_charge(user=wallet.wallet.user, data=data)
|
||||
return charge.dict()
|
||||
else:
|
||||
charge = await update_charge(charge_id=charge_id, data=data)
|
||||
return charge.dict()
|
||||
charge = await update_charge(charge_id=charge_id, data=data)
|
||||
return charge.dict()
|
||||
|
||||
|
||||
@satspay_ext.get("/api/v1/charges")
|
||||
|
||||
@@ -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,
|
||||
@@ -173,7 +175,7 @@ async def api_get_donations(g: WalletTypeInfo = Depends(get_key_type)):
|
||||
for wallet_id in wallet_ids:
|
||||
new_donations = await get_donations(wallet_id)
|
||||
donations += new_donations if new_donations else []
|
||||
return [donation._asdict() for donation in donations] if donations else []
|
||||
return [donation.dict() for donation in donations] if donations else []
|
||||
|
||||
|
||||
@streamalerts_ext.put("/api/v1/donations/{donation_id}")
|
||||
|
||||
@@ -33,7 +33,7 @@ async def create_subdomain(payment_hash, wallet, data: CreateDomain) -> Subdomai
|
||||
|
||||
async def set_subdomain_paid(payment_hash: str) -> Subdomains:
|
||||
row = await db.fetchone(
|
||||
"SELECT s.*, d.domain as domain_name FROM subdomains.subdomain s INNER JOIN domain d ON (s.domain = d.id) WHERE s.id = ?",
|
||||
"SELECT s.*, d.domain as domain_name FROM subdomains.subdomain s INNER JOIN subdomains.domain d ON (s.domain = d.id) WHERE s.id = ?",
|
||||
(payment_hash,),
|
||||
)
|
||||
if row[8] == False:
|
||||
@@ -66,7 +66,7 @@ async def set_subdomain_paid(payment_hash: str) -> Subdomains:
|
||||
|
||||
async def get_subdomain(subdomain_id: str) -> Optional[Subdomains]:
|
||||
row = await db.fetchone(
|
||||
"SELECT s.*, d.domain as domain_name FROM subdomains.subdomain s INNER JOIN domain d ON (s.domain = d.id) WHERE s.id = ?",
|
||||
"SELECT s.*, d.domain as domain_name FROM subdomains.subdomain s INNER JOIN subdomains.domain d ON (s.domain = d.id) WHERE s.id = ?",
|
||||
(subdomain_id,),
|
||||
)
|
||||
return Subdomains(**row) if row else None
|
||||
@@ -74,7 +74,7 @@ async def get_subdomain(subdomain_id: str) -> Optional[Subdomains]:
|
||||
|
||||
async def get_subdomainBySubdomain(subdomain: str) -> Optional[Subdomains]:
|
||||
row = await db.fetchone(
|
||||
"SELECT s.*, d.domain as domain_name FROM subdomains.subdomain s INNER JOIN domain d ON (s.domain = d.id) WHERE s.subdomain = ?",
|
||||
"SELECT s.*, d.domain as domain_name FROM subdomains.subdomain s INNER JOIN subdomains.domain d ON (s.domain = d.id) WHERE s.subdomain = ?",
|
||||
(subdomain,),
|
||||
)
|
||||
return Subdomains(**row) if row else None
|
||||
@@ -86,7 +86,7 @@ async def get_subdomains(wallet_ids: Union[str, List[str]]) -> List[Subdomains]:
|
||||
|
||||
q = ",".join(["?"] * len(wallet_ids))
|
||||
rows = await db.fetchall(
|
||||
f"SELECT s.*, d.domain as domain_name FROM subdomains.subdomain s INNER JOIN domain d ON (s.domain = d.id) WHERE s.wallet IN ({q})",
|
||||
f"SELECT s.*, d.domain as domain_name FROM subdomains.subdomain s INNER JOIN subdomains.domain d ON (s.domain = d.id) WHERE s.wallet IN ({q})",
|
||||
(*wallet_ids,),
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -92,7 +92,7 @@ async def api_get_tipjars(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
for wallet_id in wallet_ids:
|
||||
new_tipjars = await get_tipjars(wallet_id)
|
||||
tipjars += new_tipjars if new_tipjars else []
|
||||
return [tipjar._asdict() for tipjar in tipjars] if tipjars else []
|
||||
return [tipjar.dict() for tipjar in tipjars] if tipjars else []
|
||||
|
||||
|
||||
@tipjar_ext.get("/api/v1/tips")
|
||||
@@ -103,7 +103,7 @@ async def api_get_tips(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
for wallet_id in wallet_ids:
|
||||
new_tips = await get_tips(wallet_id)
|
||||
tips += new_tips if new_tips else []
|
||||
return [tip._asdict() for tip in tips] if tips else []
|
||||
return [tip.dict() for tip in tips] if tips else []
|
||||
|
||||
|
||||
@tipjar_ext.put("/api/v1/tips/{tip_id}")
|
||||
|
||||
@@ -222,6 +222,7 @@
|
||||
'INR',
|
||||
'IQD',
|
||||
'IRR',
|
||||
'IRT',
|
||||
'ISK',
|
||||
'JEP',
|
||||
'JMD',
|
||||
|
||||
@@ -82,11 +82,9 @@ async def api_wallet_delete(wallet_id, w: WalletTypeInfo = Depends(require_admin
|
||||
|
||||
@watchonly_ext.get("/api/v1/address/{wallet_id}")
|
||||
async def api_fresh_address(wallet_id, w: WalletTypeInfo = Depends(get_key_type)):
|
||||
await get_fresh_address(wallet_id)
|
||||
address = await get_fresh_address(wallet_id)
|
||||
|
||||
addresses = await get_addresses(wallet_id)
|
||||
|
||||
return [address.dict() for address in addresses]
|
||||
return [address.dict()]
|
||||
|
||||
|
||||
@watchonly_ext.get("/api/v1/addresses/{wallet_id}")
|
||||
@@ -114,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>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
{% if link.is_spent %}
|
||||
<q-badge color="red" class="q-mb-md">Withdraw is spent.</q-badge>
|
||||
{% endif %}
|
||||
<a href="lightning://{{ lnurl }}">
|
||||
<a href="lightning:{{ lnurl }}">
|
||||
<q-responsive :ratio="1" class="q-mx-md">
|
||||
<qrcode
|
||||
:value="this.here + '/?lightning={{lnurl }}'"
|
||||
|
||||
@@ -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,7 +160,11 @@ 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
|
||||
t.env.globals["SITE_DESCRIPTION"] = settings.LNBITS_SITE_DESCRIPTION
|
||||
t.env.globals["LNBITS_THEME_OPTIONS"] = settings.LNBITS_THEME_OPTIONS
|
||||
|
||||
@@ -28,11 +28,16 @@ LNBITS_DATABASE_URL = env.str("LNBITS_DATABASE_URL", default=None)
|
||||
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(
|
||||
"LNBITS_SITE_TAGLINE", default="free and open-source lightning wallet"
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
@@ -22,7 +22,8 @@ Vue.component('lnbits-wallet-list', {
|
||||
activeWallet: null,
|
||||
activeBalance: [],
|
||||
showForm: false,
|
||||
walletName: ''
|
||||
walletName: '',
|
||||
LNBITS_DENOMINATION: LNBITS_DENOMINATION
|
||||
}
|
||||
},
|
||||
template: `
|
||||
@@ -43,7 +44,8 @@ Vue.component('lnbits-wallet-list', {
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label lines="1">{{ wallet.name }}</q-item-label>
|
||||
<q-item-label caption>{{ wallet.live_fsat }} sat</q-item-label>
|
||||
<q-item-label v-if="LNBITS_DENOMINATION != 'sats'" caption>{{ parseFloat(String(wallet.live_fsat).replaceAll(",", "")) / 100 }} {{ LNBITS_DENOMINATION }}</q-item-label>
|
||||
<q-item-label v-else caption>{{ wallet.live_fsat }} {{ LNBITS_DENOMINATION }}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side v-show="activeWallet && activeWallet.id === wallet.id">
|
||||
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
|
||||
@@ -194,11 +196,11 @@ Vue.component('lnbits-payment-details', {
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-3"><b>Amount</b>:</div>
|
||||
<div class="col-9">{{ (payment.amount / 1000).toFixed(3) }} sat</div>
|
||||
<div class="col-9">{{ (payment.amount / 1000).toFixed(3) }} {{LNBITS_DENOMINATION}}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-3"><b>Fee</b>:</div>
|
||||
<div class="col-9">{{ (payment.fee / 1000).toFixed(3) }} sat</div>
|
||||
<div class="col-9">{{ (payment.fee / 1000).toFixed(3) }} {{LNBITS_DENOMINATION}}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-3"><b>Payment hash</b>:</div>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -192,6 +205,8 @@
|
||||
<script src="/static/js/components.js"></script>
|
||||
<script type="text/javascript">
|
||||
const themes = {{ LNBITS_THEME_OPTIONS | tojson }}
|
||||
const LNBITS_DENOMINATION = {{ LNBITS_DENOMINATION | tojson}}
|
||||
console.log(LNBITS_DENOMINATION)
|
||||
if(themes && themes.length) {
|
||||
window.allowedThemes = themes.map(str => str.trim())
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -279,8 +286,12 @@ async def btc_price(currency: str) -> float:
|
||||
|
||||
|
||||
async def get_fiat_rate_satoshis(currency: str) -> float:
|
||||
return int(100_000_000 / (await btc_price(currency)))
|
||||
return float(100_000_000 / (await btc_price(currency)))
|
||||
|
||||
|
||||
async def fiat_amount_as_satoshis(amount: float, currency: str) -> int:
|
||||
return int(amount * (await get_fiat_rate_satoshis(currency)))
|
||||
|
||||
|
||||
async def satoshis_amount_as_fiat(amount: float, currency: str) -> float:
|
||||
return float(amount / (await get_fiat_rate_satoshis(currency)))
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
from .void import VoidWallet
|
||||
from .clightning import CLightningWallet
|
||||
from .lndgrpc import LndWallet
|
||||
from .lntxbot import LntxbotWallet
|
||||
from .opennode import OpenNodeWallet
|
||||
from .lnpay import LNPayWallet
|
||||
from .lnbits import LNbitsWallet
|
||||
from .lndrest import LndRestWallet
|
||||
from .spark import SparkWallet
|
||||
from .fake import FakeWallet
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user