Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
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 |
+12
-5
@@ -5,10 +5,15 @@ QUART_DEBUG=true
|
|||||||
HOST=127.0.0.1
|
HOST=127.0.0.1
|
||||||
PORT=5000
|
PORT=5000
|
||||||
|
|
||||||
|
|
||||||
LNBITS_ALLOWED_USERS=""
|
LNBITS_ALLOWED_USERS=""
|
||||||
|
LNBITS_ADMIN_USERS=""
|
||||||
|
# Extensions only admin can access
|
||||||
|
LNBITS_ADMIN_EXTENSIONS="ngrok"
|
||||||
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
|
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
|
||||||
|
|
||||||
|
# Disable extensions for all users, use "all" to disable all extensions
|
||||||
|
LNBITS_DISABLED_EXTENSIONS="amilk"
|
||||||
|
|
||||||
# Database: to use SQLite, specify LNBITS_DATA_FOLDER
|
# Database: to use SQLite, specify LNBITS_DATA_FOLDER
|
||||||
# to use PostgreSQL, specify LNBITS_DATABASE_URL=postgres://...
|
# to use PostgreSQL, specify LNBITS_DATABASE_URL=postgres://...
|
||||||
# to use CockroachDB, specify LNBITS_DATABASE_URL=cockroachdb://...
|
# to use CockroachDB, specify LNBITS_DATABASE_URL=cockroachdb://...
|
||||||
@@ -17,8 +22,6 @@ LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
|
|||||||
LNBITS_DATA_FOLDER="./data"
|
LNBITS_DATA_FOLDER="./data"
|
||||||
# LNBITS_DATABASE_URL="postgres://user:password@host:port/databasename"
|
# 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_FORCE_HTTPS=true
|
||||||
LNBITS_SERVICE_FEE="0.0"
|
LNBITS_SERVICE_FEE="0.0"
|
||||||
|
|
||||||
@@ -30,7 +33,7 @@ LNBITS_SITE_DESCRIPTION="Some description about your service, will display if ti
|
|||||||
LNBITS_THEME_OPTIONS="mint, flamingo, classic, autumn, monochrome, salvador"
|
LNBITS_THEME_OPTIONS="mint, flamingo, classic, autumn, monochrome, salvador"
|
||||||
|
|
||||||
# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, LndWallet (gRPC),
|
# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, LndWallet (gRPC),
|
||||||
# LndRestWallet, CLightningWallet, LNbitsWallet, SparkWallet
|
# LndRestWallet, CLightningWallet, LNbitsWallet, SparkWallet, FakeWallet
|
||||||
LNBITS_BACKEND_WALLET_CLASS=VoidWallet
|
LNBITS_BACKEND_WALLET_CLASS=VoidWallet
|
||||||
# VoidWallet is just a fallback that works without any actual Lightning capabilities,
|
# VoidWallet is just a fallback that works without any actual Lightning capabilities,
|
||||||
# just so you can see the UI before dealing with this file.
|
# just so you can see the UI before dealing with this file.
|
||||||
@@ -44,7 +47,7 @@ SPARK_TOKEN=myaccesstoken
|
|||||||
CLIGHTNING_RPC="/home/bob/.lightning/bitcoin/lightning-rpc"
|
CLIGHTNING_RPC="/home/bob/.lightning/bitcoin/lightning-rpc"
|
||||||
|
|
||||||
# LnbitsWallet
|
# LnbitsWallet
|
||||||
LNBITS_ENDPOINT=http://127.0.0.1:5000
|
LNBITS_ENDPOINT=https://legend.lnbits.com
|
||||||
LNBITS_KEY=LNBITS_ADMIN_KEY
|
LNBITS_KEY=LNBITS_ADMIN_KEY
|
||||||
|
|
||||||
# LndWallet
|
# LndWallet
|
||||||
@@ -72,3 +75,7 @@ LNTXBOT_KEY=LNTXBOT_ADMIN_KEY
|
|||||||
# OpenNodeWallet
|
# OpenNodeWallet
|
||||||
OPENNODE_API_ENDPOINT=https://api.opennode.com/
|
OPENNODE_API_ENDPOINT=https://api.opennode.com/
|
||||||
OPENNODE_KEY=OPENNODE_ADMIN_KEY
|
OPENNODE_KEY=OPENNODE_ADMIN_KEY
|
||||||
|
|
||||||
|
# FakeWallet
|
||||||
|
FAKE_WALLET_SECRET="ToTheMoon1"
|
||||||
|
LNBITS_DENOMINATION=sats
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2019 Arc
|
Copyright (c) 2022 Arc
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ uvicorn = {extras = ["standard"], version = "*"}
|
|||||||
sse-starlette = "*"
|
sse-starlette = "*"
|
||||||
jinja2 = "3.0.1"
|
jinja2 = "3.0.1"
|
||||||
pyngrok = "*"
|
pyngrok = "*"
|
||||||
|
secp256k1 = "*"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
black = "==20.8b1"
|
black = "==20.8b1"
|
||||||
|
|||||||
Generated
+520
-320
File diff suppressed because it is too large
Load Diff
@@ -34,11 +34,7 @@ LNbits is inspired by all the great work of [opennode.com](https://www.opennode.
|
|||||||
|
|
||||||
## Running LNbits
|
## Running LNbits
|
||||||
|
|
||||||
See the [install guide](docs/guide/installation.md) for details on installation and setup.
|
See the [install guide](docs/devs/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.
|
|
||||||
|
|
||||||
## LNbits as an account system
|
## 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 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
|
# Change these values as needed
|
||||||
|
|
||||||
@@ -34,7 +34,8 @@ def get_postgres_cursor():
|
|||||||
def check_db_versions(sqdb):
|
def check_db_versions(sqdb):
|
||||||
sqlite = get_sqlite_cursor(sqdb)
|
sqlite = get_sqlite_cursor(sqdb)
|
||||||
dblite = dict(sqlite.execute("SELECT * FROM dbversions;").fetchall())
|
dblite = dict(sqlite.execute("SELECT * FROM dbversions;").fetchall())
|
||||||
del dblite["lnurlpos"] # wrongly added?
|
if "lnurlpos" in dblite:
|
||||||
|
del dblite["lnurlpos"]
|
||||||
sqlite.close()
|
sqlite.close()
|
||||||
|
|
||||||
postgres = get_postgres_cursor()
|
postgres = get_postgres_cursor()
|
||||||
@@ -42,7 +43,7 @@ def check_db_versions(sqdb):
|
|||||||
dbpost = dict(postgres.fetchall())
|
dbpost = dict(postgres.fetchall())
|
||||||
|
|
||||||
for key in dblite.keys():
|
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(
|
raise Exception(
|
||||||
f"sqlite database version ({dblite[key]}) of {key} doesn't match postgres database version {dbpost[key]}"
|
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
|
connection = cursor.connection
|
||||||
|
|
||||||
for d in data:
|
for d in data:
|
||||||
cursor.execute(query, d)
|
try:
|
||||||
|
cursor.execute(query, d)
|
||||||
|
except:
|
||||||
|
raise ValueError(f"Failed to insert {d}")
|
||||||
connection.commit()
|
connection.commit()
|
||||||
|
|
||||||
cursor.close()
|
cursor.close()
|
||||||
|
|||||||
+22
-43
@@ -7,59 +7,38 @@ nav_order: 1
|
|||||||
|
|
||||||
# Installation
|
# 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/
|
||||||
|
pipenv shell
|
||||||
|
# pipenv --python 3.8 shell (if you wish to use a version of Python higher than 3.7)
|
||||||
|
pipenv install --dev
|
||||||
|
# pipenv --python 3.8 install --dev (if you wish to use a version of Python higher than 3.7)
|
||||||
|
|
||||||
The application uses [Pipenv][pipenv] to manage Python packages.
|
# If any of the modules fails to install, try checking and upgrading your setupTool module
|
||||||
While in development, you will need to install all dependencies:
|
# pip install -U setuptools
|
||||||
|
```
|
||||||
|
## Running the server
|
||||||
|
|
||||||
git clone https://github.com/lnbits/lnbits-legend.git
|
Create the data folder and edit the .env file:
|
||||||
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.
|
mkdir data
|
||||||
`pip install -U setuptools`
|
cp .env.example .env
|
||||||
|
sudo nano .env
|
||||||
|
|
||||||
If you wish to use a version of Python higher than 3.7:
|
To then run the server for development purposes (includes hot-reload), use:
|
||||||
|
|
||||||
pipenv --python 3.8 install --dev
|
pipenv run python -m uvicorn lnbits.__main__:app --host 0.0.0.0 --reload
|
||||||
|
|
||||||
|
For production, use:
|
||||||
|
|
||||||
You will need to copy `.env.example` to `.env`, then set variables there.
|
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.
|
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`.
|
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.
|
Take a look at [Polar][polar] for an excellent way of spinning up a Lightning Network dev environment.
|
||||||
|
|
||||||
## Running the server
|
**Note**: We reccomend using <a href="https://caddyserver.com/docs/install#debian-ubuntu-raspbian">Caddy</a> for a reverse-proxy, if you want to serve your install through a domain, alternatively you can use [ngrok](https://ngrok.com/).
|
||||||
|
|
||||||
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
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ nav_order: 2
|
|||||||
|
|
||||||
# Basic installation
|
# Basic installation
|
||||||
Install Postgres and setup a database for LNbits:
|
Install Postgres and setup a database for LNbits:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# on debian/ubuntu 'sudo apt-get -y install postgresql'
|
# on debian/ubuntu 'sudo apt-get -y install postgresql'
|
||||||
# or follow instructions at https://www.postgresql.org/download/linux/
|
# or follow instructions at https://www.postgresql.org/download/linux/
|
||||||
@@ -25,14 +26,14 @@ Download this repo and install the dependencies:
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/lnbits/lnbits-legend.git
|
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
|
# ensure you have virtualenv installed, on debian/ubuntu 'apt install python3-venv' should work
|
||||||
# for now you'll need to `git checkout FastAPI`
|
# for now you'll need to `git checkout FastAPI`
|
||||||
python3 -m venv venv
|
python3 -m venv venv
|
||||||
./venv/bin/pip install -r requirements.txt
|
./venv/bin/pip install -r requirements.txt
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
# add the database connection string to .env 'nano .env' LNBITS_DATABASE_URL=
|
# 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"
|
LNBITS_DATABASE_URL="postgres://postgres:postgres@localhost/lnbits"
|
||||||
# save and exit
|
# save and exit
|
||||||
./venv/bin/uvicorn lnbits.__main__:app --port 5000
|
./venv/bin/uvicorn lnbits.__main__:app --port 5000
|
||||||
@@ -50,6 +51,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!
|
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.
|
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
|
```sh
|
||||||
# STOP LNbits
|
# STOP LNbits
|
||||||
# on the LNBits folder, locate and edit 'conv.py' with the relevant credentials
|
# on the LNBits folder, locate and edit 'conv.py' with the relevant credentials
|
||||||
@@ -67,6 +69,39 @@ Hopefully, everything works and get migrated... Launch LNbits again and check if
|
|||||||
|
|
||||||
# Additional guides
|
# 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
|
### 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.
|
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.
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ A backend wallet can be configured using the following LNbits environment variab
|
|||||||
### CLightning
|
### CLightning
|
||||||
|
|
||||||
Using this wallet requires the installation of the `pylightning` Python package.
|
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**
|
- `LNBITS_BACKEND_WALLET_CLASS`: **CLightningWallet**
|
||||||
- `CLIGHTNING_RPC`: /file/path/lightning-rpc
|
- `CLIGHTNING_RPC`: /file/path/lightning-rpc
|
||||||
|
|||||||
+3
-7
@@ -1,6 +1,5 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import importlib
|
import importlib
|
||||||
from lnbits.core.tasks import register_task_listeners
|
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
import warnings
|
import warnings
|
||||||
@@ -12,6 +11,7 @@ from fastapi.middleware.gzip import GZipMiddleware
|
|||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
import lnbits.settings
|
import lnbits.settings
|
||||||
|
from lnbits.core.tasks import register_task_listeners
|
||||||
|
|
||||||
from .commands import db_migrate, handle_assets
|
from .commands import db_migrate, handle_assets
|
||||||
from .core import core_app
|
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"
|
"/core/static", StaticFiles(directory="lnbits/core/static"), name="core_static"
|
||||||
)
|
)
|
||||||
|
|
||||||
origins = ["http://localhost", "http://localhost:5000"]
|
origins = ["*"]
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware, allow_origins=origins, allow_methods=["*"], allow_headers=["*"]
|
||||||
allow_origins=origins,
|
|
||||||
allow_credentials=True,
|
|
||||||
allow_methods=["*"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
g().config = lnbits.settings
|
g().config = lnbits.settings
|
||||||
|
|||||||
+205
-4
@@ -2,10 +2,14 @@ import bitstring # type: ignore
|
|||||||
import re
|
import re
|
||||||
import hashlib
|
import hashlib
|
||||||
from typing import List, NamedTuple, Optional
|
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 import SECP256k1, VerifyingKey # type: ignore
|
||||||
from ecdsa.util import sigdecode_string # type: ignore
|
from ecdsa.util import sigdecode_string # type: ignore
|
||||||
from binascii import unhexlify
|
from binascii import unhexlify
|
||||||
|
import time
|
||||||
|
from decimal import Decimal
|
||||||
|
import embit
|
||||||
|
import secp256k1
|
||||||
|
|
||||||
|
|
||||||
class Route(NamedTuple):
|
class Route(NamedTuple):
|
||||||
@@ -116,6 +120,166 @@ def decode(pr: str) -> Invoice:
|
|||||||
return 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:
|
def _unshorten_amount(amount: str) -> int:
|
||||||
"""Given a shortened amount, return millisatoshis"""
|
"""Given a shortened amount, return millisatoshis"""
|
||||||
# BOLT #11:
|
# BOLT #11:
|
||||||
@@ -146,6 +310,34 @@ def _pull_tagged(stream):
|
|||||||
return (CHARSET[tag], stream.read(length * 5), 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):
|
def _trim_to_bytes(barr):
|
||||||
# Adds a byte if necessary.
|
# Adds a byte if necessary.
|
||||||
b = barr.tobytes()
|
b = barr.tobytes()
|
||||||
@@ -156,9 +348,9 @@ def _trim_to_bytes(barr):
|
|||||||
|
|
||||||
def _readable_scid(short_channel_id: int) -> str:
|
def _readable_scid(short_channel_id: int) -> str:
|
||||||
return "{blockheight}x{transactionindex}x{outputindex}".format(
|
return "{blockheight}x{transactionindex}x{outputindex}".format(
|
||||||
blockheight=((short_channel_id >> 40) & 0xFFFFFF),
|
blockheight=((short_channel_id >> 40) & 0xffffff),
|
||||||
transactionindex=((short_channel_id >> 16) & 0xFFFFFF),
|
transactionindex=((short_channel_id >> 16) & 0xffffff),
|
||||||
outputindex=(short_channel_id & 0xFFFF),
|
outputindex=(short_channel_id & 0xffff),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -167,3 +359,12 @@ def _u5_to_bitarray(arr: List[int]) -> bitstring.BitArray:
|
|||||||
for a in arr:
|
for a in arr:
|
||||||
ret += bitstring.pack("uint:5", a)
|
ret += bitstring.pack("uint:5", a)
|
||||||
return ret
|
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
|
||||||
|
|||||||
+3
-2
@@ -11,7 +11,6 @@ from lnbits.settings import DEFAULT_WALLET_NAME
|
|||||||
from . import db
|
from . import db
|
||||||
from .models import User, Wallet, Payment, BalanceCheck
|
from .models import User, Wallet, Payment, BalanceCheck
|
||||||
|
|
||||||
|
|
||||||
# accounts
|
# accounts
|
||||||
# --------
|
# --------
|
||||||
|
|
||||||
@@ -278,7 +277,9 @@ async def get_payments(
|
|||||||
return [Payment.from_row(row) for row in rows]
|
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
|
# first we delete all invoices older than one month
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
f"""
|
f"""
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ class User(BaseModel):
|
|||||||
extensions: List[str] = []
|
extensions: List[str] = []
|
||||||
wallets: List[Wallet] = []
|
wallets: List[Wallet] = []
|
||||||
password: Optional[str] = None
|
password: Optional[str] = None
|
||||||
|
admin: bool = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def wallet_ids(self) -> List[str]:
|
def wallet_ids(self) -> List[str]:
|
||||||
|
|||||||
+16
-9
@@ -96,8 +96,6 @@ async def pay_invoice(
|
|||||||
raise ValueError("Amount in invoice is too high.")
|
raise ValueError("Amount in invoice is too high.")
|
||||||
|
|
||||||
wallet = await get_wallet(wallet_id, conn=conn)
|
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
|
# put all parameters that don't change here
|
||||||
PaymentKwargs = TypedDict(
|
PaymentKwargs = TypedDict(
|
||||||
@@ -141,11 +139,21 @@ async def pay_invoice(
|
|||||||
**payment_kwargs,
|
**payment_kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
# do the balance check
|
# do the balance check if internal payment
|
||||||
wallet = await get_wallet(wallet_id, conn=conn)
|
if internal_checking_id:
|
||||||
assert wallet
|
wallet = await get_wallet(wallet_id, conn=conn)
|
||||||
if wallet.balance_msat < 0:
|
assert wallet
|
||||||
raise PermissionError("Insufficient balance.")
|
if wallet.balance_msat < 0:
|
||||||
|
raise PermissionError("Insufficient balance.")
|
||||||
|
|
||||||
|
# do the balance check if external payment
|
||||||
|
else:
|
||||||
|
if invoice.amount_msat > wallet.balance_msat - (
|
||||||
|
wallet.balance_msat / 100 * 2
|
||||||
|
):
|
||||||
|
raise PermissionError(
|
||||||
|
"LNbits requires you keep at least 2% reserve to cover potential routing fees."
|
||||||
|
)
|
||||||
|
|
||||||
if internal_checking_id:
|
if internal_checking_id:
|
||||||
# mark the invoice from the other side as not pending anymore
|
# mark the invoice from the other side as not pending anymore
|
||||||
@@ -322,8 +330,7 @@ async def check_invoice_status(
|
|||||||
if not payment.pending:
|
if not payment.pending:
|
||||||
return status
|
return status
|
||||||
if payment.is_out and status.failed:
|
if payment.is_out and status.failed:
|
||||||
print(
|
print(f" - deleting outgoing failed payment {payment.checking_id}: {status}")
|
||||||
f" - deleting outgoing failed payment {payment.checking_id}: {status}")
|
|
||||||
await payment.delete()
|
await payment.delete()
|
||||||
elif not status.pending:
|
elif not status.pending:
|
||||||
print(
|
print(
|
||||||
|
|||||||
@@ -161,14 +161,14 @@ new Vue({
|
|||||||
{
|
{
|
||||||
name: 'sat',
|
name: 'sat',
|
||||||
align: 'right',
|
align: 'right',
|
||||||
label: 'Amount (sat)',
|
label: 'Amount (' + LNBITS_DENOMINATION + ')',
|
||||||
field: 'sat',
|
field: 'sat',
|
||||||
sortable: true
|
sortable: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'fee',
|
name: 'fee',
|
||||||
align: 'right',
|
align: 'right',
|
||||||
label: 'Fee (msat)',
|
label: 'Fee (m' + LNBITS_DENOMINATION + ')',
|
||||||
field: 'fee'
|
field: 'fee'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -185,12 +185,17 @@ new Vue({
|
|||||||
location: window.location
|
location: window.location
|
||||||
},
|
},
|
||||||
balance: 0,
|
balance: 0,
|
||||||
|
credit: 0,
|
||||||
newName: ''
|
newName: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
formattedBalance: function () {
|
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 () {
|
filteredPayments: function () {
|
||||||
var q = this.paymentsTable.filter
|
var q = this.paymentsTable.filter
|
||||||
@@ -249,6 +254,28 @@ new Vue({
|
|||||||
this.parse.data.paymentChecker = null
|
this.parse.data.paymentChecker = null
|
||||||
this.parse.camera.show = false
|
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 () {
|
closeReceiveDialog: function () {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
clearInterval(this.receive.paymentChecker)
|
clearInterval(this.receive.paymentChecker)
|
||||||
@@ -271,7 +298,9 @@ new Vue({
|
|||||||
},
|
},
|
||||||
createInvoice: function () {
|
createInvoice: function () {
|
||||||
this.receive.status = 'loading'
|
this.receive.status = 'loading'
|
||||||
|
if (LNBITS_DENOMINATION != 'sats') {
|
||||||
|
this.receive.data.amount = this.receive.data.amount * 100
|
||||||
|
}
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.createInvoice(
|
.createInvoice(
|
||||||
this.g.wallet,
|
this.g.wallet,
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ async def dispatch_invoice_listener(payment: Payment):
|
|||||||
|
|
||||||
async def dispatch_webhook(payment: Payment):
|
async def dispatch_webhook(payment: Payment):
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
data = payment._asdict()
|
data = payment.dict()
|
||||||
try:
|
try:
|
||||||
r = await client.post(payment.webhook, json=data, timeout=40)
|
r = await client.post(payment.webhook, json=data, timeout=40)
|
||||||
await mark_webhook_sent(payment, r.status_code)
|
await mark_webhook_sent(payment, r.status_code)
|
||||||
|
|||||||
@@ -15,7 +15,51 @@
|
|||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<h3 class="q-my-none">
|
<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>
|
</h3>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<div class="row q-pb-md q-px-md q-col-gutter-md">
|
<div class="row q-pb-md q-px-md q-col-gutter-md">
|
||||||
@@ -141,7 +185,17 @@
|
|||||||
<q-tooltip>{{ props.row.date }}</q-tooltip>
|
<q-tooltip>{{ props.row.date }}</q-tooltip>
|
||||||
{{ props.row.dateFrom }}
|
{{ props.row.dateFrom }}
|
||||||
</q-td>
|
</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 }}
|
{{ props.row.fsat }}
|
||||||
</q-td>
|
</q-td>
|
||||||
<q-td auto-width key="fee" :props="props">
|
<q-td auto-width key="fee" :props="props">
|
||||||
@@ -223,7 +277,7 @@
|
|||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<h6 class="text-subtitle1 q-mt-none q-mb-sm">
|
<h6 class="text-subtitle1 q-mt-none q-mb-sm">
|
||||||
{{ SITE_TITLE }} wallet: <strong><em>{{ wallet.name }}</em></strong>
|
{{ SITE_TITLE }} Wallet: <strong><em>{{ wallet.name }}</em></strong>
|
||||||
</h6>
|
</h6>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
@@ -337,7 +391,20 @@
|
|||||||
<p v-if="receive.lnurl" class="text-h6 text-center q-my-none">
|
<p v-if="receive.lnurl" class="text-h6 text-center q-my-none">
|
||||||
<b>{{receive.lnurl.domain}}</b> is requesting an invoice:
|
<b>{{receive.lnurl.domain}}</b> is requesting an invoice:
|
||||||
</p>
|
</p>
|
||||||
|
{% endraw %} {% if LNBITS_DENOMINATION != 'sats' %}
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.number="receive.data.amount"
|
||||||
|
label="Amount ({{LNBITS_DENOMINATION}}) *"
|
||||||
|
mask="#.##"
|
||||||
|
fill-mask="0"
|
||||||
|
reverse-fill-mask
|
||||||
|
:min="receive.minMax[0]"
|
||||||
|
:max="receive.minMax[1]"
|
||||||
|
:readonly="receive.lnurl && receive.lnurl.fixed"
|
||||||
|
></q-input>
|
||||||
|
{% else %}
|
||||||
<q-select
|
<q-select
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
@@ -351,19 +418,21 @@
|
|||||||
dense
|
dense
|
||||||
v-model.number="receive.data.amount"
|
v-model.number="receive.data.amount"
|
||||||
type="number"
|
type="number"
|
||||||
:label="`Amount (${receive.unit}) *`"
|
label="Amount ({{LNBITS_DENOMINATION}}) *"
|
||||||
:step="receive.unit != 'sat' ? '0.001' : '1'"
|
:step="receive.unit != 'sat' ? '0.001' : '1'"
|
||||||
:min="receive.minMax[0]"
|
:min="receive.minMax[0]"
|
||||||
:max="receive.minMax[1]"
|
:max="receive.minMax[1]"
|
||||||
:readonly="receive.lnurl && receive.lnurl.fixed"
|
:readonly="receive.lnurl && receive.lnurl.fixed"
|
||||||
></q-input>
|
></q-input>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
v-model.trim="receive.data.memo"
|
v-model.trim="receive.data.memo"
|
||||||
label="Memo *"
|
label="Memo"
|
||||||
placeholder="LNbits invoice"
|
|
||||||
></q-input>
|
></q-input>
|
||||||
|
{% raw %}
|
||||||
<div v-if="receive.status == 'pending'" class="row q-mt-lg">
|
<div v-if="receive.status == 'pending'" class="row q-mt-lg">
|
||||||
<q-btn
|
<q-btn
|
||||||
unelevated
|
unelevated
|
||||||
@@ -410,8 +479,13 @@
|
|||||||
<q-dialog v-model="parse.show" @hide="closeParseDialog">
|
<q-dialog v-model="parse.show" @hide="closeParseDialog">
|
||||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||||
<div v-if="parse.invoice">
|
<div v-if="parse.invoice">
|
||||||
{% raw %}
|
<h6 v-if="'{{LNBITS_DENOMINATION}}' != 'sats'" class="q-my-none">
|
||||||
<h6 class="q-my-none">{{ parse.invoice.fsat }} sat</h6>
|
{% 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>
|
<q-separator class="q-my-sm"></q-separator>
|
||||||
<p class="text-wrap">
|
<p class="text-wrap">
|
||||||
<strong>Description:</strong> {{ parse.invoice.description }}<br />
|
<strong>Description:</strong> {{ parse.invoice.description }}<br />
|
||||||
@@ -461,7 +535,7 @@
|
|||||||
<q-form @submit="payLnurl" class="q-gutter-md">
|
<q-form @submit="payLnurl" class="q-gutter-md">
|
||||||
<p v-if="parse.lnurlpay.fixed" class="q-my-none text-h6">
|
<p v-if="parse.lnurlpay.fixed" class="q-my-none text-h6">
|
||||||
<b>{{ parse.lnurlpay.domain }}</b> is requesting {{
|
<b>{{ parse.lnurlpay.domain }}</b> is requesting {{
|
||||||
parse.lnurlpay.maxSendable | msatoshiFormat }} sat
|
parse.lnurlpay.maxSendable | msatoshiFormat }} {{LNBITS_DENOMINATION}}
|
||||||
<span v-if="parse.lnurlpay.commentAllowed > 0">
|
<span v-if="parse.lnurlpay.commentAllowed > 0">
|
||||||
<br />
|
<br />
|
||||||
and a {{parse.lnurlpay.commentAllowed}}-char comment
|
and a {{parse.lnurlpay.commentAllowed}}-char comment
|
||||||
@@ -471,7 +545,8 @@
|
|||||||
<b>{{ parse.lnurlpay.targetUser || parse.lnurlpay.domain }}</b> is
|
<b>{{ parse.lnurlpay.targetUser || parse.lnurlpay.domain }}</b> is
|
||||||
requesting <br />
|
requesting <br />
|
||||||
between <b>{{ parse.lnurlpay.minSendable | msatoshiFormat }}</b> and
|
between <b>{{ parse.lnurlpay.minSendable | msatoshiFormat }}</b> and
|
||||||
<b>{{ parse.lnurlpay.maxSendable | msatoshiFormat }}</b> sat
|
<b>{{ parse.lnurlpay.maxSendable | msatoshiFormat }}</b>
|
||||||
|
{% endraw %} {{LNBITS_DENOMINATION}} {% raw %}
|
||||||
<span v-if="parse.lnurlpay.commentAllowed > 0">
|
<span v-if="parse.lnurlpay.commentAllowed > 0">
|
||||||
<br />
|
<br />
|
||||||
and a {{parse.lnurlpay.commentAllowed}}-char comment
|
and a {{parse.lnurlpay.commentAllowed}}-char comment
|
||||||
@@ -488,16 +563,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
|
{% endraw %}
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
v-model.number="parse.data.amount"
|
v-model.number="parse.data.amount"
|
||||||
type="number"
|
type="number"
|
||||||
label="Amount (sat) *"
|
label="Amount ({{LNBITS_DENOMINATION}}) *"
|
||||||
:min="parse.lnurlpay.minSendable / 1000"
|
:min="parse.lnurlpay.minSendable / 1000"
|
||||||
:max="parse.lnurlpay.maxSendable / 1000"
|
:max="parse.lnurlpay.maxSendable / 1000"
|
||||||
:readonly="parse.lnurlpay.fixed"
|
:readonly="parse.lnurlpay.fixed"
|
||||||
></q-input>
|
></q-input>
|
||||||
|
{% raw %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-8 q-pl-md" v-if="parse.lnurlpay.commentAllowed > 0">
|
<div class="col-8 q-pl-md" v-if="parse.lnurlpay.commentAllowed > 0">
|
||||||
<q-input
|
<q-input
|
||||||
@@ -511,7 +588,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-mt-lg">
|
<div class="row q-mt-lg">
|
||||||
<q-btn unelevated color="primary" type="submit">Send satoshis</q-btn>
|
<q-btn unelevated color="primary" type="submit"
|
||||||
|
>Send {{LNBITS_DENOMINATION}}</q-btn
|
||||||
|
>
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||||
>Cancel</q-btn
|
>Cancel</q-btn
|
||||||
>
|
>
|
||||||
|
|||||||
+81
-20
@@ -3,7 +3,7 @@ import hashlib
|
|||||||
import json
|
import json
|
||||||
from binascii import unhexlify
|
from binascii import unhexlify
|
||||||
from http import HTTPStatus
|
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
|
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
@@ -12,6 +12,7 @@ from fastapi.exceptions import HTTPException
|
|||||||
from fastapi.param_functions import Depends
|
from fastapi.param_functions import Depends
|
||||||
from fastapi.params import Body
|
from fastapi.params import Body
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from pydantic.fields import Field
|
||||||
from sse_starlette.sse import EventSourceResponse
|
from sse_starlette.sse import EventSourceResponse
|
||||||
|
|
||||||
from lnbits import bolt11, lnurl
|
from lnbits import bolt11, lnurl
|
||||||
@@ -25,7 +26,11 @@ from lnbits.decorators import (
|
|||||||
)
|
)
|
||||||
from lnbits.helpers import url_for
|
from lnbits.helpers import url_for
|
||||||
from lnbits.requestvars import g
|
from lnbits.requestvars import g
|
||||||
from lnbits.utils.exchange_rates import currencies, fiat_amount_as_satoshis
|
from lnbits.utils.exchange_rates import (
|
||||||
|
currencies,
|
||||||
|
fiat_amount_as_satoshis,
|
||||||
|
satoshis_amount_as_fiat,
|
||||||
|
)
|
||||||
|
|
||||||
from .. import core_app, db
|
from .. import core_app, db
|
||||||
from ..crud import (
|
from ..crud import (
|
||||||
@@ -33,6 +38,9 @@ from ..crud import (
|
|||||||
get_standalone_payment,
|
get_standalone_payment,
|
||||||
save_balance_check,
|
save_balance_check,
|
||||||
update_wallet,
|
update_wallet,
|
||||||
|
create_payment,
|
||||||
|
get_wallet,
|
||||||
|
update_payment_status,
|
||||||
)
|
)
|
||||||
from ..services import (
|
from ..services import (
|
||||||
InvoiceFailure,
|
InvoiceFailure,
|
||||||
@@ -43,6 +51,8 @@ from ..services import (
|
|||||||
perform_lnurlauth,
|
perform_lnurlauth,
|
||||||
)
|
)
|
||||||
from ..tasks import api_invoice_listeners
|
from ..tasks import api_invoice_listeners
|
||||||
|
from lnbits.settings import LNBITS_ADMIN_USERS
|
||||||
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
|
|
||||||
|
|
||||||
@core_app.get("/api/v1/wallet")
|
@core_app.get("/api/v1/wallet")
|
||||||
@@ -57,6 +67,35 @@ async def api_wallet(wallet: WalletTypeInfo = Depends(get_key_type)):
|
|||||||
return {"name": wallet.wallet.name, "balance": wallet.wallet.balance_msat}
|
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 LNBITS_ADMIN_USERS and 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}")
|
@core_app.put("/api/v1/wallet/{new_name}")
|
||||||
async def api_update_wallet(
|
async def api_update_wallet(
|
||||||
new_name: str, wallet: WalletTypeInfo = Depends(WalletAdminKeyChecker())
|
new_name: str, wallet: WalletTypeInfo = Depends(WalletAdminKeyChecker())
|
||||||
@@ -72,7 +111,9 @@ async def api_update_wallet(
|
|||||||
@core_app.get("/api/v1/payments")
|
@core_app.get("/api/v1/payments")
|
||||||
async def api_payments(wallet: WalletTypeInfo = Depends(get_key_type)):
|
async def api_payments(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||||
await get_payments(wallet_id=wallet.wallet.id, pending=True, complete=True)
|
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:
|
for payment in pendingPayments:
|
||||||
await check_invoice_status(
|
await check_invoice_status(
|
||||||
wallet_id=payment.wallet_id, payment_hash=payment.payment_hash
|
wallet_id=payment.wallet_id, payment_hash=payment.payment_hash
|
||||||
@@ -126,8 +167,8 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
|
|||||||
|
|
||||||
lnurl_response: Union[None, bool, str] = None
|
lnurl_response: Union[None, bool, str] = None
|
||||||
if data.lnurl_callback:
|
if data.lnurl_callback:
|
||||||
if "lnurl_balance_check" in g().data:
|
if "lnurl_balance_check" in data:
|
||||||
save_balance_check(g().wallet.id, data.lnurl_balance_check)
|
save_balance_check(wallet.id, data.lnurl_balance_check)
|
||||||
|
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
try:
|
try:
|
||||||
@@ -138,7 +179,7 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
|
|||||||
"balanceNotify": url_for(
|
"balanceNotify": url_for(
|
||||||
f"/withdraw/notify/{urlparse(data.lnurl_callback).netloc}",
|
f"/withdraw/notify/{urlparse(data.lnurl_callback).netloc}",
|
||||||
external=True,
|
external=True,
|
||||||
wal=g().wallet.id,
|
wal=wallet.id,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
timeout=10,
|
timeout=10,
|
||||||
@@ -193,8 +234,7 @@ async def api_payments_create(
|
|||||||
invoiceData: CreateInvoiceData = Body(...),
|
invoiceData: CreateInvoiceData = Body(...),
|
||||||
):
|
):
|
||||||
if wallet.wallet_type < 0 or wallet.wallet_type > 2:
|
if wallet.wallet_type < 0 or wallet.wallet_type > 2:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="Key is invalid")
|
||||||
status_code=HTTPStatus.BAD_REQUEST, detail="Key is invalid")
|
|
||||||
|
|
||||||
if invoiceData.out is True and wallet.wallet_type == 0:
|
if invoiceData.out is True and wallet.wallet_type == 0:
|
||||||
if not invoiceData.bolt11:
|
if not invoiceData.bolt11:
|
||||||
@@ -249,14 +289,14 @@ async def api_payments_pay_lnurl(
|
|||||||
if invoice.amount_msat != data.amount:
|
if invoice.amount_msat != data.amount:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
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:
|
# if invoice.description_hash != data.description_hash:
|
||||||
raise HTTPException(
|
# raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
# status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail=f"{domain} returned an invalid invoice. Expected description_hash == {data['description_hash']}, got {invoice.description_hash}.",
|
# detail=f"{domain} returned an invalid invoice. Expected description_hash == {data.description_hash}, got {invoice.description_hash}.",
|
||||||
)
|
# )
|
||||||
|
|
||||||
extra = {}
|
extra = {}
|
||||||
|
|
||||||
@@ -374,16 +414,14 @@ async def api_lnurlscan(code: str):
|
|||||||
params.update(callback=url) # with k1 already in it
|
params.update(callback=url) # with k1 already in it
|
||||||
|
|
||||||
lnurlauth_key = g().wallet.lnurlauth_key(domain)
|
lnurlauth_key = g().wallet.lnurlauth_key(domain)
|
||||||
params.update(
|
params.update(pubkey=lnurlauth_key.verifying_key.to_string("compressed").hex())
|
||||||
pubkey=lnurlauth_key.verifying_key.to_string("compressed").hex())
|
|
||||||
else:
|
else:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
r = await client.get(url, timeout=5)
|
r = await client.get(url, timeout=5)
|
||||||
if r.is_error:
|
if r.is_error:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
|
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
|
||||||
detail={"domain": domain,
|
detail={"domain": domain, "message": "failed to get parameters"},
|
||||||
"message": "failed to get parameters"},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -413,8 +451,7 @@ async def api_lnurlscan(code: str):
|
|||||||
|
|
||||||
if tag == "withdrawRequest":
|
if tag == "withdrawRequest":
|
||||||
params.update(kind="withdraw")
|
params.update(kind="withdraw")
|
||||||
params.update(fixed=data["minWithdrawable"]
|
params.update(fixed=data["minWithdrawable"] == data["maxWithdrawable"])
|
||||||
== data["maxWithdrawable"])
|
|
||||||
|
|
||||||
# callback with k1 already in it
|
# callback with k1 already in it
|
||||||
parsed_callback: ParseResult = urlparse(data["callback"])
|
parsed_callback: ParseResult = urlparse(data["callback"])
|
||||||
@@ -501,3 +538,27 @@ async def api_perform_lnurlauth(callback: str):
|
|||||||
@core_app.get("/api/v1/currencies")
|
@core_app.get("/api/v1/currencies")
|
||||||
async def api_list_currencies_available():
|
async def api_list_currencies_available():
|
||||||
return list(currencies.keys())
|
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.to)
|
||||||
|
output["BTC"] = output["sats"] / 100000000
|
||||||
|
return output
|
||||||
|
|||||||
@@ -14,7 +14,12 @@ from lnbits.core import db
|
|||||||
from lnbits.core.models import User
|
from lnbits.core.models import User
|
||||||
from lnbits.decorators import check_user_exists
|
from lnbits.decorators import check_user_exists
|
||||||
from lnbits.helpers import template_renderer, url_for
|
from lnbits.helpers import template_renderer, url_for
|
||||||
from lnbits.settings import LNBITS_ALLOWED_USERS, LNBITS_SITE_TITLE, SERVICE_FEE
|
from lnbits.settings import (
|
||||||
|
LNBITS_ALLOWED_USERS,
|
||||||
|
LNBITS_ADMIN_USERS,
|
||||||
|
LNBITS_SITE_TITLE,
|
||||||
|
SERVICE_FEE,
|
||||||
|
)
|
||||||
|
|
||||||
from ..crud import (
|
from ..crud import (
|
||||||
create_account,
|
create_account,
|
||||||
@@ -43,9 +48,7 @@ async def home(request: Request, lightning: str = None):
|
|||||||
|
|
||||||
|
|
||||||
@core_html_routes.get(
|
@core_html_routes.get(
|
||||||
"/extensions",
|
"/extensions", name="core.extensions", response_class=HTMLResponse
|
||||||
name="core.extensions",
|
|
||||||
response_class=HTMLResponse,
|
|
||||||
)
|
)
|
||||||
async def extensions(
|
async def extensions(
|
||||||
request: Request,
|
request: Request,
|
||||||
@@ -115,6 +118,8 @@ async def wallet(
|
|||||||
return template_renderer().TemplateResponse(
|
return template_renderer().TemplateResponse(
|
||||||
"error.html", {"request": request, "err": "User not authorized."}
|
"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 not wallet_id:
|
||||||
if user.wallets and not wallet_name:
|
if user.wallets and not wallet_name:
|
||||||
wallet = user.wallets[0]
|
wallet = user.wallets[0]
|
||||||
|
|||||||
+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.crud import get_user, get_wallet_for_key
|
||||||
from lnbits.core.models import User, Wallet
|
from lnbits.core.models import User, Wallet
|
||||||
from lnbits.requestvars import g
|
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):
|
class KeyChecker(SecurityBase):
|
||||||
@@ -122,6 +122,7 @@ async def get_key_type(
|
|||||||
# 0: admin
|
# 0: admin
|
||||||
# 1: invoice
|
# 1: invoice
|
||||||
# 2: invalid
|
# 2: invalid
|
||||||
|
pathname = r['path'].split('/')[1]
|
||||||
|
|
||||||
if not api_key_header and not api_key_query:
|
if not api_key_header and not api_key_query:
|
||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
|
||||||
@@ -131,7 +132,10 @@ async def get_key_type(
|
|||||||
try:
|
try:
|
||||||
checker = WalletAdminKeyChecker(api_key=token)
|
checker = WalletAdminKeyChecker(api_key=token)
|
||||||
await checker.__call__(r)
|
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:
|
except HTTPException as e:
|
||||||
if e.status_code == HTTPStatus.BAD_REQUEST:
|
if e.status_code == HTTPStatus.BAD_REQUEST:
|
||||||
raise
|
raise
|
||||||
@@ -143,7 +147,10 @@ async def get_key_type(
|
|||||||
try:
|
try:
|
||||||
checker = WalletInvoiceKeyChecker(api_key=token)
|
checker = WalletInvoiceKeyChecker(api_key=token)
|
||||||
await checker.__call__(r)
|
await checker.__call__(r)
|
||||||
return WalletTypeInfo(1, 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:
|
except HTTPException as e:
|
||||||
if e.status_code == HTTPStatus.BAD_REQUEST:
|
if e.status_code == HTTPStatus.BAD_REQUEST:
|
||||||
raise
|
raise
|
||||||
@@ -172,6 +179,26 @@ async def require_admin_key(
|
|||||||
return wallet
|
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:
|
async def check_user_exists(usr: UUID4) -> User:
|
||||||
g().user = await get_user(usr.hex)
|
g().user = await get_user(usr.hex)
|
||||||
if not g().user:
|
if not g().user:
|
||||||
@@ -184,4 +211,7 @@ async def check_user_exists(usr: UUID4) -> User:
|
|||||||
status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized."
|
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
|
return g().user
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
from typing import List, Optional, Union
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
|
|
||||||
from . import db
|
from . import db
|
||||||
from .models import Copilots, CreateCopilotData
|
from .models import Copilots, CreateCopilotData
|
||||||
from lnbits.helpers import urlsafe_short_hash
|
|
||||||
|
|
||||||
###############COPILOTS##########################
|
###############COPILOTS##########################
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@ async def create_copilot(
|
|||||||
"""
|
"""
|
||||||
INSERT INTO copilot.newer_copilots (
|
INSERT INTO copilot.newer_copilots (
|
||||||
id,
|
id,
|
||||||
user,
|
"user",
|
||||||
lnurl_toggle,
|
lnurl_toggle,
|
||||||
wallet,
|
wallet,
|
||||||
title,
|
title,
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ async def set_address_renewed(address_id: str, duration: int):
|
|||||||
|
|
||||||
|
|
||||||
async def check_address_available(username: str, domain: str):
|
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 = ?",
|
"SELECT COUNT(username) FROM lnaddress.address WHERE username = ? AND domain = ?",
|
||||||
(username, domain),
|
(username, domain),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ async def lnurl_callback(address_id, amount: int = Query(...)):
|
|||||||
amount_received = amount
|
amount_received = amount
|
||||||
|
|
||||||
domain = await get_domain(address.domain)
|
domain = await get_domain(address.domain)
|
||||||
|
|
||||||
base_url = (
|
base_url = (
|
||||||
address.wallet_endpoint[:-1]
|
address.wallet_endpoint[:-1]
|
||||||
if address.wallet_endpoint.endswith("/")
|
if address.wallet_endpoint.endswith("/")
|
||||||
@@ -71,7 +71,9 @@ async def lnurl_callback(address_id, amount: int = Query(...)):
|
|||||||
"out": False,
|
"out": False,
|
||||||
"amount": int(amount_received / 1000),
|
"amount": int(amount_received / 1000),
|
||||||
"description_hash": hashlib.sha256(
|
"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(),
|
).hexdigest(),
|
||||||
"extra": {"tag": f"Payment to {address.username}@{domain.domain}"},
|
"extra": {"tag": f"Payment to {address.username}@{domain.domain}"},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -53,5 +53,5 @@ class Addresses(BaseModel):
|
|||||||
text = f"Payment to {self.username}"
|
text = f"Payment to {self.username}"
|
||||||
identifier = f"{self.username}@{domain}"
|
identifier = f"{self.username}@{domain}"
|
||||||
metadata = [["text/plain", text], ["text/identifier", identifier]]
|
metadata = [["text/plain", text], ["text/identifier", identifier]]
|
||||||
|
|
||||||
return LnurlPayMetadata(json.dumps(metadata))
|
return LnurlPayMetadata(json.dumps(metadata))
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ async def display(domain_id, request: Request):
|
|||||||
|
|
||||||
wallet = await get_wallet(domain.wallet)
|
wallet = await get_wallet(domain.wallet)
|
||||||
url = urlparse(str(request.url))
|
url = urlparse(str(request.url))
|
||||||
|
|
||||||
return lnaddress_renderer().TemplateResponse(
|
return lnaddress_renderer().TemplateResponse(
|
||||||
"lnaddress/display.html",
|
"lnaddress/display.html",
|
||||||
{
|
{
|
||||||
@@ -45,6 +45,6 @@ async def display(domain_id, request: Request):
|
|||||||
"domain_domain": domain.domain,
|
"domain_domain": domain.domain,
|
||||||
"domain_cost": domain.cost,
|
"domain_cost": domain.cost,
|
||||||
"domain_wallet_inkey": wallet.inkey,
|
"domain_wallet_inkey": wallet.inkey,
|
||||||
"root_url": f"{url.scheme}://{url.netloc}"
|
"root_url": f"{url.scheme}://{url.netloc}",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -108,7 +108,9 @@ async def lndhub_payinvoice(
|
|||||||
|
|
||||||
|
|
||||||
@lndhub_ext.get("/ext/balance")
|
@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}}
|
return {"BTC": {"AvailableBalance": wallet.wallet.balance}}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
from pydantic.main import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class createLnurlpos(BaseModel):
|
class createLnurldevice(BaseModel):
|
||||||
title: str
|
title: str
|
||||||
wallet: str
|
wallet: str
|
||||||
currency: str
|
currency: str
|
||||||
|
device: str
|
||||||
|
profit: float
|
||||||
|
|
||||||
|
|
||||||
class lnurlposs(BaseModel):
|
class lnurldevices(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
key: str
|
key: str
|
||||||
title: str
|
title: str
|
||||||
wallet: str
|
wallet: str
|
||||||
currency: str
|
currency: str
|
||||||
|
device: str
|
||||||
|
profit: float
|
||||||
timestamp: str
|
timestamp: str
|
||||||
|
|
||||||
def from_row(cls, row: Row) -> "lnurlposs":
|
def from_row(cls, row: Row) -> "lnurldevices":
|
||||||
return cls(**dict(row))
|
return cls(**dict(row))
|
||||||
|
|
||||||
def lnurl(self, req: Request) -> Lnurl:
|
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)
|
return lnurl_encode(url)
|
||||||
|
|
||||||
async def lnurlpay_metadata(self) -> LnurlPayMetadata:
|
async def lnurlpay_metadata(self) -> LnurlPayMetadata:
|
||||||
return LnurlPayMetadata(json.dumps([["text/plain", self.title]]))
|
return LnurlPayMetadata(json.dumps([["text/plain", self.title]]))
|
||||||
|
|
||||||
class lnurlpospayment(BaseModel):
|
|
||||||
|
class lnurldevicepayment(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
posid: str
|
deviceid: str
|
||||||
payhash: str
|
payhash: str
|
||||||
payload: str
|
payload: str
|
||||||
pin: int
|
pin: int
|
||||||
@@ -45,5 +52,5 @@ class lnurlpospayment(BaseModel):
|
|||||||
timestamp: str
|
timestamp: str
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_row(cls, row: Row) -> "lnurlpospayment":
|
def from_row(cls, row: Row) -> "lnurldevicepayment":
|
||||||
return cls(**dict(row))
|
return cls(**dict(row))
|
||||||
+35
-24
@@ -1,10 +1,10 @@
|
|||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<p>
|
<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
|
Build your own here
|
||||||
<a href="https://github.com/arcbtc/LNURLPoS"
|
<a href="https://github.com/arcbtc/bitcoinpos"
|
||||||
>https://github.com/arcbtc/LNURLPoS</a
|
>https://github.com/arcbtc/bitcoinpos</a
|
||||||
><br />
|
><br />
|
||||||
<small>
|
<small>
|
||||||
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
|
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
|
||||||
@@ -21,12 +21,12 @@
|
|||||||
group="api"
|
group="api"
|
||||||
dense
|
dense
|
||||||
expand-separator
|
expand-separator
|
||||||
label="Create lnurlpos"
|
label="Create lnurldevice"
|
||||||
>
|
>
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<code
|
<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>
|
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||||
@@ -36,10 +36,10 @@
|
|||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||||
Returns 200 OK (application/json)
|
Returns 200 OK (application/json)
|
||||||
</h5>
|
</h5>
|
||||||
<code>[<lnurlpos_object>, ...]</code>
|
<code>[<lnurldevice_object>, ...]</code>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X POST {{ request.base_url }}api/v1/lnurlpos -d '{"title":
|
>curl -X POST {{ request.base_url }}api/v1/lnurldevice -d '{"title":
|
||||||
<string>, "message":<string>, "currency":
|
<string>, "message":<string>, "currency":
|
||||||
<integer>}' -H "Content-type: application/json" -H "X-Api-Key:
|
<integer>}' -H "Content-type: application/json" -H "X-Api-Key:
|
||||||
{{user.wallets[0].adminkey }}"
|
{{user.wallets[0].adminkey }}"
|
||||||
@@ -51,13 +51,13 @@
|
|||||||
group="api"
|
group="api"
|
||||||
dense
|
dense
|
||||||
expand-separator
|
expand-separator
|
||||||
label="Update lnurlpos"
|
label="Update lnurldevice"
|
||||||
>
|
>
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<code
|
<code
|
||||||
><span class="text-blue">PUT</span>
|
><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>
|
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||||
@@ -67,25 +67,30 @@
|
|||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||||
Returns 200 OK (application/json)
|
Returns 200 OK (application/json)
|
||||||
</h5>
|
</h5>
|
||||||
<code>[<lnurlpos_object>, ...]</code>
|
<code>[<lnurldevice_object>, ...]</code>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X POST {{ request.base_url
|
>curl -X POST {{ request.base_url
|
||||||
}}api/v1/lnurlpos/<lnurlpos_id> -d ''{"title": <string>,
|
}}api/v1/lnurlpos/<lnurldevice_id> -d ''{"title":
|
||||||
"message":<string>, "currency": <integer>} -H
|
<string>, "message":<string>, "currency":
|
||||||
"Content-type: application/json" -H "X-Api-Key:
|
<integer>} -H "Content-type: application/json" -H "X-Api-Key:
|
||||||
{{user.wallets[0].adminkey }}"
|
{{user.wallets[0].adminkey }}"
|
||||||
</code>
|
</code>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-expansion-item>
|
</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>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<code
|
<code
|
||||||
><span class="text-blue">GET</span>
|
><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>
|
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||||
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
||||||
@@ -95,21 +100,27 @@
|
|||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||||
Returns 200 OK (application/json)
|
Returns 200 OK (application/json)
|
||||||
</h5>
|
</h5>
|
||||||
<code>[<lnurlpos_object>, ...]</code>
|
<code>[<lnurldevice_object>, ...]</code>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X GET {{ request.base_url
|
>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 }}"
|
user.wallets[0].inkey }}"
|
||||||
</code>
|
</code>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-expansion-item>
|
</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>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<code
|
<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>
|
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||||
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
||||||
@@ -119,11 +130,11 @@
|
|||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||||
Returns 200 OK (application/json)
|
Returns 200 OK (application/json)
|
||||||
</h5>
|
</h5>
|
||||||
<code>[<lnurlpos_object>, ...]</code>
|
<code>[<lnurldevice_object>, ...]</code>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X GET {{ request.base_url }}api/v1/lnurlposs -H "X-Api-Key:
|
>curl -X GET {{ request.base_url }}api/v1/lnurldevices -H
|
||||||
{{ user.wallets[0].inkey }}"
|
"X-Api-Key: {{ user.wallets[0].inkey }}"
|
||||||
</code>
|
</code>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
@@ -139,7 +150,7 @@
|
|||||||
<q-card-section>
|
<q-card-section>
|
||||||
<code
|
<code
|
||||||
><span class="text-pink">DELETE</span>
|
><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>
|
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
<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>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X DELETE {{ request.base_url
|
>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 }}"
|
user.wallets[0].adminkey }}"
|
||||||
</code>
|
</code>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
+148
-88
@@ -8,8 +8,8 @@
|
|||||||
<q-btn
|
<q-btn
|
||||||
unelevated
|
unelevated
|
||||||
color="primary"
|
color="primary"
|
||||||
@click="formDialoglnurlpos.show = true"
|
@click="formDialoglnurldevice.show = true"
|
||||||
>New LNURLPoS instance
|
>New LNURLDevice instance
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div class="row items-center no-wrap q-mb-md">
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h5 class="text-subtitle1 q-my-none">lNURLPoS</h5>
|
<h5 class="text-subtitle1 q-my-none">lNURLdevice</h5>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
<q-icon name="search"></q-icon>
|
<q-icon name="search"></q-icon>
|
||||||
</template>
|
</template>
|
||||||
</q-input>
|
</q-input>
|
||||||
<q-btn flat color="grey" @click="exportlnurlposCSV"
|
<q-btn flat color="grey" @click="exportlnurldeviceCSV"
|
||||||
>Export to CSV</q-btn
|
>Export to CSV</q-btn
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@@ -41,10 +41,10 @@
|
|||||||
<q-table
|
<q-table
|
||||||
flat
|
flat
|
||||||
dense
|
dense
|
||||||
:data="lnurlposLinks"
|
:data="lnurldeviceLinks"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
:columns="lnurlpossTable.columns"
|
:columns="lnurldevicesTable.columns"
|
||||||
:pagination.sync="lnurlpossTable.pagination"
|
:pagination.sync="lnurldevicesTable.pagination"
|
||||||
:filter="filter"
|
:filter="filter"
|
||||||
>
|
>
|
||||||
<template v-slot:header="props">
|
<template v-slot:header="props">
|
||||||
@@ -72,11 +72,11 @@
|
|||||||
flat
|
flat
|
||||||
dense
|
dense
|
||||||
size="xs"
|
size="xs"
|
||||||
@click="deletelnurlposLink(props.row.id)"
|
@click="deletelnurldeviceLink(props.row.id)"
|
||||||
icon="cancel"
|
icon="cancel"
|
||||||
color="pink"
|
color="pink"
|
||||||
>
|
>
|
||||||
<q-tooltip> Delete LNURLPoS </q-tooltip>
|
<q-tooltip> Delete LNURLDevice </q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</q-td>
|
</q-td>
|
||||||
<q-td>
|
<q-td>
|
||||||
@@ -84,11 +84,11 @@
|
|||||||
flat
|
flat
|
||||||
dense
|
dense
|
||||||
size="xs"
|
size="xs"
|
||||||
@click="openlnurlposSettings(props.row.id)"
|
@click="openlnurldeviceSettings(props.row.id)"
|
||||||
icon="perm_data_setting"
|
icon="perm_data_setting"
|
||||||
color="primary"
|
color="primary"
|
||||||
>
|
>
|
||||||
<q-tooltip> LNURLPoS Settings </q-tooltip>
|
<q-tooltip> LNURLDevice Settings </q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</q-td>
|
</q-td>
|
||||||
<q-td
|
<q-td
|
||||||
@@ -112,46 +112,57 @@
|
|||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<h6 class="text-subtitle1 q-my-none">
|
<h6 class="text-subtitle1 q-my-none">
|
||||||
{{SITE_TITLE}} LNURLPoS Extension
|
{{SITE_TITLE}} LNURLDevice Extension
|
||||||
</h6>
|
</h6>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<q-separator></q-separator>
|
<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-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-dialog
|
<q-dialog
|
||||||
v-model="settingsDialog.show"
|
v-model="settingsDialog.show"
|
||||||
position="top"
|
deviceition="top"
|
||||||
@hide="closeFormDialog"
|
@hide="closeFormDialog"
|
||||||
>
|
>
|
||||||
<q-card
|
<q-card
|
||||||
style="width: 700px; max-width: 80vw"
|
style="width: 700px; max-width: 80vw"
|
||||||
class="q-pa-lg q-pt-xl lnbits__dialog-card"
|
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">
|
<div class="text-subtitle2">
|
||||||
{% raw %} String baseURL =
|
<small> </small>
|
||||||
"{{location}}/lnurlpos/api/v1/lnurl/{{settingsDialog.data.id}}";<br />
|
|
||||||
String key = "{{settingsDialog.data.key}}";<br />
|
|
||||||
String currency = "{{settingsDialog.data.currency}}";{% endraw %}
|
|
||||||
</div>
|
</div>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
||||||
<q-dialog
|
<q-dialog
|
||||||
v-model="formDialoglnurlpos.show"
|
v-model="formDialoglnurldevice.show"
|
||||||
position="top"
|
deviceition="top"
|
||||||
@hide="closeFormDialog"
|
@hide="closeFormDialog"
|
||||||
>
|
>
|
||||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
<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
|
<q-input
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
v-model.trim="formDialoglnurlpos.data.title"
|
v-model.trim="formDialoglnurldevice.data.title"
|
||||||
type="text"
|
type="text"
|
||||||
label="Title"
|
label="Title"
|
||||||
></q-input>
|
></q-input>
|
||||||
@@ -160,7 +171,7 @@
|
|||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
emit-value
|
emit-value
|
||||||
v-model="formDialoglnurlpos.data.wallet"
|
v-model="formDialoglnurldevice.data.wallet"
|
||||||
:options="g.user.walletOptions"
|
:options="g.user.walletOptions"
|
||||||
label="Wallet *"
|
label="Wallet *"
|
||||||
></q-select>
|
></q-select>
|
||||||
@@ -168,32 +179,46 @@
|
|||||||
<q-select
|
<q-select
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
v-model.trim="formDialoglnurlpos.data.currency"
|
v-model.trim="formDialoglnurldevice.data.currency"
|
||||||
type="text"
|
type="text"
|
||||||
label="Fiat currency for PoS"
|
label="Fiat currency for device"
|
||||||
:options="currency"
|
:options="currency"
|
||||||
></q-select>
|
></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">
|
<div class="row q-mt-lg">
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="formDialoglnurlpos.data.id"
|
v-if="formDialoglnurldevice.data.id"
|
||||||
unelevated
|
unelevated
|
||||||
color="primary"
|
color="primary"
|
||||||
:disable="
|
:disable="
|
||||||
formDialoglnurlpos.data.title == ''"
|
formDialoglnurldevice.data.title == ''"
|
||||||
type="submit"
|
type="submit"
|
||||||
>Update lnurlpos</q-btn
|
>Update lnurldevice</q-btn
|
||||||
>
|
>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-else
|
v-else
|
||||||
unelevated
|
unelevated
|
||||||
color="primary"
|
color="primary"
|
||||||
:disable="
|
:disable="
|
||||||
formDialoglnurlpos.data.title == ''"
|
formDialoglnurldevice.data.title == ''"
|
||||||
type="submit"
|
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
|
>Cancel</q-btn
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@@ -206,7 +231,7 @@
|
|||||||
<script>
|
<script>
|
||||||
Vue.component(VueQrcode.name, VueQrcode)
|
Vue.component(VueQrcode.name, VueQrcode)
|
||||||
|
|
||||||
var maplnurlpos = obj => {
|
var maplnurldevice = obj => {
|
||||||
obj._data = _.clone(obj)
|
obj._data = _.clone(obj)
|
||||||
obj.theTime = obj.time * 60 - (Date.now() / 1000 - obj.timestamp)
|
obj.theTime = obj.time * 60 - (Date.now() / 1000 - obj.timestamp)
|
||||||
obj.time = obj.time + 'mins'
|
obj.time = obj.time + 'mins'
|
||||||
@@ -230,9 +255,19 @@
|
|||||||
location: window.location.hostname,
|
location: window.location.hostname,
|
||||||
filter: '',
|
filter: '',
|
||||||
currency: 'USD',
|
currency: 'USD',
|
||||||
lnurlposLinks: [],
|
lnurldeviceLinks: [],
|
||||||
lnurlposLinksObj: [],
|
lnurldeviceLinksObj: [],
|
||||||
lnurlpossTable: {
|
devices: [
|
||||||
|
{
|
||||||
|
label: 'PoS',
|
||||||
|
value: 'pos'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'ATM',
|
||||||
|
value: 'atm'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
lnurldevicesTable: {
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
name: 'title',
|
name: 'title',
|
||||||
@@ -258,6 +293,18 @@
|
|||||||
label: 'wallet',
|
label: 'wallet',
|
||||||
field: 'wallet'
|
field: 'wallet'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'device',
|
||||||
|
align: 'left',
|
||||||
|
label: 'device',
|
||||||
|
field: 'device'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'profit',
|
||||||
|
align: 'left',
|
||||||
|
label: 'profit',
|
||||||
|
field: 'profit'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'currency',
|
name: 'currency',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
@@ -269,7 +316,7 @@
|
|||||||
rowsPerPage: 10
|
rowsPerPage: 10
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
passedlnurlpos: {},
|
passedlnurldevice: {},
|
||||||
settingsDialog: {
|
settingsDialog: {
|
||||||
show: false,
|
show: false,
|
||||||
data: {}
|
data: {}
|
||||||
@@ -278,13 +325,15 @@
|
|||||||
show: false,
|
show: false,
|
||||||
data: {}
|
data: {}
|
||||||
},
|
},
|
||||||
formDialoglnurlpos: {
|
formDialoglnurldevice: {
|
||||||
show: false,
|
show: false,
|
||||||
data: {
|
data: {
|
||||||
lnurl_toggle: false,
|
lnurl_toggle: false,
|
||||||
show_message: false,
|
show_message: false,
|
||||||
show_ack: false,
|
show_ack: false,
|
||||||
show_price: 'None',
|
show_price: 'None',
|
||||||
|
device: 'pos',
|
||||||
|
profit: 2,
|
||||||
title: ''
|
title: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -295,33 +344,33 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
cancellnurlpos: function (data) {
|
cancellnurldevice: function (data) {
|
||||||
var self = this
|
var self = this
|
||||||
self.formDialoglnurlpos.show = false
|
self.formDialoglnurldevice.show = false
|
||||||
self.clearFormDialoglnurlpos()
|
self.clearFormDialoglnurldevice()
|
||||||
},
|
},
|
||||||
closeFormDialog: function () {
|
closeFormDialog: function () {
|
||||||
this.clearFormDialoglnurlpos()
|
this.clearFormDialoglnurldevice()
|
||||||
this.formDialog.data = {
|
this.formDialog.data = {
|
||||||
is_unique: false
|
is_unique: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
sendFormDatalnurlpos: function () {
|
sendFormDatalnurldevice: function () {
|
||||||
var self = this
|
var self = this
|
||||||
if (self.formDialoglnurlpos.data.id) {
|
if (self.formDialoglnurldevice.data.id) {
|
||||||
this.updatelnurlpos(
|
this.updatelnurldevice(
|
||||||
self.g.user.wallets[0].adminkey,
|
self.g.user.wallets[0].adminkey,
|
||||||
self.formDialoglnurlpos.data
|
self.formDialoglnurldevice.data
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
this.createlnurlpos(
|
this.createlnurldevice(
|
||||||
self.g.user.wallets[0].adminkey,
|
self.g.user.wallets[0].adminkey,
|
||||||
self.formDialoglnurlpos.data
|
self.formDialoglnurldevice.data
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
createlnurlpos: function (wallet, data) {
|
createlnurldevice: function (wallet, data) {
|
||||||
var self = this
|
var self = this
|
||||||
var updatedData = {}
|
var updatedData = {}
|
||||||
for (const property in data) {
|
for (const property in data) {
|
||||||
@@ -330,86 +379,91 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request('POST', '/lnurlpos/api/v1/lnurlpos', wallet, updatedData)
|
.request('POST', '/lnurldevice/api/v1/lnurlpos', wallet, updatedData)
|
||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
self.lnurlposLinks.push(maplnurlpos(response.data))
|
self.lnurldeviceLinks.push(maplnurldevice(response.data))
|
||||||
self.formDialoglnurlpos.show = false
|
self.formDialoglnurldevice.show = false
|
||||||
self.clearFormDialoglnurlpos()
|
self.clearFormDialoglnurldevice()
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
getlnurlposs: function () {
|
getlnurldevices: function () {
|
||||||
var self = this
|
var self = this
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request(
|
.request(
|
||||||
'GET',
|
'GET',
|
||||||
'/lnurlpos/api/v1/lnurlpos',
|
'/lnurldevice/api/v1/lnurlpos',
|
||||||
self.g.user.wallets[0].adminkey
|
self.g.user.wallets[0].adminkey
|
||||||
)
|
)
|
||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
self.lnurlposLinks = response.data.map(maplnurlpos)
|
self.lnurldeviceLinks = response.data.map(maplnurldevice)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
getlnurlpos: function (lnurlpos_id) {
|
getlnurldevice: function (lnurldevice_id) {
|
||||||
var self = this
|
var self = this
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request(
|
.request(
|
||||||
'GET',
|
'GET',
|
||||||
'/lnurlpos/api/v1/lnurlpos/' + lnurlpos_id,
|
'/lnurldevice/api/v1/lnurlpos/' + lnurldevice_id,
|
||||||
self.g.user.wallets[0].adminkey
|
self.g.user.wallets[0].adminkey
|
||||||
)
|
)
|
||||||
.then(function (response) {
|
.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)
|
localStorage.setItem('inkey', self.g.user.wallets[0].inkey)
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
deletelnurlposLink: function (lnurlposId) {
|
deletelnurldeviceLink: function (lnurldeviceId) {
|
||||||
var self = this
|
var self = this
|
||||||
var link = _.findWhere(this.lnurlposLinks, {id: lnurlposId})
|
var link = _.findWhere(this.lnurldeviceLinks, {id: lnurldeviceId})
|
||||||
LNbits.utils
|
LNbits.utils
|
||||||
.confirmDialog('Are you sure you want to delete this pay link?')
|
.confirmDialog('Are you sure you want to delete this pay link?')
|
||||||
.onOk(function () {
|
.onOk(function () {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request(
|
.request(
|
||||||
'DELETE',
|
'DELETE',
|
||||||
'/lnurlpos/api/v1/lnurlpos/' + lnurlposId,
|
'/lnurldevice/api/v1/lnurlpos/' + lnurldeviceId,
|
||||||
self.g.user.wallets[0].adminkey
|
self.g.user.wallets[0].adminkey
|
||||||
)
|
)
|
||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
self.lnurlposLinks = _.reject(self.lnurlposLinks, function (
|
self.lnurldeviceLinks = _.reject(
|
||||||
obj
|
self.lnurldeviceLinks,
|
||||||
) {
|
function (obj) {
|
||||||
return obj.id === lnurlposId
|
return obj.id === lnurldeviceId
|
||||||
})
|
}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
openUpdatelnurlposLink: function (lnurlposId) {
|
openUpdatelnurldeviceLink: function (lnurldeviceId) {
|
||||||
var self = this
|
var self = this
|
||||||
var lnurlpos = _.findWhere(this.lnurlposLinks, {id: lnurlposId})
|
var lnurldevice = _.findWhere(this.lnurldeviceLinks, {
|
||||||
self.formDialoglnurlpos.data = _.clone(lnurlpos._data)
|
id: lnurldeviceId
|
||||||
self.formDialoglnurlpos.show = true
|
})
|
||||||
|
self.formDialoglnurldevice.data = _.clone(lnurldevice._data)
|
||||||
|
self.formDialoglnurldevice.show = true
|
||||||
},
|
},
|
||||||
openlnurlposSettings: function (lnurlposId) {
|
openlnurldeviceSettings: function (lnurldeviceId) {
|
||||||
var self = this
|
var self = this
|
||||||
var lnurlpos = _.findWhere(this.lnurlposLinks, {id: lnurlposId})
|
var lnurldevice = _.findWhere(this.lnurldeviceLinks, {
|
||||||
self.settingsDialog.data = _.clone(lnurlpos._data)
|
id: lnurldeviceId
|
||||||
|
})
|
||||||
|
self.settingsDialog.data = _.clone(lnurldevice._data)
|
||||||
self.settingsDialog.show = true
|
self.settingsDialog.show = true
|
||||||
},
|
},
|
||||||
updatelnurlpos: function (wallet, data) {
|
updatelnurldevice: function (wallet, data) {
|
||||||
var self = this
|
var self = this
|
||||||
var updatedData = {}
|
var updatedData = {}
|
||||||
for (const property in data) {
|
for (const property in data) {
|
||||||
@@ -421,24 +475,27 @@
|
|||||||
LNbits.api
|
LNbits.api
|
||||||
.request(
|
.request(
|
||||||
'PUT',
|
'PUT',
|
||||||
'/lnurlpos/api/v1/lnurlpos/' + updatedData.id,
|
'/lnurldevice/api/v1/lnurlpos/' + updatedData.id,
|
||||||
wallet,
|
wallet,
|
||||||
updatedData
|
updatedData
|
||||||
)
|
)
|
||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
self.lnurlposLinks = _.reject(self.lnurlposLinks, function (obj) {
|
self.lnurldeviceLinks = _.reject(
|
||||||
return obj.id === updatedData.id
|
self.lnurldeviceLinks,
|
||||||
})
|
function (obj) {
|
||||||
self.lnurlposLinks.push(maplnurlpos(response.data))
|
return obj.id === updatedData.id
|
||||||
self.formDialoglnurlpos.show = false
|
}
|
||||||
self.clearFormDialoglnurlpos()
|
)
|
||||||
|
self.lnurldeviceLinks.push(maplnurldevice(response.data))
|
||||||
|
self.formDialoglnurldevice.show = false
|
||||||
|
self.clearFormDialoglnurldevice()
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
clearFormDialoglnurlpos() {
|
clearFormDialoglnurldevice() {
|
||||||
this.formDialoglnurlpos.data = {
|
this.formDialoglnurldevice.data = {
|
||||||
lnurl_toggle: false,
|
lnurl_toggle: false,
|
||||||
show_message: false,
|
show_message: false,
|
||||||
show_ack: false,
|
show_ack: false,
|
||||||
@@ -446,15 +503,18 @@
|
|||||||
title: ''
|
title: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
exportlnurlposCSV: function () {
|
exportlnurldeviceCSV: function () {
|
||||||
var self = this
|
var self = this
|
||||||
LNbits.utils.exportCSV(self.lnurlpossTable.columns, this.lnurlposLinks)
|
LNbits.utils.exportCSV(
|
||||||
|
self.lnurldevicesTable.columns,
|
||||||
|
this.lnurldeviceLinks
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created: function () {
|
created: function () {
|
||||||
var self = this
|
var self = this
|
||||||
var getlnurlposs = this.getlnurlposs
|
var getlnurldevices = this.getlnurldevices
|
||||||
getlnurlposs()
|
getlnurldevices()
|
||||||
self.location = [
|
self.location = [
|
||||||
window.location.protocol,
|
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
|
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)
|
@lnurlp_ext.post("/api/v1/links", status_code=HTTPStatus.CREATED)
|
||||||
|
|||||||
@@ -13,10 +13,12 @@ lnurlpayout_ext: APIRouter = APIRouter(prefix="/lnurlpayout", tags=["lnurlpayout
|
|||||||
def lnurlpayout_renderer():
|
def lnurlpayout_renderer():
|
||||||
return template_renderer(["lnbits/extensions/lnurlpayout/templates"])
|
return template_renderer(["lnbits/extensions/lnurlpayout/templates"])
|
||||||
|
|
||||||
|
|
||||||
from .tasks import wait_for_paid_invoices
|
from .tasks import wait_for_paid_invoices
|
||||||
from .views import * # noqa
|
from .views import * # noqa
|
||||||
from .views_api import * # noqa
|
from .views_api import * # noqa
|
||||||
|
|
||||||
|
|
||||||
def lnurlpayout_start():
|
def lnurlpayout_start():
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
|
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
|
||||||
|
|||||||
@@ -6,14 +6,23 @@ from . import db
|
|||||||
from .models import lnurlpayout, CreateLnurlPayoutData
|
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()
|
lnurlpayout_id = urlsafe_short_hash()
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO lnurlpayout.lnurlpayouts (id, title, wallet, admin_key, lnurlpay, threshold)
|
INSERT INTO lnurlpayout.lnurlpayouts (id, title, wallet, admin_key, lnurlpay, threshold)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
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)
|
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]:
|
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
|
return lnurlpayout(**row) if row else None
|
||||||
|
|
||||||
|
|
||||||
async def get_lnurlpayout_from_wallet(wallet_id: str) -> Optional[lnurlpayout]:
|
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
|
return lnurlpayout(**row) if row else None
|
||||||
|
|
||||||
|
|
||||||
async def get_lnurlpayouts(wallet_ids: Union[str, List[str]]) -> List[lnurlpayout]:
|
async def get_lnurlpayouts(wallet_ids: Union[str, List[str]]) -> List[lnurlpayout]:
|
||||||
if isinstance(wallet_ids, str):
|
if isinstance(wallet_ids, str):
|
||||||
wallet_ids = [wallet_ids]
|
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:
|
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
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class CreateLnurlPayoutData(BaseModel):
|
class CreateLnurlPayoutData(BaseModel):
|
||||||
title: str
|
title: str
|
||||||
lnurlpay: str
|
lnurlpay: str
|
||||||
threshold: int
|
threshold: int
|
||||||
|
|
||||||
|
|
||||||
class lnurlpayout(BaseModel):
|
class lnurlpayout(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
title: str
|
title: str
|
||||||
|
|||||||
@@ -26,28 +26,33 @@ async def on_invoice_paid(payment: Payment) -> None:
|
|||||||
# Check its got a payout associated with it
|
# Check its got a payout associated with it
|
||||||
lnurlpayout_link = await get_lnurlpayout_from_wallet(payment.wallet_id)
|
lnurlpayout_link = await get_lnurlpayout_from_wallet(payment.wallet_id)
|
||||||
if lnurlpayout_link:
|
if lnurlpayout_link:
|
||||||
|
|
||||||
# Check the wallet balance is more than the threshold
|
# Check the wallet balance is more than the threshold
|
||||||
|
|
||||||
wallet = await get_wallet(lnurlpayout_link.wallet)
|
wallet = await get_wallet(lnurlpayout_link.wallet)
|
||||||
if wallet.balance < lnurlpayout_link.threshold + (lnurlpayout_link.threshold*0.02):
|
if wallet.balance < lnurlpayout_link.threshold + (
|
||||||
|
lnurlpayout_link.threshold * 0.02
|
||||||
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get the invoice from the LNURL to pay
|
# Get the invoice from the LNURL to pay
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
try:
|
try:
|
||||||
url = await api_payments_decode({"data": lnurlpayout_link.lnurlpay})
|
url = await api_payments_decode({"data": lnurlpayout_link.lnurlpay})
|
||||||
if str(url["domain"])[0:4] != "http":
|
if str(url["domain"])[0:4] != "http":
|
||||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="LNURL broken")
|
raise HTTPException(
|
||||||
try:
|
status_code=HTTPStatus.FORBIDDEN, detail="LNURL broken"
|
||||||
r = await client.get(
|
|
||||||
str(url["domain"]),
|
|
||||||
timeout=40,
|
|
||||||
)
|
)
|
||||||
|
try:
|
||||||
|
r = await client.get(str(url["domain"]), timeout=40)
|
||||||
res = r.json()
|
res = r.json()
|
||||||
try:
|
try:
|
||||||
r = await client.get(
|
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,
|
timeout=40,
|
||||||
)
|
)
|
||||||
res = r.json()
|
res = r.json()
|
||||||
@@ -65,6 +70,9 @@ async def on_invoice_paid(payment: Payment) -> None:
|
|||||||
except (httpx.ConnectError, httpx.RequestError):
|
except (httpx.ConnectError, httpx.RequestError):
|
||||||
return
|
return
|
||||||
except Exception:
|
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:
|
except:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -10,10 +10,17 @@ from lnbits.core.views.api import api_payment, api_payments_decode
|
|||||||
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
||||||
|
|
||||||
from . import lnurlpayout_ext
|
from . import lnurlpayout_ext
|
||||||
from .crud import create_lnurlpayout, delete_lnurlpayout, get_lnurlpayout, get_lnurlpayouts, get_lnurlpayout_from_wallet
|
from .crud import (
|
||||||
|
create_lnurlpayout,
|
||||||
|
delete_lnurlpayout,
|
||||||
|
get_lnurlpayout,
|
||||||
|
get_lnurlpayouts,
|
||||||
|
get_lnurlpayout_from_wallet,
|
||||||
|
)
|
||||||
from .models import lnurlpayout, CreateLnurlPayoutData
|
from .models import lnurlpayout, CreateLnurlPayoutData
|
||||||
from .tasks import on_invoice_paid
|
from .tasks import on_invoice_paid
|
||||||
|
|
||||||
|
|
||||||
@lnurlpayout_ext.get("/api/v1/lnurlpayouts", status_code=HTTPStatus.OK)
|
@lnurlpayout_ext.get("/api/v1/lnurlpayouts", status_code=HTTPStatus.OK)
|
||||||
async def api_lnurlpayouts(
|
async def api_lnurlpayouts(
|
||||||
all_wallets: bool = Query(None), wallet: WalletTypeInfo = Depends(get_key_type)
|
all_wallets: bool = Query(None), wallet: WalletTypeInfo = Depends(get_key_type)
|
||||||
@@ -28,23 +35,33 @@ async def api_lnurlpayouts(
|
|||||||
@lnurlpayout_ext.post("/api/v1/lnurlpayouts", status_code=HTTPStatus.CREATED)
|
@lnurlpayout_ext.post("/api/v1/lnurlpayouts", status_code=HTTPStatus.CREATED)
|
||||||
async def api_lnurlpayout_create(
|
async def api_lnurlpayout_create(
|
||||||
data: CreateLnurlPayoutData, wallet: WalletTypeInfo = Depends(get_key_type)
|
data: CreateLnurlPayoutData, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||||
):
|
):
|
||||||
if await get_lnurlpayout_from_wallet(wallet.wallet.id):
|
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
|
return
|
||||||
url = await api_payments_decode({"data": data.lnurlpay})
|
url = await api_payments_decode({"data": data.lnurlpay})
|
||||||
if "domain" not in url:
|
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
|
return
|
||||||
if str(url["domain"])[0:4] != "http":
|
if str(url["domain"])[0:4] != "http":
|
||||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not valid LNURL")
|
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not valid LNURL")
|
||||||
return
|
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:
|
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
|
||||||
return lnurlpayout.dict()
|
return lnurlpayout.dict()
|
||||||
|
|
||||||
|
|
||||||
@lnurlpayout_ext.delete("/api/v1/lnurlpayouts/{lnurlpayout_id}")
|
@lnurlpayout_ext.delete("/api/v1/lnurlpayouts/{lnurlpayout_id}")
|
||||||
async def api_lnurlpayout_delete(
|
async def api_lnurlpayout_delete(
|
||||||
lnurlpayout_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
|
lnurlpayout_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||||
@@ -57,26 +74,34 @@ async def api_lnurlpayout_delete(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if lnurlpayout.wallet != wallet.wallet.id:
|
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)
|
await delete_lnurlpayout(lnurlpayout_id)
|
||||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
@lnurlpayout_ext.get("/api/v1/lnurlpayouts/{lnurlpayout_id}", status_code=HTTPStatus.OK)
|
@lnurlpayout_ext.get("/api/v1/lnurlpayouts/{lnurlpayout_id}", status_code=HTTPStatus.OK)
|
||||||
async def api_lnurlpayout_check(
|
async def api_lnurlpayout_check(
|
||||||
lnurlpayout_id: str, wallet: WalletTypeInfo = Depends(get_key_type)
|
lnurlpayout_id: str, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||||
):
|
):
|
||||||
lnurlpayout = await get_lnurlpayout(lnurlpayout_id)
|
lnurlpayout = await get_lnurlpayout(lnurlpayout_id)
|
||||||
payments = await get_payments(
|
payments = await get_payments(
|
||||||
wallet_id=lnurlpayout.wallet, complete=True, pending=False, outgoing=True, incoming=True
|
wallet_id=lnurlpayout.wallet,
|
||||||
|
complete=True,
|
||||||
|
pending=False,
|
||||||
|
outgoing=True,
|
||||||
|
incoming=True,
|
||||||
)
|
)
|
||||||
result = await on_invoice_paid(payments[0])
|
result = await on_invoice_paid(payments[0])
|
||||||
return
|
return
|
||||||
|
|
||||||
# get payouts func
|
|
||||||
# lnurlpayouts = await get_lnurlpayouts(wallet_ids)
|
# get payouts func
|
||||||
# for lnurlpayout in lnurlpayouts:
|
# lnurlpayouts = await get_lnurlpayouts(wallet_ids)
|
||||||
# payments = await get_payments(
|
# for lnurlpayout in lnurlpayouts:
|
||||||
# wallet_id=lnurlpayout.wallet, complete=True, pending=False, outgoing=True, incoming=True
|
# payments = await get_payments(
|
||||||
# )
|
# wallet_id=lnurlpayout.wallet, complete=True, pending=False, outgoing=True, incoming=True
|
||||||
# await on_invoice_paid(payments[0])
|
# )
|
||||||
|
# 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))
|
key = base64.b32decode(key.upper() + "=" * ((8 - len(key)) % 8))
|
||||||
counter = struct.pack(">Q", counter)
|
counter = struct.pack(">Q", counter)
|
||||||
mac = hmac.new(key, counter, digest).digest()
|
mac = hmac.new(key, counter, digest).digest()
|
||||||
offset = mac[-1] & 0x0F
|
offset = mac[-1] & 0x0f
|
||||||
binary = struct.unpack(">L", mac[offset : offset + 4])[0] & 0x7FFFFFFF
|
binary = struct.unpack(">L", mac[offset : offset + 4])[0] & 0x7fffffff
|
||||||
return str(binary)[-digits:].zfill(digits)
|
return str(binary)[-digits:].zfill(digits)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import asyncio
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from lnbits.db import Database
|
from lnbits.db import Database
|
||||||
from lnbits.helpers import template_renderer
|
from lnbits.helpers import template_renderer
|
||||||
|
from lnbits.tasks import catch_everything_and_restart
|
||||||
|
|
||||||
db = Database("ext_satspay")
|
db = Database("ext_satspay")
|
||||||
|
|
||||||
@@ -13,5 +16,11 @@ def satspay_renderer():
|
|||||||
return template_renderer(["lnbits/extensions/satspay/templates"])
|
return template_renderer(["lnbits/extensions/satspay/templates"])
|
||||||
|
|
||||||
|
|
||||||
|
from .tasks import wait_for_paid_invoices
|
||||||
from .views import * # noqa
|
from .views import * # noqa
|
||||||
from .views_api 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
|
onchainaddress = None
|
||||||
if data.lnbitswallet:
|
if data.lnbitswallet:
|
||||||
payment_hash, payment_request = await create_invoice(
|
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:
|
else:
|
||||||
payment_hash = None
|
payment_hash = None
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import time
|
||||||
from sqlite3 import Row
|
from sqlite3 import Row
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi.param_functions import Query
|
from fastapi.param_functions import Query
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
import time
|
|
||||||
|
|
||||||
|
|
||||||
class CreateCharge(BaseModel):
|
class CreateCharge(BaseModel):
|
||||||
@@ -23,8 +24,8 @@ class Charges(BaseModel):
|
|||||||
onchainwallet: Optional[str]
|
onchainwallet: Optional[str]
|
||||||
onchainaddress: Optional[str]
|
onchainaddress: Optional[str]
|
||||||
lnbitswallet: Optional[str]
|
lnbitswallet: Optional[str]
|
||||||
payment_request: str
|
payment_request: Optional[str]
|
||||||
payment_hash: str
|
payment_hash: Optional[str]
|
||||||
webhook: Optional[str]
|
webhook: Optional[str]
|
||||||
completelink: Optional[str]
|
completelink: Optional[str]
|
||||||
completelinktext: Optional[str] = "Back to Merchant"
|
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)
|
||||||
@@ -132,7 +132,7 @@
|
|||||||
lightning-network invoice</span
|
lightning-network invoice</span
|
||||||
>
|
>
|
||||||
</center>
|
</center>
|
||||||
<a href="lightning://{{ charge.payment_request }}">
|
<a href="lightning:{{ charge.payment_request }}">
|
||||||
<q-responsive :ratio="1" class="q-mx-md">
|
<q-responsive :ratio="1" class="q-mx-md">
|
||||||
<qrcode
|
<qrcode
|
||||||
:value="'{{ charge.payment_request }}'"
|
:value="'{{ charge.payment_request }}'"
|
||||||
@@ -182,7 +182,7 @@
|
|||||||
to this onchain address</span
|
to this onchain address</span
|
||||||
>
|
>
|
||||||
</center>
|
</center>
|
||||||
<a href="bitcoin://{{ charge.onchainaddress }}">
|
<a href="bitcoin:{{ charge.onchainaddress }}">
|
||||||
<q-responsive :ratio="1" class="q-mx-md">
|
<q-responsive :ratio="1" class="q-mx-md">
|
||||||
<qrcode
|
<qrcode
|
||||||
:value="'{{ charge.onchainaddress }}'"
|
:value="'{{ charge.onchainaddress }}'"
|
||||||
@@ -235,10 +235,24 @@
|
|||||||
charge_time_elapsed: '{{charge.time_elapsed}}',
|
charge_time_elapsed: '{{charge.time_elapsed}}',
|
||||||
charge_amount: '{{charge.amount}}',
|
charge_amount: '{{charge.amount}}',
|
||||||
charge_balance: '{{charge.balance}}',
|
charge_balance: '{{charge.balance}}',
|
||||||
charge_paid: '{{charge.paid}}'
|
charge_paid: '{{charge.paid}}',
|
||||||
|
wallet: {
|
||||||
|
inkey: ''
|
||||||
|
},
|
||||||
|
cancelListener: () => {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
startPaymentNotifier(){
|
||||||
|
this.cancelListener()
|
||||||
|
|
||||||
|
this.cancelListener = LNbits.event.onInvoicePaid(
|
||||||
|
this.wallet,
|
||||||
|
payment => {
|
||||||
|
this.checkBalance()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
checkBalance: function () {
|
checkBalance: function () {
|
||||||
var self = this
|
var self = this
|
||||||
LNbits.api
|
LNbits.api
|
||||||
@@ -307,12 +321,14 @@
|
|||||||
this.lnbtc = false
|
this.lnbtc = false
|
||||||
this.onbtc = true
|
this.onbtc = true
|
||||||
}
|
}
|
||||||
|
this.wallet.inkey = '{{ wallet_inkey }}'
|
||||||
this.getTheTime()
|
this.getTheTime()
|
||||||
this.getThePercentage()
|
this.getThePercentage()
|
||||||
var timerCount = this.timerCount
|
var timerCount = this.timerCount
|
||||||
if ('{{ charge.paid }}' == 'False') {
|
if ('{{ charge.paid }}' == 'False') {
|
||||||
timerCount()
|
timerCount()
|
||||||
}
|
}
|
||||||
|
this.startPaymentNotifier()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -475,7 +475,7 @@
|
|||||||
},
|
},
|
||||||
sendFormDataCharge: function () {
|
sendFormDataCharge: function () {
|
||||||
var self = this
|
var self = this
|
||||||
var wallet = this.g.user.wallets[0].adminkey
|
var wallet = this.g.user.wallets[0].inkey
|
||||||
var data = this.formDialogCharge.data
|
var data = this.formDialogCharge.data
|
||||||
data.amount = parseInt(data.amount)
|
data.amount = parseInt(data.amount)
|
||||||
data.time = parseInt(data.time)
|
data.time = parseInt(data.time)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from starlette.exceptions import HTTPException
|
|||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
from starlette.responses import HTMLResponse
|
from starlette.responses import HTMLResponse
|
||||||
|
|
||||||
|
from lnbits.core.crud import get_wallet
|
||||||
from lnbits.core.models import User
|
from lnbits.core.models import User
|
||||||
from lnbits.decorators import check_user_exists
|
from lnbits.decorators import check_user_exists
|
||||||
|
|
||||||
@@ -29,6 +30,8 @@ async def display(request: Request, charge_id):
|
|||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Charge link does not exist."
|
status_code=HTTPStatus.NOT_FOUND, detail="Charge link does not exist."
|
||||||
)
|
)
|
||||||
|
wallet = await get_wallet(charge.lnbitswallet)
|
||||||
return satspay_renderer().TemplateResponse(
|
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 fastapi.params import Depends
|
||||||
from starlette.exceptions import HTTPException
|
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 lnbits.extensions.satspay import satspay_ext
|
||||||
|
|
||||||
from .crud import (
|
from .crud import (
|
||||||
@@ -22,18 +27,21 @@ from .models import CreateCharge
|
|||||||
|
|
||||||
|
|
||||||
@satspay_ext.post("/api/v1/charge")
|
@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}")
|
@satspay_ext.put("/api/v1/charge/{charge_id}")
|
||||||
async def api_charge_create_or_update(
|
async def api_charge_update(
|
||||||
data: CreateCharge,
|
data: CreateCharge,
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
charge_id=None,
|
charge_id=None,
|
||||||
):
|
):
|
||||||
if not charge_id:
|
charge = await update_charge(charge_id=charge_id, data=data)
|
||||||
charge = await create_charge(user=wallet.wallet.user, data=data)
|
return charge.dict()
|
||||||
return charge.dict()
|
|
||||||
else:
|
|
||||||
charge = await update_charge(charge_id=charge_id, data=data)
|
|
||||||
return charge.dict()
|
|
||||||
|
|
||||||
|
|
||||||
@satspay_ext.get("/api/v1/charges")
|
@satspay_ext.get("/api/v1/charges")
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ async def api_get_donations(g: WalletTypeInfo = Depends(get_key_type)):
|
|||||||
for wallet_id in wallet_ids:
|
for wallet_id in wallet_ids:
|
||||||
new_donations = await get_donations(wallet_id)
|
new_donations = await get_donations(wallet_id)
|
||||||
donations += new_donations if new_donations else []
|
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}")
|
@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:
|
async def set_subdomain_paid(payment_hash: str) -> Subdomains:
|
||||||
row = await db.fetchone(
|
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,),
|
(payment_hash,),
|
||||||
)
|
)
|
||||||
if row[8] == False:
|
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]:
|
async def get_subdomain(subdomain_id: str) -> Optional[Subdomains]:
|
||||||
row = await db.fetchone(
|
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,),
|
(subdomain_id,),
|
||||||
)
|
)
|
||||||
return Subdomains(**row) if row else None
|
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]:
|
async def get_subdomainBySubdomain(subdomain: str) -> Optional[Subdomains]:
|
||||||
row = await db.fetchone(
|
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,),
|
(subdomain,),
|
||||||
)
|
)
|
||||||
return Subdomains(**row) if row else None
|
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))
|
q = ",".join(["?"] * len(wallet_ids))
|
||||||
rows = await db.fetchall(
|
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,),
|
(*wallet_ids,),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ async def api_get_tipjars(wallet: WalletTypeInfo = Depends(get_key_type)):
|
|||||||
for wallet_id in wallet_ids:
|
for wallet_id in wallet_ids:
|
||||||
new_tipjars = await get_tipjars(wallet_id)
|
new_tipjars = await get_tipjars(wallet_id)
|
||||||
tipjars += new_tipjars if new_tipjars else []
|
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")
|
@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:
|
for wallet_id in wallet_ids:
|
||||||
new_tips = await get_tips(wallet_id)
|
new_tips = await get_tips(wallet_id)
|
||||||
tips += new_tips if new_tips else []
|
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}")
|
@tipjar_ext.put("/api/v1/tips/{tip_id}")
|
||||||
|
|||||||
@@ -82,11 +82,9 @@ async def api_wallet_delete(wallet_id, w: WalletTypeInfo = Depends(require_admin
|
|||||||
|
|
||||||
@watchonly_ext.get("/api/v1/address/{wallet_id}")
|
@watchonly_ext.get("/api/v1/address/{wallet_id}")
|
||||||
async def api_fresh_address(wallet_id, w: WalletTypeInfo = Depends(get_key_type)):
|
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()]
|
||||||
|
|
||||||
return [address.dict() for address in addresses]
|
|
||||||
|
|
||||||
|
|
||||||
@watchonly_ext.get("/api/v1/addresses/{wallet_id}")
|
@watchonly_ext.get("/api/v1/addresses/{wallet_id}")
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
{% if link.is_spent %}
|
{% if link.is_spent %}
|
||||||
<q-badge color="red" class="q-mb-md">Withdraw is spent.</q-badge>
|
<q-badge color="red" class="q-mb-md">Withdraw is spent.</q-badge>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="lightning://{{ lnurl }}">
|
<a href="lightning:{{ lnurl }}">
|
||||||
<q-responsive :ratio="1" class="q-mx-md">
|
<q-responsive :ratio="1" class="q-mx-md">
|
||||||
<qrcode
|
<qrcode
|
||||||
:value="this.here + '/?lightning={{lnurl }}'"
|
:value="this.here + '/?lightning={{lnurl }}'"
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import lnbits.settings as settings
|
|||||||
class Extension(NamedTuple):
|
class Extension(NamedTuple):
|
||||||
code: str
|
code: str
|
||||||
is_valid: bool
|
is_valid: bool
|
||||||
|
is_admin_only: bool
|
||||||
name: Optional[str] = None
|
name: Optional[str] = None
|
||||||
short_description: Optional[str] = None
|
short_description: Optional[str] = None
|
||||||
icon: Optional[str] = None
|
icon: Optional[str] = None
|
||||||
@@ -25,6 +26,7 @@ class Extension(NamedTuple):
|
|||||||
class ExtensionManager:
|
class ExtensionManager:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._disabled: List[str] = settings.LNBITS_DISABLED_EXTENSIONS
|
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] = [
|
self._extension_folders: List[str] = [
|
||||||
x[1] for x in os.walk(os.path.join(settings.LNBITS_PATH, "extensions"))
|
x[1] for x in os.walk(os.path.join(settings.LNBITS_PATH, "extensions"))
|
||||||
][0]
|
][0]
|
||||||
@@ -47,14 +49,17 @@ class ExtensionManager:
|
|||||||
) as json_file:
|
) as json_file:
|
||||||
config = json.load(json_file)
|
config = json.load(json_file)
|
||||||
is_valid = True
|
is_valid = True
|
||||||
|
is_admin_only = True if extension in self._admin_only else False
|
||||||
except Exception:
|
except Exception:
|
||||||
config = {}
|
config = {}
|
||||||
is_valid = False
|
is_valid = False
|
||||||
|
is_admin_only = False
|
||||||
|
|
||||||
output.append(
|
output.append(
|
||||||
Extension(
|
Extension(
|
||||||
extension,
|
extension,
|
||||||
is_valid,
|
is_valid,
|
||||||
|
is_admin_only,
|
||||||
config.get("name"),
|
config.get("name"),
|
||||||
config.get("short_description"),
|
config.get("short_description"),
|
||||||
config.get("icon"),
|
config.get("icon"),
|
||||||
@@ -156,6 +161,7 @@ def template_renderer(additional_folders: List = []) -> Jinja2Templates:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
t.env.globals["SITE_TITLE"] = settings.LNBITS_SITE_TITLE
|
t.env.globals["SITE_TITLE"] = settings.LNBITS_SITE_TITLE
|
||||||
|
t.env.globals["LNBITS_DENOMINATION"] = settings.LNBITS_DENOMINATION
|
||||||
t.env.globals["SITE_TAGLINE"] = settings.LNBITS_SITE_TAGLINE
|
t.env.globals["SITE_TAGLINE"] = settings.LNBITS_SITE_TAGLINE
|
||||||
t.env.globals["SITE_DESCRIPTION"] = settings.LNBITS_SITE_DESCRIPTION
|
t.env.globals["SITE_DESCRIPTION"] = settings.LNBITS_SITE_DESCRIPTION
|
||||||
t.env.globals["LNBITS_THEME_OPTIONS"] = settings.LNBITS_THEME_OPTIONS
|
t.env.globals["LNBITS_THEME_OPTIONS"] = settings.LNBITS_THEME_OPTIONS
|
||||||
|
|||||||
@@ -28,11 +28,14 @@ LNBITS_DATABASE_URL = env.str("LNBITS_DATABASE_URL", default=None)
|
|||||||
LNBITS_ALLOWED_USERS: List[str] = env.list(
|
LNBITS_ALLOWED_USERS: List[str] = env.list(
|
||||||
"LNBITS_ALLOWED_USERS", default=[], subcast=str
|
"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: List[str] = env.list(
|
||||||
"LNBITS_DISABLED_EXTENSIONS", default=[], subcast=str
|
"LNBITS_DISABLED_EXTENSIONS", default=[], subcast=str
|
||||||
)
|
)
|
||||||
|
|
||||||
LNBITS_SITE_TITLE = env.str("LNBITS_SITE_TITLE", default="LNbits")
|
LNBITS_SITE_TITLE = env.str("LNBITS_SITE_TITLE", default="LNbits")
|
||||||
|
LNBITS_DENOMINATION = env.str("LNBITS_DENOMINATION", default="sats")
|
||||||
LNBITS_SITE_TAGLINE = env.str(
|
LNBITS_SITE_TAGLINE = env.str(
|
||||||
"LNBITS_SITE_TAGLINE", default="free and open-source lightning wallet"
|
"LNBITS_SITE_TAGLINE", default="free and open-source lightning wallet"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ window.LNbits = {
|
|||||||
'/wallet?' + (userId ? 'usr=' + userId + '&' : '') + 'nme=' + walletName
|
'/wallet?' + (userId ? 'usr=' + userId + '&' : '') + 'nme=' + walletName
|
||||||
},
|
},
|
||||||
updateWallet: function (walletName, userId, walletId) {
|
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) {
|
deleteWallet: function (walletId, userId) {
|
||||||
window.location.href = '/deletewallet?usr=' + userId + '&wal=' + walletId
|
window.location.href = '/deletewallet?usr=' + userId + '&wal=' + walletId
|
||||||
@@ -123,6 +123,7 @@ window.LNbits = {
|
|||||||
[
|
[
|
||||||
'code',
|
'code',
|
||||||
'isValid',
|
'isValid',
|
||||||
|
'isAdminOnly',
|
||||||
'name',
|
'name',
|
||||||
'shortDescription',
|
'shortDescription',
|
||||||
'icon',
|
'icon',
|
||||||
@@ -135,7 +136,12 @@ window.LNbits = {
|
|||||||
return obj
|
return obj
|
||||||
},
|
},
|
||||||
user: function (data) {
|
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
|
var mapWallet = this.wallet
|
||||||
obj.wallets = obj.wallets
|
obj.wallets = obj.wallets
|
||||||
.map(function (obj) {
|
.map(function (obj) {
|
||||||
@@ -153,16 +159,23 @@ window.LNbits = {
|
|||||||
return obj
|
return obj
|
||||||
},
|
},
|
||||||
wallet: function (data) {
|
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.msat = data.balance_msat
|
||||||
newWallet.sat = Math.round(data.balance_msat / 1000)
|
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('')
|
newWallet.url = ['/wallet?usr=', data.user, '&wal=', data.id].join('')
|
||||||
return newWallet
|
return newWallet
|
||||||
},
|
},
|
||||||
payment: function (data) {
|
payment: function (data) {
|
||||||
obj = {
|
obj = {
|
||||||
checking_id:data.id,
|
checking_id: data.id,
|
||||||
pending: data.pending,
|
pending: data.pending,
|
||||||
amount: data.amount,
|
amount: data.amount,
|
||||||
fee: data.fee,
|
fee: data.fee,
|
||||||
@@ -174,8 +187,8 @@ window.LNbits = {
|
|||||||
extra: data.extra,
|
extra: data.extra,
|
||||||
wallet_id: data.wallet_id,
|
wallet_id: data.wallet_id,
|
||||||
webhook: data.webhook,
|
webhook: data.webhook,
|
||||||
webhook_status: data.webhook_status,
|
webhook_status: data.webhook_status
|
||||||
}
|
}
|
||||||
|
|
||||||
obj.date = Quasar.utils.date.formatDate(
|
obj.date = Quasar.utils.date.formatDate(
|
||||||
new Date(obj.time * 1000),
|
new Date(obj.time * 1000),
|
||||||
@@ -225,7 +238,8 @@ window.LNbits = {
|
|||||||
Quasar.plugins.Notify.create({
|
Quasar.plugins.Notify.create({
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
type: types[error.response.status] || 'warning',
|
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:
|
caption:
|
||||||
[error.response.status, ' ', error.response.statusText]
|
[error.response.status, ' ', error.response.statusText]
|
||||||
.join('')
|
.join('')
|
||||||
@@ -368,6 +382,10 @@ window.windowMixin = {
|
|||||||
.filter(function (obj) {
|
.filter(function (obj) {
|
||||||
return !obj.hidden
|
return !obj.hidden
|
||||||
})
|
})
|
||||||
|
.filter(function (obj) {
|
||||||
|
if (window.user.admin) return obj
|
||||||
|
return !obj.isAdminOnly
|
||||||
|
})
|
||||||
.map(function (obj) {
|
.map(function (obj) {
|
||||||
if (user) {
|
if (user) {
|
||||||
obj.isEnabled = user.extensions.indexOf(obj.code) !== -1
|
obj.isEnabled = user.extensions.indexOf(obj.code) !== -1
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ Vue.component('lnbits-wallet-list', {
|
|||||||
activeWallet: null,
|
activeWallet: null,
|
||||||
activeBalance: [],
|
activeBalance: [],
|
||||||
showForm: false,
|
showForm: false,
|
||||||
walletName: ''
|
walletName: '',
|
||||||
|
LNBITS_DENOMINATION: LNBITS_DENOMINATION
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
@@ -43,7 +44,8 @@ Vue.component('lnbits-wallet-list', {
|
|||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label lines="1">{{ wallet.name }}</q-item-label>
|
<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>
|
||||||
<q-item-section side v-show="activeWallet && activeWallet.id === wallet.id">
|
<q-item-section side v-show="activeWallet && activeWallet.id === wallet.id">
|
||||||
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
|
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
|
||||||
@@ -194,11 +196,11 @@ Vue.component('lnbits-payment-details', {
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-3"><b>Amount</b>:</div>
|
<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>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-3"><b>Fee</b>:</div>
|
<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>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-3"><b>Payment hash</b>:</div>
|
<div class="col-3"><b>Payment hash</b>:</div>
|
||||||
|
|||||||
@@ -192,6 +192,8 @@
|
|||||||
<script src="/static/js/components.js"></script>
|
<script src="/static/js/components.js"></script>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
const themes = {{ LNBITS_THEME_OPTIONS | tojson }}
|
const themes = {{ LNBITS_THEME_OPTIONS | tojson }}
|
||||||
|
const LNBITS_DENOMINATION = {{ LNBITS_DENOMINATION | tojson}}
|
||||||
|
console.log(LNBITS_DENOMINATION)
|
||||||
if(themes && themes.length) {
|
if(themes && themes.length) {
|
||||||
window.allowedThemes = themes.map(str => str.trim())
|
window.allowedThemes = themes.map(str => str.trim())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -279,8 +279,12 @@ async def btc_price(currency: str) -> float:
|
|||||||
|
|
||||||
|
|
||||||
async def get_fiat_rate_satoshis(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:
|
async def fiat_amount_as_satoshis(amount: float, currency: str) -> int:
|
||||||
return int(amount * (await get_fiat_rate_satoshis(currency)))
|
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)))
|
||||||
|
|||||||
@@ -9,3 +9,4 @@ from .lnpay import LNPayWallet
|
|||||||
from .lnbits import LNbitsWallet
|
from .lnbits import LNbitsWallet
|
||||||
from .lndrest import LndRestWallet
|
from .lndrest import LndRestWallet
|
||||||
from .spark import SparkWallet
|
from .spark import SparkWallet
|
||||||
|
from .fake import FakeWallet
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import httpx
|
||||||
|
from os import getenv
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Optional, Dict, AsyncGenerator
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
|
import hashlib
|
||||||
|
from ..bolt11 import encode, decode
|
||||||
|
from .base import (
|
||||||
|
StatusResponse,
|
||||||
|
InvoiceResponse,
|
||||||
|
PaymentResponse,
|
||||||
|
PaymentStatus,
|
||||||
|
Wallet,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FakeWallet(Wallet):
|
||||||
|
async def status(self) -> StatusResponse:
|
||||||
|
print(
|
||||||
|
"FakeWallet funding source is for using LNbits as a centralised, stand-alone payment system with brrrrrr."
|
||||||
|
)
|
||||||
|
return StatusResponse(None, float("inf"))
|
||||||
|
|
||||||
|
async def create_invoice(
|
||||||
|
self,
|
||||||
|
amount: int,
|
||||||
|
memo: Optional[str] = None,
|
||||||
|
description_hash: Optional[bytes] = None,
|
||||||
|
) -> InvoiceResponse:
|
||||||
|
secret = getenv("FAKE_WALLET_SECRET")
|
||||||
|
data: Dict = {
|
||||||
|
"out": False,
|
||||||
|
"amount": amount,
|
||||||
|
"currency": "bc",
|
||||||
|
"privkey": hashlib.pbkdf2_hmac('sha256', secret.encode("utf-8"), ("FakeWallet").encode("utf-8"), 2048, 32).hex(),
|
||||||
|
"memo": None,
|
||||||
|
"description_hash": None,
|
||||||
|
"description": "",
|
||||||
|
"fallback": None,
|
||||||
|
"expires": None,
|
||||||
|
"route": None,
|
||||||
|
}
|
||||||
|
data["amount"] = amount * 1000
|
||||||
|
data["timestamp"] = datetime.now().timestamp()
|
||||||
|
if description_hash:
|
||||||
|
data["tags_set"] = ["h"]
|
||||||
|
data["description_hash"] = description_hash.hex()
|
||||||
|
else:
|
||||||
|
data["tags_set"] = ["d"]
|
||||||
|
data["memo"] = memo
|
||||||
|
data["description"] = memo
|
||||||
|
randomHash = data["privkey"][:6] + hashlib.sha256(
|
||||||
|
str(random.getrandbits(256)).encode("utf-8")
|
||||||
|
).hexdigest()[6:]
|
||||||
|
data["paymenthash"] = randomHash
|
||||||
|
payment_request = encode(data)
|
||||||
|
checking_id = randomHash
|
||||||
|
|
||||||
|
return InvoiceResponse(True, checking_id, payment_request)
|
||||||
|
|
||||||
|
async def pay_invoice(self, bolt11: str) -> PaymentResponse:
|
||||||
|
invoice = decode(bolt11)
|
||||||
|
if hasattr(invoice, 'checking_id') and invoice.checking_id[6:] == data["privkey"][:6]:
|
||||||
|
return PaymentResponse(True, invoice.payment_hash, 0)
|
||||||
|
else:
|
||||||
|
return PaymentResponse(ok = False, error_message="Only internal invoices can be used!")
|
||||||
|
|
||||||
|
|
||||||
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
|
return PaymentStatus(False)
|
||||||
|
|
||||||
|
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||||
|
return PaymentStatus(False)
|
||||||
|
|
||||||
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
|
self.queue = asyncio.Queue(0)
|
||||||
|
while True:
|
||||||
|
value = await self.queue.get()
|
||||||
|
yield value
|
||||||
@@ -23,8 +23,7 @@ class LndRestWallet(Wallet):
|
|||||||
endpoint = getenv("LND_REST_ENDPOINT")
|
endpoint = getenv("LND_REST_ENDPOINT")
|
||||||
endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
|
endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
|
||||||
endpoint = (
|
endpoint = (
|
||||||
"https://" +
|
"https://" + endpoint if not endpoint.startswith("http") else endpoint
|
||||||
endpoint if not endpoint.startswith("http") else endpoint
|
|
||||||
)
|
)
|
||||||
self.endpoint = endpoint
|
self.endpoint = endpoint
|
||||||
|
|
||||||
@@ -103,10 +102,7 @@ class LndRestWallet(Wallet):
|
|||||||
r = await client.post(
|
r = await client.post(
|
||||||
url=f"{self.endpoint}/v1/channels/transactions",
|
url=f"{self.endpoint}/v1/channels/transactions",
|
||||||
headers=self.auth,
|
headers=self.auth,
|
||||||
json={
|
json={"payment_request": bolt11, "fee_limit": lnrpcFeeLimit},
|
||||||
"payment_request": bolt11,
|
|
||||||
"fee_limit": lnrpcFeeLimit,
|
|
||||||
},
|
|
||||||
timeout=180,
|
timeout=180,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -183,8 +179,7 @@ class LndRestWallet(Wallet):
|
|||||||
except:
|
except:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
payment_hash = base64.b64decode(
|
payment_hash = base64.b64decode(inv["r_hash"]).hex()
|
||||||
inv["r_hash"]).hex()
|
|
||||||
yield payment_hash
|
yield payment_hash
|
||||||
except (OSError, httpx.ConnectError, httpx.ReadError):
|
except (OSError, httpx.ConnectError, httpx.ReadError):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
from typing import Optional, AsyncGenerator
|
from typing import AsyncGenerator, Optional
|
||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
StatusResponse,
|
|
||||||
InvoiceResponse,
|
InvoiceResponse,
|
||||||
PaymentResponse,
|
PaymentResponse,
|
||||||
PaymentStatus,
|
PaymentStatus,
|
||||||
Wallet,
|
StatusResponse,
|
||||||
Unsupported,
|
Unsupported,
|
||||||
|
Wallet,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -20,10 +20,10 @@ class VoidWallet(Wallet):
|
|||||||
raise Unsupported("")
|
raise Unsupported("")
|
||||||
|
|
||||||
async def status(self) -> StatusResponse:
|
async def status(self) -> StatusResponse:
|
||||||
return StatusResponse(
|
print(
|
||||||
"This backend does nothing, it is here just as a placeholder, you must configure an actual backend before being able to do anything useful with LNbits.",
|
"This backend does nothing, it is here just as a placeholder, you must configure an actual backend before being able to do anything useful with LNbits."
|
||||||
0,
|
|
||||||
)
|
)
|
||||||
|
return StatusResponse(None, 0)
|
||||||
|
|
||||||
async def pay_invoice(self, bolt11: str) -> PaymentResponse:
|
async def pay_invoice(self, bolt11: str) -> PaymentResponse:
|
||||||
raise Unsupported("")
|
raise Unsupported("")
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "lnbits",
|
"name": "lnbits-legend",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ python-dotenv==0.19.0
|
|||||||
pyyaml==5.4.1
|
pyyaml==5.4.1
|
||||||
represent==1.6.0.post0
|
represent==1.6.0.post0
|
||||||
rfc3986==1.5.0
|
rfc3986==1.5.0
|
||||||
|
secp256k1==0.14.0
|
||||||
shortuuid==1.0.1
|
shortuuid==1.0.1
|
||||||
six==1.16.0
|
six==1.16.0
|
||||||
sniffio==1.2.0
|
sniffio==1.2.0
|
||||||
|
|||||||
Reference in New Issue
Block a user