Merge branch 'main' into ext-boltcards_keys
This commit is contained in:
commit
47f5865e6a
159 changed files with 5562 additions and 1170 deletions
32
.env.example
32
.env.example
|
|
@ -1,16 +1,23 @@
|
|||
HOST=127.0.0.1
|
||||
PORT=5000
|
||||
|
||||
# uvicorn variable, allow https behind a proxy
|
||||
# FORWARDED_ALLOW_IPS="*"
|
||||
|
||||
DEBUG=false
|
||||
|
||||
# Find "usr" string in wallet url to explicit allow users or set admins (comma separated list)
|
||||
LNBITS_ALLOWED_USERS=""
|
||||
LNBITS_ADMIN_USERS=""
|
||||
# Extensions only admin can access
|
||||
LNBITS_ADMIN_EXTENSIONS="ngrok"
|
||||
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
|
||||
|
||||
LNBITS_AD_SPACE="" # csv ad image filepaths or urls, extensions can choose to honor
|
||||
LNBITS_HIDE_API=false # Hides wallet api, extensions can choose to honor
|
||||
# csv ad image filepaths or urls, extensions can choose to honor
|
||||
LNBITS_AD_SPACE=""
|
||||
|
||||
# Hides wallet api, extensions can choose to honor
|
||||
LNBITS_HIDE_API=false
|
||||
|
||||
# Disable extensions for all users, use "all" to disable all extensions
|
||||
LNBITS_DISABLED_EXTENSIONS="amilk"
|
||||
|
|
@ -25,18 +32,20 @@ LNBITS_DATA_FOLDER="./data"
|
|||
|
||||
LNBITS_FORCE_HTTPS=true
|
||||
LNBITS_SERVICE_FEE="0.0"
|
||||
LNBITS_RESERVE_FEE_MIN=2000 # value in millisats
|
||||
LNBITS_RESERVE_FEE_PERCENT=1.0 # value in percent
|
||||
# value in millisats
|
||||
LNBITS_RESERVE_FEE_MIN=2000
|
||||
# value in percent
|
||||
LNBITS_RESERVE_FEE_PERCENT=1.0
|
||||
|
||||
# Change theme
|
||||
LNBITS_SITE_TITLE="LNbits"
|
||||
LNBITS_SITE_TAGLINE="free and open-source lightning wallet"
|
||||
LNBITS_SITE_DESCRIPTION="Some description about your service, will display if title is not 'LNbits'"
|
||||
# Choose from mint, flamingo, freedom, salvador, autumn, monochrome, classic
|
||||
LNBITS_THEME_OPTIONS="classic, bitcoin, freedom, mint, autumn, monochrome, salvador"
|
||||
# Choose from bitcoin, mint, flamingo, freedom, salvador, autumn, monochrome, classic
|
||||
LNBITS_THEME_OPTIONS="classic, bitcoin, flamingo, freedom, mint, autumn, monochrome, salvador"
|
||||
# LNBITS_CUSTOM_LOGO="https://lnbits.com/assets/images/logo/logo.svg"
|
||||
|
||||
# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, ClicheWallet
|
||||
# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, ClicheWallet, LnTipsWallet
|
||||
# LndRestWallet, CoreLightningWallet, LNbitsWallet, SparkWallet, FakeWallet, EclairWallet
|
||||
LNBITS_BACKEND_WALLET_CLASS=VoidWallet
|
||||
# VoidWallet is just a fallback that works without any actual Lightning capabilities,
|
||||
|
|
@ -62,7 +71,7 @@ LNBITS_KEY=LNBITS_ADMIN_KEY
|
|||
LND_REST_ENDPOINT=https://127.0.0.1:8080/
|
||||
LND_REST_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert"
|
||||
LND_REST_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/admin.macaroon or HEXSTRING"
|
||||
# To use an AES-encrypted macaroon, set
|
||||
# To use an AES-encrypted macaroon, set
|
||||
# LND_REST_MACAROON_ENCRYPTED="eNcRyPtEdMaCaRoOn"
|
||||
|
||||
# LNPayWallet
|
||||
|
|
@ -86,4 +95,9 @@ LNBITS_DENOMINATION=sats
|
|||
|
||||
# EclairWallet
|
||||
ECLAIR_URL=http://127.0.0.1:8283
|
||||
ECLAIR_PASS=eclairpw
|
||||
ECLAIR_PASS=eclairpw
|
||||
|
||||
# LnTipsWallet
|
||||
# Enter /api in LightningTipBot to get your key
|
||||
LNTIPS_API_KEY=LNTIPS_ADMIN_KEY
|
||||
LNTIPS_API_ENDPOINT=https://ln.tips
|
||||
|
|
|
|||
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG]"
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- LNbits version: [e.g. 0.9.2 or commit hash]
|
||||
- Database [e.g. sqlite, postgres]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: "[Feature request]"
|
||||
labels: feature request
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
10
.github/ISSUE_TEMPLATE/something-else.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/something-else.md
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
name: Something else
|
||||
about: Anything else that you need to say
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
13
.github/workflows/formatting.yml
vendored
13
.github/workflows/formatting.yml
vendored
|
|
@ -9,9 +9,20 @@ on:
|
|||
jobs:
|
||||
checks:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9"]
|
||||
poetry-version: ["1.2.1"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: abatilo/actions-poetry@v2.1.3
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Set up Poetry ${{ matrix.poetry-version }}
|
||||
uses: abatilo/actions-poetry@v2
|
||||
with:
|
||||
poetry-version: ${{ matrix.poetry-version }}
|
||||
- name: Install packages
|
||||
run: poetry install
|
||||
- name: Check black
|
||||
|
|
|
|||
8
.github/workflows/migrations.yml
vendored
8
.github/workflows/migrations.yml
vendored
|
|
@ -22,14 +22,18 @@ jobs:
|
|||
--health-retries 5
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.9]
|
||||
python-version: ["3.9"]
|
||||
poetry-version: ["1.2.1"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- uses: abatilo/actions-poetry@v2.1.3
|
||||
- name: Set up Poetry ${{ matrix.poetry-version }}
|
||||
uses: abatilo/actions-poetry@v2
|
||||
with:
|
||||
poetry-version: ${{ matrix.poetry-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
poetry install
|
||||
|
|
|
|||
8
.github/workflows/mypy.yml
vendored
8
.github/workflows/mypy.yml
vendored
|
|
@ -7,14 +7,18 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.9]
|
||||
python-version: ["3.9"]
|
||||
poetry-version: ["1.2.1"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- uses: abatilo/actions-poetry@v2.1.3
|
||||
- name: Set up Poetry ${{ matrix.poetry-version }}
|
||||
uses: abatilo/actions-poetry@v2
|
||||
with:
|
||||
poetry-version: ${{ matrix.poetry-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
poetry install
|
||||
|
|
|
|||
26
.github/workflows/regtest.yml
vendored
26
.github/workflows/regtest.yml
vendored
|
|
@ -7,14 +7,18 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.9]
|
||||
python-version: ["3.9"]
|
||||
poetry-version: ["1.2.1"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- uses: abatilo/actions-poetry@v2.1.3
|
||||
- name: Set up Poetry ${{ matrix.poetry-version }}
|
||||
uses: abatilo/actions-poetry@v2
|
||||
with:
|
||||
poetry-version: ${{ matrix.poetry-version }}
|
||||
- name: Setup Regtest
|
||||
run: |
|
||||
docker build -t lnbitsdocker/lnbits-legend .
|
||||
|
|
@ -46,14 +50,18 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.8]
|
||||
python-version: ["3.9"]
|
||||
poetry-version: ["1.2.1"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- uses: abatilo/actions-poetry@v2.1.3
|
||||
- name: Set up Poetry ${{ matrix.poetry-version }}
|
||||
uses: abatilo/actions-poetry@v2
|
||||
with:
|
||||
poetry-version: ${{ matrix.poetry-version }}
|
||||
- name: Setup Regtest
|
||||
run: |
|
||||
docker build -t lnbitsdocker/lnbits-legend .
|
||||
|
|
@ -65,7 +73,6 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: |
|
||||
poetry install
|
||||
poetry add grpcio protobuf
|
||||
- name: Run tests
|
||||
env:
|
||||
PYTHONUNBUFFERED: 1
|
||||
|
|
@ -87,14 +94,18 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.9]
|
||||
python-version: ["3.9"]
|
||||
poetry-version: ["1.2.1"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- uses: abatilo/actions-poetry@v2.1.3
|
||||
- name: Set up Poetry ${{ matrix.poetry-version }}
|
||||
uses: abatilo/actions-poetry@v2
|
||||
with:
|
||||
poetry-version: ${{ matrix.poetry-version }}
|
||||
- name: Setup Regtest
|
||||
run: |
|
||||
docker build -t lnbitsdocker/lnbits-legend .
|
||||
|
|
@ -106,7 +117,6 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: |
|
||||
poetry install
|
||||
poetry add pyln-client
|
||||
- name: Run tests
|
||||
env:
|
||||
PYTHONUNBUFFERED: 1
|
||||
|
|
|
|||
19
.github/workflows/tests.yml
vendored
19
.github/workflows/tests.yml
vendored
|
|
@ -7,7 +7,8 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.9]
|
||||
python-version: ["3.9"]
|
||||
poetry-version: ["1.2.1"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
|
|
@ -29,14 +30,18 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.9]
|
||||
python-version: ["3.9"]
|
||||
poetry-version: ["1.2.1"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- uses: abatilo/actions-poetry@v2.1.3
|
||||
- name: Set up Poetry ${{ matrix.poetry-version }}
|
||||
uses: abatilo/actions-poetry@v2
|
||||
with:
|
||||
poetry-version: ${{ matrix.poetry-version }}
|
||||
- name: Install dependencies
|
||||
env:
|
||||
VIRTUAL_ENV: ./venv
|
||||
|
|
@ -64,14 +69,18 @@ jobs:
|
|||
--health-retries 5
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.9]
|
||||
python-version: ["3.9"]
|
||||
poetry-version: ["1.2.1"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- uses: abatilo/actions-poetry@v2.1.3
|
||||
- name: Set up Poetry ${{ matrix.poetry-version }}
|
||||
uses: abatilo/actions-poetry@v2
|
||||
with:
|
||||
poetry-version: ${{ matrix.poetry-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
poetry install
|
||||
|
|
|
|||
13
Dockerfile
13
Dockerfile
|
|
@ -1,13 +1,24 @@
|
|||
FROM python:3.9-slim
|
||||
|
||||
RUN apt-get clean
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y curl pkg-config build-essential
|
||||
RUN curl -sSL https://install.python-poetry.org | python3 -
|
||||
|
||||
ENV PATH="/root/.local/bin:$PATH"
|
||||
|
||||
WORKDIR /app
|
||||
RUN mkdir -p lnbits/data
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN poetry config virtualenvs.create false
|
||||
RUN poetry install --no-dev --no-root
|
||||
RUN poetry run python build.py
|
||||
|
||||
ENV LNBITS_PORT="5000"
|
||||
ENV LNBITS_HOST="0.0.0.0"
|
||||
|
||||
EXPOSE 5000
|
||||
CMD ["poetry", "run", "lnbits", "--port", "5000", "--host", "0.0.0.0"]
|
||||
|
||||
CMD ["sh", "-c", "poetry run lnbits --port $LNBITS_PORT --host $LNBITS_HOST"]
|
||||
|
|
|
|||
8
Makefile
8
Makefile
|
|
@ -28,6 +28,10 @@ checkisort:
|
|||
poetry run isort --check-only .
|
||||
|
||||
test:
|
||||
BOLTZ_NETWORK="regtest" \
|
||||
BOLTZ_URL="http://127.0.0.1:9001" \
|
||||
BOLTZ_MEMPOOL_SPACE_URL="http://127.0.0.1:8080" \
|
||||
BOLTZ_MEMPOOL_SPACE_URL_WS="ws://127.0.0.1:8080" \
|
||||
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
|
||||
FAKE_WALLET_SECRET="ToTheMoon1" \
|
||||
LNBITS_DATA_FOLDER="./tests/data" \
|
||||
|
|
@ -46,6 +50,10 @@ test-real-wallet:
|
|||
poetry run pytest
|
||||
|
||||
test-venv:
|
||||
BOLTZ_NETWORK="regtest" \
|
||||
BOLTZ_URL="http://127.0.0.1:9001" \
|
||||
BOLTZ_MEMPOOL_SPACE_URL="http://127.0.0.1:8080" \
|
||||
BOLTZ_MEMPOOL_SPACE_URL_WS="ws://127.0.0.1:8080" \
|
||||
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
|
||||
FAKE_WALLET_SECRET="ToTheMoon1" \
|
||||
LNBITS_DATA_FOLDER="./tests/data" \
|
||||
|
|
|
|||
87
docs/devs/websockets.md
Normal file
87
docs/devs/websockets.md
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
---
|
||||
layout: default
|
||||
parent: For developers
|
||||
title: Websockets
|
||||
nav_order: 2
|
||||
---
|
||||
|
||||
|
||||
Websockets
|
||||
=================
|
||||
|
||||
`websockets` are a great way to add a two way instant data channel between server and client. This example was taken from the `copilot` extension, we create a websocket endpoint which can be restricted by `id`, then can feed it data to broadcast to any client on the socket using the `updater(extension_id, data)` function (`extension` has been used in place of an extension name, wreplace to your own extension):
|
||||
|
||||
|
||||
```sh
|
||||
from fastapi import Request, WebSocket, WebSocketDisconnect
|
||||
|
||||
class ConnectionManager:
|
||||
def __init__(self):
|
||||
self.active_connections: List[WebSocket] = []
|
||||
|
||||
async def connect(self, websocket: WebSocket, extension_id: str):
|
||||
await websocket.accept()
|
||||
websocket.id = extension_id
|
||||
self.active_connections.append(websocket)
|
||||
|
||||
def disconnect(self, websocket: WebSocket):
|
||||
self.active_connections.remove(websocket)
|
||||
|
||||
async def send_personal_message(self, message: str, extension_id: str):
|
||||
for connection in self.active_connections:
|
||||
if connection.id == extension_id:
|
||||
await connection.send_text(message)
|
||||
|
||||
async def broadcast(self, message: str):
|
||||
for connection in self.active_connections:
|
||||
await connection.send_text(message)
|
||||
|
||||
|
||||
manager = ConnectionManager()
|
||||
|
||||
|
||||
@extension_ext.websocket("/ws/{extension_id}", name="extension.websocket_by_id")
|
||||
async def websocket_endpoint(websocket: WebSocket, extension_id: str):
|
||||
await manager.connect(websocket, extension_id)
|
||||
try:
|
||||
while True:
|
||||
data = await websocket.receive_text()
|
||||
except WebSocketDisconnect:
|
||||
manager.disconnect(websocket)
|
||||
|
||||
|
||||
async def updater(extension_id, data):
|
||||
extension = await get_extension(extension_id)
|
||||
if not extension:
|
||||
return
|
||||
await manager.send_personal_message(f"{data}", extension_id)
|
||||
```
|
||||
|
||||
Example vue-js function for listening to the websocket:
|
||||
|
||||
```
|
||||
initWs: async function () {
|
||||
if (location.protocol !== 'http:') {
|
||||
localUrl =
|
||||
'wss://' +
|
||||
document.domain +
|
||||
':' +
|
||||
location.port +
|
||||
'/extension/ws/' +
|
||||
self.extension.id
|
||||
} else {
|
||||
localUrl =
|
||||
'ws://' +
|
||||
document.domain +
|
||||
':' +
|
||||
location.port +
|
||||
'/extension/ws/' +
|
||||
self.extension.id
|
||||
}
|
||||
this.ws = new WebSocket(localUrl)
|
||||
this.ws.addEventListener('message', async ({data}) => {
|
||||
const res = JSON.parse(data.toString())
|
||||
console.log(res)
|
||||
})
|
||||
},
|
||||
```
|
||||
|
|
@ -18,21 +18,25 @@ If you have problems installing LNbits using these instructions, please have a l
|
|||
git clone https://github.com/lnbits/lnbits-legend.git
|
||||
cd lnbits-legend/
|
||||
|
||||
# for making sure python 3.9 is installed, skip if installed
|
||||
# for making sure python 3.9 is installed, skip if installed. To check your installed version: python3 --version
|
||||
sudo apt update
|
||||
sudo apt install software-properties-common
|
||||
sudo add-apt-repository ppa:deadsnakes/ppa
|
||||
sudo apt install python3.9 python3.9-distutils
|
||||
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
export PATH="/home/ubuntu/.local/bin:$PATH" # or whatever is suggested in the poetry install notes printed to terminal
|
||||
# Once the above poetry install is completed, use the installation path printed to terminal and replace in the following command
|
||||
export PATH="/home/user/.local/bin:$PATH"
|
||||
# Next command, you can exchange with python3.10 or newer versions.
|
||||
# Identify your version with python3 --version and specify in the next line
|
||||
# command is only needed when your default python is not ^3.9 or ^3.10
|
||||
poetry env use python3.9
|
||||
poetry install --no-dev
|
||||
poetry run python build.py
|
||||
poetry install --only main
|
||||
|
||||
mkdir data
|
||||
cp .env.example .env
|
||||
nano .env # set funding source
|
||||
# set funding source amongst other options
|
||||
nano .env
|
||||
```
|
||||
|
||||
#### Running the server
|
||||
|
|
@ -40,9 +44,13 @@ nano .env # set funding source
|
|||
```sh
|
||||
poetry run lnbits
|
||||
# To change port/host pass 'poetry run lnbits --port 9000 --host 0.0.0.0'
|
||||
# adding --debug in the start-up command above to help your troubleshooting and generate a more verbose output
|
||||
# Note that you have to add the line DEBUG=true in your .env file, too.
|
||||
```
|
||||
|
||||
## Option 2: Nix
|
||||
## Option 2: Nix
|
||||
|
||||
> note: currently not supported while we make some architectural changes on the path to leave beta
|
||||
|
||||
```sh
|
||||
git clone https://github.com/lnbits/lnbits-legend.git
|
||||
|
|
@ -149,6 +157,7 @@ kill_timeout = 30
|
|||
HOST="127.0.0.1"
|
||||
PORT=5000
|
||||
LNBITS_FORCE_HTTPS=true
|
||||
FORWARDED_ALLOW_IPS="*"
|
||||
LNBITS_DATA_FOLDER="/data"
|
||||
|
||||
${PUT_YOUR_LNBITS_ENV_VARS_HERE}
|
||||
|
|
@ -211,8 +220,8 @@ You need to edit the `.env` file.
|
|||
|
||||
```sh
|
||||
# add the database connection string to .env 'nano .env' LNBITS_DATABASE_URL=
|
||||
# postgres://<user>:<myPassword>@<host>/<lnbits> - alter line bellow with your user, password and db name
|
||||
LNBITS_DATABASE_URL="postgres://postgres:postgres@localhost/lnbits"
|
||||
# postgres://<user>:<myPassword>@<host>:<port>/<lnbits> - alter line bellow with your user, password and db name
|
||||
LNBITS_DATABASE_URL="postgres://postgres:postgres@localhost:5432/lnbits"
|
||||
# save and exit
|
||||
```
|
||||
|
||||
|
|
@ -292,6 +301,43 @@ Save the file and run the following commands:
|
|||
sudo systemctl enable lnbits.service
|
||||
sudo systemctl start lnbits.service
|
||||
```
|
||||
## Reverse proxy with automatic https using Caddy
|
||||
|
||||
Use Caddy to make your LNbits install accessible over clearnet with a domain and https cert.
|
||||
|
||||
Point your domain at the IP of the server you're running LNbits on, by making an `A` record.
|
||||
|
||||
Install Caddy on the server
|
||||
https://caddyserver.com/docs/install#debian-ubuntu-raspbian
|
||||
|
||||
```
|
||||
sudo caddy stop
|
||||
```
|
||||
Create a Caddyfile
|
||||
```
|
||||
sudo nano Caddyfile
|
||||
```
|
||||
Assuming your LNbits is running on port `5000` add:
|
||||
```
|
||||
yourdomain.com {
|
||||
handle /api/v1/payments/sse* {
|
||||
reverse_proxy 0.0.0.0:5000 {
|
||||
header_up X-Forwarded-Host yourdomain.com
|
||||
transport http {
|
||||
keepalive off
|
||||
compression off
|
||||
}
|
||||
}
|
||||
}
|
||||
reverse_proxy 0.0.0.0:5000 {
|
||||
header_up X-Forwarded-Host yourdomain.com
|
||||
}
|
||||
}
|
||||
```
|
||||
Save and exit `CTRL + x`
|
||||
```
|
||||
sudo caddy start
|
||||
```
|
||||
|
||||
## Running behind an apache2 reverse proxy over https
|
||||
Install apache2 and enable apache2 mods
|
||||
|
|
|
|||
|
|
@ -15,8 +15,6 @@ A backend wallet can be configured using the following LNbits environment variab
|
|||
|
||||
### CoreLightning
|
||||
|
||||
Using this wallet requires the installation of the `pylightning` Python package.
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **CoreLightningWallet**
|
||||
- `CORELIGHTNING_RPC`: /file/path/lightning-rpc
|
||||
|
||||
|
|
@ -39,8 +37,6 @@ or
|
|||
|
||||
### LND (gRPC)
|
||||
|
||||
Using this wallet requires the installation of the `grpcio` and `protobuf` Python packages.
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **LndWallet**
|
||||
- `LND_GRPC_ENDPOINT`: ip_address
|
||||
- `LND_GRPC_PORT`: port
|
||||
|
|
@ -83,3 +79,8 @@ For the invoice to work you must have a publicly accessible URL in your LNbits.
|
|||
- `LNBITS_BACKEND_WALLET_CLASS`: **OpenNodeWallet**
|
||||
- `OPENNODE_API_ENDPOINT`: https://api.opennode.com/
|
||||
- `OPENNODE_KEY`: opennodeAdminApiKey
|
||||
|
||||
|
||||
### Cliche Wallet
|
||||
|
||||
- `CLICHE_ENDPOINT`: ws://127.0.0.1:12000
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ from .tasks import (
|
|||
check_pending_payments,
|
||||
internal_invoice_listener,
|
||||
invoice_listener,
|
||||
run_deferred_async,
|
||||
webhook_handler,
|
||||
)
|
||||
|
||||
|
|
@ -92,7 +91,6 @@ def create_app(config_object="lnbits.settings") -> FastAPI:
|
|||
)
|
||||
|
||||
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
||||
# app.add_middleware(ASGIProxyFix)
|
||||
|
||||
check_funding_source(app)
|
||||
register_assets(app)
|
||||
|
|
@ -127,7 +125,7 @@ def check_funding_source(app: FastAPI) -> None:
|
|||
logger.info("Retrying connection to backend in 5 seconds...")
|
||||
await asyncio.sleep(5)
|
||||
signal.signal(signal.SIGINT, original_sigint_handler)
|
||||
logger.info(
|
||||
logger.success(
|
||||
f"✔️ Backend {WALLET.__class__.__name__} connected and with a balance of {balance} msat."
|
||||
)
|
||||
|
||||
|
|
@ -185,7 +183,7 @@ def register_async_tasks(app):
|
|||
loop.create_task(catch_everything_and_restart(invoice_listener))
|
||||
loop.create_task(catch_everything_and_restart(internal_invoice_listener))
|
||||
await register_task_listeners()
|
||||
await run_deferred_async()
|
||||
# await run_deferred_async() # calle: doesn't do anyting?
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def stop_listeners():
|
||||
|
|
|
|||
|
|
@ -177,6 +177,11 @@ async def get_wallet_for_key(
|
|||
return Wallet(**row)
|
||||
|
||||
|
||||
async def get_total_balance(conn: Optional[Connection] = None):
|
||||
row = await (conn or db).fetchone("SELECT SUM(balance) FROM balances")
|
||||
return 0 if row[0] is None else row[0]
|
||||
|
||||
|
||||
# wallet payments
|
||||
# ---------------
|
||||
|
||||
|
|
@ -224,6 +229,24 @@ async def get_wallet_payment(
|
|||
return Payment.from_row(row) if row else None
|
||||
|
||||
|
||||
async def get_latest_payments_by_extension(ext_name: str, ext_id: str, limit: int = 5):
|
||||
rows = await db.fetchall(
|
||||
f"""
|
||||
SELECT * FROM apipayments
|
||||
WHERE pending = 'false'
|
||||
AND extra LIKE ?
|
||||
AND extra LIKE ?
|
||||
ORDER BY time DESC LIMIT {limit}
|
||||
""",
|
||||
(
|
||||
f"%{ext_name}%",
|
||||
f"%{ext_id}%",
|
||||
),
|
||||
)
|
||||
|
||||
return rows
|
||||
|
||||
|
||||
async def get_payments(
|
||||
*,
|
||||
wallet_id: Optional[str] = None,
|
||||
|
|
@ -328,7 +351,7 @@ async def delete_expired_invoices(
|
|||
"""
|
||||
)
|
||||
logger.debug(f"Checking expiry of {len(rows)} invoices")
|
||||
for (payment_request,) in rows:
|
||||
for i, (payment_request,) in enumerate(rows):
|
||||
try:
|
||||
invoice = bolt11.decode(payment_request)
|
||||
except:
|
||||
|
|
@ -338,7 +361,7 @@ async def delete_expired_invoices(
|
|||
if expiration_date > datetime.datetime.utcnow():
|
||||
continue
|
||||
logger.debug(
|
||||
f"Deleting expired invoice: {invoice.payment_hash} (expired: {expiration_date})"
|
||||
f"Deleting expired invoice {i}/{len(rows)}: {invoice.payment_hash} (expired: {expiration_date})"
|
||||
)
|
||||
await (conn or db).execute(
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ async def m001_initial(db):
|
|||
f"""
|
||||
CREATE TABLE IF NOT EXISTS apipayments (
|
||||
payhash TEXT NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
amount {db.big_int} NOT NULL,
|
||||
fee INTEGER NOT NULL DEFAULT 0,
|
||||
wallet TEXT NOT NULL,
|
||||
pending BOOLEAN NOT NULL,
|
||||
|
|
|
|||
|
|
@ -187,9 +187,9 @@ async def pay_invoice(
|
|||
)
|
||||
|
||||
# notify receiver asynchronously
|
||||
|
||||
from lnbits.tasks import internal_invoice_queue
|
||||
|
||||
logger.debug(f"enqueuing internal invoice {internal_checking_id}")
|
||||
await internal_invoice_queue.put(internal_checking_id)
|
||||
else:
|
||||
logger.debug(f"backend: sending payment {temp_id}")
|
||||
|
|
@ -224,8 +224,8 @@ async def pay_invoice(
|
|||
logger.debug(f"deleting temporary payment {temp_id}")
|
||||
await delete_wallet_payment(temp_id, wallet_id, conn=conn)
|
||||
raise PaymentFailure(
|
||||
f"payment failed: {payment.error_message}"
|
||||
or "payment failed, but backend didn't give us an error message"
|
||||
f"Payment failed: {payment.error_message}"
|
||||
or "Payment failed, but backend didn't give us an error message."
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
|
|
|
|||
|
|
@ -3,7 +3,11 @@ const CACHE_VERSION = 1
|
|||
const CURRENT_CACHE = `lnbits-${CACHE_VERSION}-`
|
||||
|
||||
const getApiKey = request => {
|
||||
return request.headers.get('X-Api-Key') || 'none'
|
||||
let api_key = request.headers.get('X-Api-Key')
|
||||
if (!api_key || api_key == 'undefined') {
|
||||
api_key = 'no_api_key'
|
||||
}
|
||||
return api_key
|
||||
}
|
||||
|
||||
// on activation we clean up the previously registered service workers
|
||||
|
|
@ -26,8 +30,10 @@ self.addEventListener('activate', evt =>
|
|||
// If no response is found, it populates the runtime cache with the response
|
||||
// from the network before returning it to the page.
|
||||
self.addEventListener('fetch', event => {
|
||||
// Skip cross-origin requests, like those for Google Analytics.
|
||||
if (
|
||||
!event.request.url.startsWith(
|
||||
self.location.origin + '/api/v1/payments/sse'
|
||||
) &&
|
||||
event.request.url.startsWith(self.location.origin) &&
|
||||
event.request.method == 'GET'
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -361,6 +361,35 @@ new Vue({
|
|||
this.receive.status = 'pending'
|
||||
})
|
||||
},
|
||||
onInitQR: async function (promise) {
|
||||
try {
|
||||
await promise
|
||||
} catch (error) {
|
||||
let mapping = {
|
||||
NotAllowedError: 'ERROR: you need to grant camera access permission',
|
||||
NotFoundError: 'ERROR: no camera on this device',
|
||||
NotSupportedError:
|
||||
'ERROR: secure context required (HTTPS, localhost)',
|
||||
NotReadableError: 'ERROR: is the camera already in use?',
|
||||
OverconstrainedError: 'ERROR: installed cameras are not suitable',
|
||||
StreamApiNotSupportedError:
|
||||
'ERROR: Stream API is not supported in this browser',
|
||||
InsecureContextError:
|
||||
'ERROR: Camera access is only permitted in secure context. Use HTTPS or localhost rather than HTTP.'
|
||||
}
|
||||
let valid_error = Object.keys(mapping).filter(key => {
|
||||
return error.name === key
|
||||
})
|
||||
let camera_error = valid_error
|
||||
? mapping[valid_error]
|
||||
: `ERROR: Camera error (${error.name})`
|
||||
this.parse.camera.show = false
|
||||
this.$q.notify({
|
||||
message: camera_error,
|
||||
type: 'negative'
|
||||
})
|
||||
}
|
||||
},
|
||||
decodeQR: function (res) {
|
||||
this.parse.data.request = res
|
||||
this.decodeRequest()
|
||||
|
|
@ -675,7 +704,7 @@ new Vue({
|
|||
// status is important for export but it is not in paymentsTable
|
||||
// because it is manually added with payment detail link and icons
|
||||
// and would cause duplication in the list
|
||||
let columns = this.paymentsTable.columns
|
||||
let columns = structuredClone(this.paymentsTable.columns)
|
||||
columns.unshift({
|
||||
name: 'pending',
|
||||
align: 'left',
|
||||
|
|
|
|||
|
|
@ -1,30 +1,43 @@
|
|||
import asyncio
|
||||
from typing import List
|
||||
from typing import Dict
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.tasks import register_invoice_listener
|
||||
from lnbits.helpers import get_current_extension_name
|
||||
from lnbits.tasks import SseListenersDict, register_invoice_listener
|
||||
|
||||
from . import db
|
||||
from .crud import get_balance_notify
|
||||
from .models import Payment
|
||||
|
||||
api_invoice_listeners: List[asyncio.Queue] = []
|
||||
api_invoice_listeners: Dict[str, asyncio.Queue] = SseListenersDict(
|
||||
"api_invoice_listeners"
|
||||
)
|
||||
|
||||
|
||||
async def register_task_listeners():
|
||||
"""
|
||||
Registers an invoice listener queue for the core tasks.
|
||||
Incoming payaments in this queue will eventually trigger the signals sent to all other extensions
|
||||
and fulfill other core tasks such as dispatching webhooks.
|
||||
"""
|
||||
invoice_paid_queue = asyncio.Queue(5)
|
||||
register_invoice_listener(invoice_paid_queue)
|
||||
# we register invoice_paid_queue to receive all incoming invoices
|
||||
register_invoice_listener(invoice_paid_queue, "core/tasks.py")
|
||||
# register a worker that will react to invoices
|
||||
asyncio.create_task(wait_for_paid_invoices(invoice_paid_queue))
|
||||
|
||||
|
||||
async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
|
||||
"""
|
||||
This worker dispatches events to all extensions, dispatches webhooks and balance notifys.
|
||||
"""
|
||||
while True:
|
||||
payment = await invoice_paid_queue.get()
|
||||
logger.debug("received invoice paid event")
|
||||
logger.trace("received invoice paid event")
|
||||
# send information to sse channel
|
||||
await dispatch_invoice_listener(payment)
|
||||
await dispatch_api_invoice_listeners(payment)
|
||||
|
||||
# dispatch webhook
|
||||
if payment.webhook and not payment.webhook_status:
|
||||
|
|
@ -41,16 +54,23 @@ async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
|
|||
pass
|
||||
|
||||
|
||||
async def dispatch_invoice_listener(payment: Payment):
|
||||
for send_channel in api_invoice_listeners:
|
||||
async def dispatch_api_invoice_listeners(payment: Payment):
|
||||
"""
|
||||
Emits events to invoice listener subscribed from the API.
|
||||
"""
|
||||
for chan_name, send_channel in api_invoice_listeners.items():
|
||||
try:
|
||||
logger.debug(f"sending invoice paid event to {chan_name}")
|
||||
send_channel.put_nowait(payment)
|
||||
except asyncio.QueueFull:
|
||||
logger.debug("removing sse listener", send_channel)
|
||||
api_invoice_listeners.remove(send_channel)
|
||||
logger.error(f"removing sse listener {send_channel}:{chan_name}")
|
||||
api_invoice_listeners.pop(chan_name)
|
||||
|
||||
|
||||
async def dispatch_webhook(payment: Payment):
|
||||
"""
|
||||
Dispatches the webhook to the webhook url.
|
||||
"""
|
||||
async with httpx.AsyncClient() as client:
|
||||
data = payment.dict()
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -171,6 +171,17 @@
|
|||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<a href="https://mynodebtc.com">
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '/static/images/mynode.png' : '/static/images/mynodel.png'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col q-pl-md"> </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -653,6 +653,7 @@
|
|||
<q-responsive :ratio="1">
|
||||
<qrcode-stream
|
||||
@decode="decodeQR"
|
||||
@init="onInitQR"
|
||||
class="rounded-borders"
|
||||
></qrcode-stream>
|
||||
</q-responsive>
|
||||
|
|
@ -671,6 +672,7 @@
|
|||
<div class="text-center q-mb-lg">
|
||||
<qrcode-stream
|
||||
@decode="decodeQR"
|
||||
@init="onInitQR"
|
||||
class="rounded-borders"
|
||||
></qrcode-stream>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,11 +2,14 @@ import asyncio
|
|||
import binascii
|
||||
import hashlib
|
||||
import json
|
||||
import time
|
||||
import uuid
|
||||
from http import HTTPStatus
|
||||
from io import BytesIO
|
||||
from typing import Dict, List, Optional, Tuple, Union
|
||||
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
|
||||
|
||||
import async_timeout
|
||||
import httpx
|
||||
import pyqrcode
|
||||
from fastapi import Depends, Header, Query, Request
|
||||
|
|
@ -15,7 +18,7 @@ from fastapi.params import Body
|
|||
from loguru import logger
|
||||
from pydantic import BaseModel
|
||||
from pydantic.fields import Field
|
||||
from sse_starlette.sse import EventSourceResponse
|
||||
from sse_starlette.sse import EventSourceResponse, ServerSentEvent
|
||||
from starlette.responses import HTMLResponse, StreamingResponse
|
||||
|
||||
from lnbits import bolt11, lnurl
|
||||
|
|
@ -27,7 +30,7 @@ from lnbits.decorators import (
|
|||
require_invoice_key,
|
||||
)
|
||||
from lnbits.helpers import url_for, urlsafe_short_hash
|
||||
from lnbits.settings import LNBITS_ADMIN_USERS, LNBITS_SITE_TITLE
|
||||
from lnbits.settings import LNBITS_ADMIN_USERS, LNBITS_SITE_TITLE, WALLET
|
||||
from lnbits.utils.exchange_rates import (
|
||||
currencies,
|
||||
fiat_amount_as_satoshis,
|
||||
|
|
@ -39,6 +42,7 @@ from ..crud import (
|
|||
create_payment,
|
||||
get_payments,
|
||||
get_standalone_payment,
|
||||
get_total_balance,
|
||||
get_wallet,
|
||||
get_wallet_for_key,
|
||||
save_balance_check,
|
||||
|
|
@ -364,37 +368,48 @@ async def api_payments_pay_lnurl(
|
|||
}
|
||||
|
||||
|
||||
async def subscribe(request: Request, wallet: Wallet):
|
||||
async def subscribe_wallet_invoices(request: Request, wallet: Wallet):
|
||||
"""
|
||||
Subscribe to new invoices for a wallet. Can be wrapped in EventSourceResponse.
|
||||
Listenes invoming payments for a wallet and yields jsons with payment details.
|
||||
"""
|
||||
this_wallet_id = wallet.id
|
||||
|
||||
payment_queue: asyncio.Queue[Payment] = asyncio.Queue(0)
|
||||
|
||||
logger.debug("adding sse listener", payment_queue)
|
||||
api_invoice_listeners.append(payment_queue)
|
||||
uid = f"{this_wallet_id}_{str(uuid.uuid4())[:8]}"
|
||||
logger.debug(f"adding sse listener for wallet: {uid}")
|
||||
api_invoice_listeners[uid] = payment_queue
|
||||
|
||||
send_queue: asyncio.Queue[Tuple[str, Payment]] = asyncio.Queue(0)
|
||||
|
||||
async def payment_received() -> None:
|
||||
while True:
|
||||
payment: Payment = await payment_queue.get()
|
||||
if payment.wallet_id == this_wallet_id:
|
||||
logger.debug("payment received", payment)
|
||||
await send_queue.put(("payment-received", payment))
|
||||
try:
|
||||
async with async_timeout.timeout(1):
|
||||
payment: Payment = await payment_queue.get()
|
||||
if payment.wallet_id == this_wallet_id:
|
||||
logger.debug("sse listener: payment receieved", payment)
|
||||
await send_queue.put(("payment-received", payment))
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
|
||||
asyncio.create_task(payment_received())
|
||||
task = asyncio.create_task(payment_received())
|
||||
|
||||
try:
|
||||
while True:
|
||||
if await request.is_disconnected():
|
||||
await request.close()
|
||||
break
|
||||
typ, data = await send_queue.get()
|
||||
|
||||
if data:
|
||||
jdata = json.dumps(dict(data.dict(), pending=False))
|
||||
|
||||
# yield dict(id=1, event="this", data="1234")
|
||||
# await asyncio.sleep(2)
|
||||
yield dict(data=jdata, event=typ)
|
||||
# yield dict(data=jdata.encode("utf-8"), event=typ.encode("utf-8"))
|
||||
except asyncio.CancelledError:
|
||||
except asyncio.CancelledError as e:
|
||||
logger.debug(f"CancelledError on listener {uid}: {e}")
|
||||
api_invoice_listeners.pop(uid)
|
||||
task.cancel()
|
||||
return
|
||||
|
||||
|
||||
|
|
@ -403,7 +418,9 @@ async def api_payments_sse(
|
|||
request: Request, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
):
|
||||
return EventSourceResponse(
|
||||
subscribe(request, wallet.wallet), ping=20, media_type="text/event-stream"
|
||||
subscribe_wallet_invoices(request, wallet.wallet),
|
||||
ping=20,
|
||||
media_type="text/event-stream",
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -459,7 +476,7 @@ async def api_lnurlscan(code: str, wallet: WalletTypeInfo = Depends(get_key_type
|
|||
except:
|
||||
# parse internet identifier (user@domain.com)
|
||||
name_domain = code.split("@")
|
||||
if len(name_domain) == 2 and len(name_domain[1].split(".")) == 2:
|
||||
if len(name_domain) == 2 and len(name_domain[1].split(".")) >= 2:
|
||||
name, domain = name_domain
|
||||
url = (
|
||||
("http://" if domain.endswith(".onion") else "https://")
|
||||
|
|
@ -657,3 +674,26 @@ async def img(request: Request, data):
|
|||
"Expires": "0",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@core_app.get("/api/v1/audit/")
|
||||
async def api_auditor(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
if wallet.wallet.user not in LNBITS_ADMIN_USERS:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN, detail="Not an admin user"
|
||||
)
|
||||
|
||||
total_balance = await get_total_balance()
|
||||
error_message, node_balance = await WALLET.status()
|
||||
|
||||
if not error_message:
|
||||
delta = node_balance - total_balance
|
||||
else:
|
||||
node_balance, delta = None, None
|
||||
|
||||
return {
|
||||
"node_balance_msats": node_balance,
|
||||
"lnbits_balance_msats": total_balance,
|
||||
"delta_msats": delta,
|
||||
"timestamp": int(time.time()),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,8 +46,8 @@ async def api_public_payment_longpolling(payment_hash):
|
|||
|
||||
payment_queue = asyncio.Queue(0)
|
||||
|
||||
logger.debug("adding standalone invoice listener", payment_hash, payment_queue)
|
||||
api_invoice_listeners.append(payment_queue)
|
||||
logger.debug(f"adding standalone invoice listener for hash: {payment_hash}")
|
||||
api_invoice_listeners[payment_hash] = payment_queue
|
||||
|
||||
response = None
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,12 @@ class Compat:
|
|||
return ""
|
||||
return "<nothing>"
|
||||
|
||||
@property
|
||||
def big_int(self) -> str:
|
||||
if self.type in {POSTGRES}:
|
||||
return "BIGINT"
|
||||
return "INT"
|
||||
|
||||
|
||||
class Connection(Compat):
|
||||
def __init__(self, conn: AsyncConnection, txn, typ, name, schema):
|
||||
|
|
|
|||
|
|
@ -95,4 +95,4 @@ async def api_bleskomat_delete(
|
|||
)
|
||||
|
||||
await delete_bleskomat(bleskomat_id)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ async def m001_initial(db):
|
|||
)
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
f"""
|
||||
CREATE TABLE boltcards.hits (
|
||||
id TEXT PRIMARY KEY UNIQUE,
|
||||
card_id TEXT NOT NULL,
|
||||
|
|
@ -38,7 +38,7 @@ async def m001_initial(db):
|
|||
useragent TEXT,
|
||||
old_ctr INT NOT NULL DEFAULT 0,
|
||||
new_ctr INT NOT NULL DEFAULT 0,
|
||||
amount INT NOT NULL,
|
||||
amount {db.big_int} NOT NULL,
|
||||
time TIMESTAMP NOT NULL DEFAULT """
|
||||
+ db.timestamp_now
|
||||
+ """
|
||||
|
|
@ -47,11 +47,11 @@ async def m001_initial(db):
|
|||
)
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
f"""
|
||||
CREATE TABLE boltcards.refunds (
|
||||
id TEXT PRIMARY KEY UNIQUE,
|
||||
hit_id TEXT NOT NULL,
|
||||
refund_amount INT NOT NULL,
|
||||
refund_amount {db.big_int} NOT NULL,
|
||||
time TIMESTAMP NOT NULL DEFAULT """
|
||||
+ db.timestamp_now
|
||||
+ """
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from loguru import logger
|
|||
|
||||
from lnbits.core import db as core_db
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.helpers import get_current_extension_name
|
||||
from lnbits.tasks import register_invoice_listener
|
||||
|
||||
from .crud import create_refund, get_card, get_hit
|
||||
|
|
@ -13,7 +14,7 @@ from .crud import create_refund, get_card, get_hit
|
|||
|
||||
async def wait_for_paid_invoices():
|
||||
invoice_queue = asyncio.Queue()
|
||||
register_invoice_listener(invoice_queue)
|
||||
register_invoice_listener(invoice_queue, get_current_extension_name())
|
||||
|
||||
while True:
|
||||
payment = await invoice_queue.get()
|
||||
|
|
|
|||
|
|
@ -434,7 +434,10 @@
|
|||
<strong>Notification webhook:</strong> {{ qrCodeDialog.data.webhook_url
|
||||
}}<br />
|
||||
</p>
|
||||
<br />
|
||||
<p>
|
||||
Always backup all keys that you're trying to write on the card. Without
|
||||
them you may not be able to change them in the future!
|
||||
</p>
|
||||
<q-btn
|
||||
unelevated
|
||||
outline
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ async def api_card_delete(card_id, wallet: WalletTypeInfo = Depends(require_admi
|
|||
raise HTTPException(detail="Not your card.", status_code=HTTPStatus.FORBIDDEN)
|
||||
|
||||
await delete_card(card_id)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
@boltcards_ext.get("/api/v1/hits")
|
||||
|
|
|
|||
|
|
@ -34,8 +34,8 @@ from .models import (
|
|||
from .utils import check_balance, get_timestamp, req_wrap
|
||||
|
||||
net = NETWORKS[BOLTZ_NETWORK]
|
||||
logger.debug(f"BOLTZ_URL: {BOLTZ_URL}")
|
||||
logger.debug(f"Bitcoin Network: {net['name']}")
|
||||
logger.trace(f"BOLTZ_URL: {BOLTZ_URL}")
|
||||
logger.trace(f"Bitcoin Network: {net['name']}")
|
||||
|
||||
|
||||
async def create_swap(data: CreateSubmarineSwap) -> SubmarineSwap:
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ from lnbits.settings import BOLTZ_MEMPOOL_SPACE_URL, BOLTZ_MEMPOOL_SPACE_URL_WS
|
|||
|
||||
from .utils import req_wrap
|
||||
|
||||
logger.debug(f"BOLTZ_MEMPOOL_SPACE_URL: {BOLTZ_MEMPOOL_SPACE_URL}")
|
||||
logger.debug(f"BOLTZ_MEMPOOL_SPACE_URL_WS: {BOLTZ_MEMPOOL_SPACE_URL_WS}")
|
||||
logger.trace(f"BOLTZ_MEMPOOL_SPACE_URL: {BOLTZ_MEMPOOL_SPACE_URL}")
|
||||
logger.trace(f"BOLTZ_MEMPOOL_SPACE_URL_WS: {BOLTZ_MEMPOOL_SPACE_URL_WS}")
|
||||
|
||||
websocket_url = f"{BOLTZ_MEMPOOL_SPACE_URL_WS}/api/v1/ws"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
async def m001_initial(db):
|
||||
await db.execute(
|
||||
"""
|
||||
f"""
|
||||
CREATE TABLE boltz.submarineswap (
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT NOT NULL,
|
||||
payment_hash TEXT NOT NULL,
|
||||
amount INT NOT NULL,
|
||||
amount {db.big_int} NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
boltz_id TEXT NOT NULL,
|
||||
refund_address TEXT NOT NULL,
|
||||
refund_privkey TEXT NOT NULL,
|
||||
expected_amount INT NOT NULL,
|
||||
expected_amount {db.big_int} NOT NULL,
|
||||
timeout_block_height INT NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
bip21 TEXT NOT NULL,
|
||||
|
|
@ -22,12 +22,12 @@ async def m001_initial(db):
|
|||
"""
|
||||
)
|
||||
await db.execute(
|
||||
"""
|
||||
f"""
|
||||
CREATE TABLE boltz.reverse_submarineswap (
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT NOT NULL,
|
||||
onchain_address TEXT NOT NULL,
|
||||
amount INT NOT NULL,
|
||||
amount {db.big_int} NOT NULL,
|
||||
instant_settlement BOOLEAN NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
boltz_id TEXT NOT NULL,
|
||||
|
|
@ -37,7 +37,7 @@ async def m001_initial(db):
|
|||
claim_privkey TEXT NOT NULL,
|
||||
lockup_address TEXT NOT NULL,
|
||||
invoice TEXT NOT NULL,
|
||||
onchain_amount INT NOT NULL,
|
||||
onchain_amount {db.big_int} NOT NULL,
|
||||
time TIMESTAMP NOT NULL DEFAULT """
|
||||
+ db.timestamp_now
|
||||
+ """
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from loguru import logger
|
|||
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.core.services import check_transaction_status
|
||||
from lnbits.helpers import get_current_extension_name
|
||||
from lnbits.tasks import register_invoice_listener
|
||||
|
||||
from .boltz import (
|
||||
|
|
@ -56,7 +57,7 @@ async def check_for_pending_swaps():
|
|||
swap_status = get_swap_status(swap)
|
||||
# should only happen while development when regtest is reset
|
||||
if swap_status.exists is False:
|
||||
logger.warning(f"Boltz - swap: {swap.boltz_id} does not exist.")
|
||||
logger.debug(f"Boltz - swap: {swap.boltz_id} does not exist.")
|
||||
await update_swap_status(swap.id, "failed")
|
||||
continue
|
||||
|
||||
|
|
@ -72,7 +73,7 @@ async def check_for_pending_swaps():
|
|||
else:
|
||||
if swap_status.hit_timeout:
|
||||
if not swap_status.has_lockup:
|
||||
logger.warning(
|
||||
logger.debug(
|
||||
f"Boltz - swap: {swap.id} hit timeout, but no lockup tx..."
|
||||
)
|
||||
await update_swap_status(swap.id, "timeout")
|
||||
|
|
@ -127,7 +128,7 @@ async def check_for_pending_swaps():
|
|||
|
||||
async def wait_for_paid_invoices():
|
||||
invoice_queue = asyncio.Queue()
|
||||
register_invoice_listener(invoice_queue)
|
||||
register_invoice_listener(invoice_queue, get_current_extension_name())
|
||||
|
||||
while True:
|
||||
payment = await invoice_queue.get()
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from .models import Copilots, CreateCopilotData
|
|||
|
||||
async def create_copilot(
|
||||
data: CreateCopilotData, inkey: Optional[str] = ""
|
||||
) -> Copilots:
|
||||
) -> Optional[Copilots]:
|
||||
copilot_id = urlsafe_short_hash()
|
||||
await db.execute(
|
||||
"""
|
||||
|
|
@ -67,19 +67,19 @@ async def create_copilot(
|
|||
|
||||
|
||||
async def update_copilot(
|
||||
data: CreateCopilotData, copilot_id: Optional[str] = ""
|
||||
data: CreateCopilotData, copilot_id: str
|
||||
) -> Optional[Copilots]:
|
||||
q = ", ".join([f"{field[0]} = ?" for field in data])
|
||||
items = [f"{field[1]}" for field in data]
|
||||
items.append(copilot_id)
|
||||
await db.execute(f"UPDATE copilot.newer_copilots SET {q} WHERE id = ?", (items))
|
||||
await db.execute(f"UPDATE copilot.newer_copilots SET {q} WHERE id = ?", (items,))
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM copilot.newer_copilots WHERE id = ?", (copilot_id,)
|
||||
)
|
||||
return Copilots(**row) if row else None
|
||||
|
||||
|
||||
async def get_copilot(copilot_id: str) -> Copilots:
|
||||
async def get_copilot(copilot_id: str) -> Optional[Copilots]:
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM copilot.newer_copilots WHERE id = ?", (copilot_id,)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from starlette.exceptions import HTTPException
|
|||
|
||||
from lnbits.core import db as core_db
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.helpers import get_current_extension_name
|
||||
from lnbits.tasks import register_invoice_listener
|
||||
|
||||
from .crud import get_copilot
|
||||
|
|
@ -15,7 +16,7 @@ from .views import updater
|
|||
|
||||
async def wait_for_paid_invoices():
|
||||
invoice_queue = asyncio.Queue()
|
||||
register_invoice_listener(invoice_queue)
|
||||
register_invoice_listener(invoice_queue, get_current_extension_name())
|
||||
|
||||
while True:
|
||||
payment = await invoice_queue.get()
|
||||
|
|
@ -25,7 +26,7 @@ async def wait_for_paid_invoices():
|
|||
async def on_invoice_paid(payment: Payment) -> None:
|
||||
webhook = None
|
||||
data = None
|
||||
if payment.extra.get("tag") != "copilot":
|
||||
if not payment.extra or payment.extra.get("tag") != "copilot":
|
||||
# not an copilot invoice
|
||||
return
|
||||
|
||||
|
|
@ -70,12 +71,12 @@ async def on_invoice_paid(payment: Payment) -> None:
|
|||
|
||||
|
||||
async def mark_webhook_sent(payment: Payment, status: int) -> None:
|
||||
payment.extra["wh_status"] = status
|
||||
|
||||
await core_db.execute(
|
||||
"""
|
||||
UPDATE apipayments SET extra = ?
|
||||
WHERE hash = ?
|
||||
""",
|
||||
(json.dumps(payment.extra), payment.payment_hash),
|
||||
)
|
||||
if payment.extra:
|
||||
payment.extra["wh_status"] = status
|
||||
await core_db.execute(
|
||||
"""
|
||||
UPDATE apipayments SET extra = ?
|
||||
WHERE hash = ?
|
||||
""",
|
||||
(json.dumps(payment.extra), payment.payment_hash),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@ templates = Jinja2Templates(directory="templates")
|
|||
|
||||
|
||||
@copilot_ext.get("/", response_class=HTMLResponse)
|
||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||
async def index(
|
||||
request: Request, user: User = Depends(check_user_exists) # type: ignore
|
||||
):
|
||||
return copilot_renderer().TemplateResponse(
|
||||
"copilot/index.html", {"request": request, "user": user.dict()}
|
||||
)
|
||||
|
|
@ -44,7 +46,7 @@ class ConnectionManager:
|
|||
|
||||
async def connect(self, websocket: WebSocket, copilot_id: str):
|
||||
await websocket.accept()
|
||||
websocket.id = copilot_id
|
||||
websocket.id = copilot_id # type: ignore
|
||||
self.active_connections.append(websocket)
|
||||
|
||||
def disconnect(self, websocket: WebSocket):
|
||||
|
|
@ -52,7 +54,7 @@ class ConnectionManager:
|
|||
|
||||
async def send_personal_message(self, message: str, copilot_id: str):
|
||||
for connection in self.active_connections:
|
||||
if connection.id == copilot_id:
|
||||
if connection.id == copilot_id: # type: ignore
|
||||
await connection.send_text(message)
|
||||
|
||||
async def broadcast(self, message: str):
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ from .views import updater
|
|||
|
||||
@copilot_ext.get("/api/v1/copilot")
|
||||
async def api_copilots_retrieve(
|
||||
req: Request, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
req: Request, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||
):
|
||||
wallet_user = wallet.wallet.user
|
||||
copilots = [copilot.dict() for copilot in await get_copilots(wallet_user)]
|
||||
|
|
@ -37,7 +37,7 @@ async def api_copilots_retrieve(
|
|||
async def api_copilot_retrieve(
|
||||
req: Request,
|
||||
copilot_id: str = Query(None),
|
||||
wallet: WalletTypeInfo = Depends(get_key_type),
|
||||
wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
|
||||
):
|
||||
copilot = await get_copilot(copilot_id)
|
||||
if not copilot:
|
||||
|
|
@ -54,7 +54,7 @@ async def api_copilot_retrieve(
|
|||
async def api_copilot_create_or_update(
|
||||
data: CreateCopilotData,
|
||||
copilot_id: str = Query(None),
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key), # type: ignore
|
||||
):
|
||||
data.user = wallet.wallet.user
|
||||
data.wallet = wallet.wallet.id
|
||||
|
|
@ -67,7 +67,8 @@ async def api_copilot_create_or_update(
|
|||
|
||||
@copilot_ext.delete("/api/v1/copilot/{copilot_id}")
|
||||
async def api_copilot_delete(
|
||||
copilot_id: str = Query(None), wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
copilot_id: str = Query(None),
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key), # type: ignore
|
||||
):
|
||||
copilot = await get_copilot(copilot_id)
|
||||
|
||||
|
|
|
|||
|
|
@ -98,21 +98,21 @@ async def get_discordbot_wallet(wallet_id: str) -> Optional[Wallets]:
|
|||
return Wallets(**row) if row else None
|
||||
|
||||
|
||||
async def get_discordbot_wallets(admin_id: str) -> Optional[Wallets]:
|
||||
async def get_discordbot_wallets(admin_id: str) -> List[Wallets]:
|
||||
rows = await db.fetchall(
|
||||
"SELECT * FROM discordbot.wallets WHERE admin = ?", (admin_id,)
|
||||
)
|
||||
return [Wallets(**row) for row in rows]
|
||||
|
||||
|
||||
async def get_discordbot_users_wallets(user_id: str) -> Optional[Wallets]:
|
||||
async def get_discordbot_users_wallets(user_id: str) -> List[Wallets]:
|
||||
rows = await db.fetchall(
|
||||
"""SELECT * FROM discordbot.wallets WHERE "user" = ?""", (user_id,)
|
||||
)
|
||||
return [Wallets(**row) for row in rows]
|
||||
|
||||
|
||||
async def get_discordbot_wallet_transactions(wallet_id: str) -> Optional[Payment]:
|
||||
async def get_discordbot_wallet_transactions(wallet_id: str) -> List[Payment]:
|
||||
return await get_payments(
|
||||
wallet_id=wallet_id, complete=True, pending=False, outgoing=True, incoming=True
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ from . import discordbot_ext, discordbot_renderer
|
|||
|
||||
|
||||
@discordbot_ext.get("/", response_class=HTMLResponse)
|
||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||
async def index(
|
||||
request: Request, user: User = Depends(check_user_exists) # type: ignore
|
||||
):
|
||||
return discordbot_renderer().TemplateResponse(
|
||||
"discordbot/index.html", {"request": request, "user": user.dict()}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -27,32 +27,37 @@ from .models import CreateUserData, CreateUserWallet
|
|||
|
||||
|
||||
@discordbot_ext.get("/api/v1/users", status_code=HTTPStatus.OK)
|
||||
async def api_discordbot_users(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
async def api_discordbot_users(
|
||||
wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
|
||||
):
|
||||
user_id = wallet.wallet.user
|
||||
return [user.dict() for user in await get_discordbot_users(user_id)]
|
||||
|
||||
|
||||
@discordbot_ext.get("/api/v1/users/{user_id}", status_code=HTTPStatus.OK)
|
||||
async def api_discordbot_user(user_id, wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
async def api_discordbot_user(
|
||||
user_id, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||
):
|
||||
user = await get_discordbot_user(user_id)
|
||||
return user.dict()
|
||||
if user:
|
||||
return user.dict()
|
||||
|
||||
|
||||
@discordbot_ext.post("/api/v1/users", status_code=HTTPStatus.CREATED)
|
||||
async def api_discordbot_users_create(
|
||||
data: CreateUserData, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
data: CreateUserData, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||
):
|
||||
user = await create_discordbot_user(data)
|
||||
full = user.dict()
|
||||
full["wallets"] = [
|
||||
wallet.dict() for wallet in await get_discordbot_users_wallets(user.id)
|
||||
]
|
||||
wallets = await get_discordbot_users_wallets(user.id)
|
||||
if wallets:
|
||||
full["wallets"] = [wallet for wallet in wallets]
|
||||
return full
|
||||
|
||||
|
||||
@discordbot_ext.delete("/api/v1/users/{user_id}")
|
||||
async def api_discordbot_users_delete(
|
||||
user_id, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
user_id, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||
):
|
||||
user = await get_discordbot_user(user_id)
|
||||
if not user:
|
||||
|
|
@ -60,7 +65,7 @@ async def api_discordbot_users_delete(
|
|||
status_code=HTTPStatus.NOT_FOUND, detail="User does not exist."
|
||||
)
|
||||
await delete_discordbot_user(user_id)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
# Activate Extension
|
||||
|
|
@ -75,7 +80,7 @@ async def api_discordbot_activate_extension(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="User does not exist."
|
||||
)
|
||||
update_user_extension(user_id=userid, extension=extension, active=active)
|
||||
await update_user_extension(user_id=userid, extension=extension, active=active)
|
||||
return {"extension": "updated"}
|
||||
|
||||
|
||||
|
|
@ -84,7 +89,7 @@ async def api_discordbot_activate_extension(
|
|||
|
||||
@discordbot_ext.post("/api/v1/wallets")
|
||||
async def api_discordbot_wallets_create(
|
||||
data: CreateUserWallet, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
data: CreateUserWallet, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||
):
|
||||
user = await create_discordbot_wallet(
|
||||
user_id=data.user_id, wallet_name=data.wallet_name, admin_id=data.admin_id
|
||||
|
|
@ -93,28 +98,30 @@ async def api_discordbot_wallets_create(
|
|||
|
||||
|
||||
@discordbot_ext.get("/api/v1/wallets")
|
||||
async def api_discordbot_wallets(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
async def api_discordbot_wallets(
|
||||
wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
|
||||
):
|
||||
admin_id = wallet.wallet.user
|
||||
return [wallet.dict() for wallet in await get_discordbot_wallets(admin_id)]
|
||||
return await get_discordbot_wallets(admin_id)
|
||||
|
||||
|
||||
@discordbot_ext.get("/api/v1/transactions/{wallet_id}")
|
||||
async def api_discordbot_wallet_transactions(
|
||||
wallet_id, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
wallet_id, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||
):
|
||||
return await get_discordbot_wallet_transactions(wallet_id)
|
||||
|
||||
|
||||
@discordbot_ext.get("/api/v1/wallets/{user_id}")
|
||||
async def api_discordbot_users_wallets(
|
||||
user_id, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
user_id, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||
):
|
||||
return [s_wallet.dict() for s_wallet in await get_discordbot_users_wallets(user_id)]
|
||||
return await get_discordbot_users_wallets(user_id)
|
||||
|
||||
|
||||
@discordbot_ext.delete("/api/v1/wallets/{wallet_id}")
|
||||
async def api_discordbot_wallets_delete(
|
||||
wallet_id, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
wallet_id, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||
):
|
||||
get_wallet = await get_discordbot_wallet(wallet_id)
|
||||
if not get_wallet:
|
||||
|
|
@ -122,4 +129,4 @@ async def api_discordbot_wallets_delete(
|
|||
status_code=HTTPStatus.NOT_FOUND, detail="Wallet does not exist."
|
||||
)
|
||||
await delete_discordbot_wallet(wallet_id, get_wallet.user)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import asyncio
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from lnbits.db import Database
|
||||
from lnbits.helpers import template_renderer
|
||||
from lnbits.tasks import catch_everything_and_restart
|
||||
|
||||
db = Database("ext_events")
|
||||
|
||||
|
|
@ -13,5 +16,11 @@ def events_renderer():
|
|||
return template_renderer(["lnbits/extensions/events/templates"])
|
||||
|
||||
|
||||
from .tasks import wait_for_paid_invoices
|
||||
from .views import * # noqa
|
||||
from .views_api import * # noqa
|
||||
|
||||
|
||||
def events_start():
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
|
||||
|
|
|
|||
39
lnbits/extensions/events/tasks.py
Normal file
39
lnbits/extensions/events/tasks.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import asyncio
|
||||
import json
|
||||
from http import HTTPStatus
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import httpx
|
||||
from fastapi import HTTPException
|
||||
from loguru import logger
|
||||
|
||||
from lnbits import bolt11
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.core.services import pay_invoice
|
||||
from lnbits.extensions.events.models import CreateTicket
|
||||
from lnbits.helpers import get_current_extension_name
|
||||
from lnbits.tasks import register_invoice_listener
|
||||
|
||||
from .views_api import api_ticket_send_ticket
|
||||
|
||||
|
||||
async def wait_for_paid_invoices():
|
||||
invoice_queue = asyncio.Queue()
|
||||
register_invoice_listener(invoice_queue, get_current_extension_name())
|
||||
|
||||
while True:
|
||||
payment = await invoice_queue.get()
|
||||
await on_invoice_paid(payment)
|
||||
|
||||
|
||||
async def on_invoice_paid(payment: Payment) -> None:
|
||||
# (avoid loops)
|
||||
if (
|
||||
"events" == payment.extra.get("tag")
|
||||
and payment.extra.get("name")
|
||||
and payment.extra.get("email")
|
||||
):
|
||||
CreateTicket.name = str(payment.extra.get("name"))
|
||||
CreateTicket.email = str(payment.extra.get("email"))
|
||||
await api_ticket_send_ticket(payment.memo, payment.payment_hash, CreateTicket)
|
||||
return
|
||||
|
|
@ -135,7 +135,14 @@
|
|||
var self = this
|
||||
axios
|
||||
|
||||
.get('/events/api/v1/tickets/' + '{{ event_id }}')
|
||||
.get(
|
||||
'/events/api/v1/tickets/' +
|
||||
'{{ event_id }}' +
|
||||
'/' +
|
||||
self.formDialog.data.name +
|
||||
'/' +
|
||||
self.formDialog.data.email
|
||||
)
|
||||
.then(function (response) {
|
||||
self.paymentReq = response.data.payment_request
|
||||
self.paymentCheck = response.data.payment_hash
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from http import HTTPStatus
|
|||
|
||||
from fastapi.param_functions import Query
|
||||
from fastapi.params import Depends
|
||||
from loguru import logger
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.requests import Request
|
||||
|
||||
|
|
@ -78,7 +79,7 @@ async def api_form_delete(event_id, wallet: WalletTypeInfo = Depends(get_key_typ
|
|||
|
||||
await delete_event(event_id)
|
||||
await delete_event_tickets(event_id)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
#########Tickets##########
|
||||
|
|
@ -96,8 +97,8 @@ async def api_tickets(
|
|||
return [ticket.dict() for ticket in await get_tickets(wallet_ids)]
|
||||
|
||||
|
||||
@events_ext.get("/api/v1/tickets/{event_id}")
|
||||
async def api_ticket_make_ticket(event_id):
|
||||
@events_ext.get("/api/v1/tickets/{event_id}/{name}/{email}")
|
||||
async def api_ticket_make_ticket(event_id, name, email):
|
||||
event = await get_event(event_id)
|
||||
if not event:
|
||||
raise HTTPException(
|
||||
|
|
@ -108,11 +109,10 @@ async def api_ticket_make_ticket(event_id):
|
|||
wallet_id=event.wallet,
|
||||
amount=event.price_per_ticket,
|
||||
memo=f"{event_id}",
|
||||
extra={"tag": "events"},
|
||||
extra={"tag": "events", "name": name, "email": email},
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
|
||||
|
||||
return {"payment_hash": payment_hash, "payment_request": payment_request}
|
||||
|
||||
|
||||
|
|
@ -156,7 +156,7 @@ async def api_ticket_delete(ticket_id, wallet: WalletTypeInfo = Depends(get_key_
|
|||
)
|
||||
|
||||
await delete_ticket(ticket_id)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
# Event Tickets
|
||||
|
|
|
|||
|
|
@ -12,7 +12,10 @@ templates = Jinja2Templates(directory="templates")
|
|||
|
||||
|
||||
@example_ext.get("/", response_class=HTMLResponse)
|
||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||
async def index(
|
||||
request: Request,
|
||||
user: User = Depends(check_user_exists), # type: ignore
|
||||
):
|
||||
return example_renderer().TemplateResponse(
|
||||
"example/index.html", {"request": request, "user": user.dict()}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ async def m001_initial_invoices(db):
|
|||
id TEXT PRIMARY KEY,
|
||||
invoice_id TEXT NOT NULL,
|
||||
|
||||
amount INT NOT NULL,
|
||||
amount {db.big_int} NOT NULL,
|
||||
|
||||
time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from typing import List, Optional
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
|
|
@ -6,11 +6,9 @@ from . import db
|
|||
from .models import CreateJukeboxPayment, CreateJukeLinkData, Jukebox, JukeboxPayment
|
||||
|
||||
|
||||
async def create_jukebox(
|
||||
data: CreateJukeLinkData, inkey: Optional[str] = ""
|
||||
) -> Jukebox:
|
||||
async def create_jukebox(data: CreateJukeLinkData) -> Jukebox:
|
||||
juke_id = urlsafe_short_hash()
|
||||
result = await db.execute(
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO jukebox.jukebox (id, "user", title, wallet, sp_user, sp_secret, sp_access_token, sp_refresh_token, sp_device, sp_playlists, price, profit)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
|
|
@ -36,13 +34,13 @@ async def create_jukebox(
|
|||
|
||||
|
||||
async def update_jukebox(
|
||||
data: CreateJukeLinkData, juke_id: Optional[str] = ""
|
||||
data: Union[CreateJukeLinkData, Jukebox], juke_id: str = ""
|
||||
) -> Optional[Jukebox]:
|
||||
q = ", ".join([f"{field[0]} = ?" for field in data])
|
||||
items = [f"{field[1]}" for field in data]
|
||||
items.append(juke_id)
|
||||
q = q.replace("user", '"user"', 1) # hack to make user be "user"!
|
||||
await db.execute(f"UPDATE jukebox.jukebox SET {q} WHERE id = ?", (items))
|
||||
await db.execute(f"UPDATE jukebox.jukebox SET {q} WHERE id = ?", (items,))
|
||||
row = await db.fetchone("SELECT * FROM jukebox.jukebox WHERE id = ?", (juke_id,))
|
||||
return Jukebox(**row) if row else None
|
||||
|
||||
|
|
@ -72,7 +70,7 @@ async def delete_jukebox(juke_id: str):
|
|||
"""
|
||||
DELETE FROM jukebox.jukebox WHERE id = ?
|
||||
""",
|
||||
(juke_id),
|
||||
(juke_id,),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -80,7 +78,7 @@ async def delete_jukebox(juke_id: str):
|
|||
|
||||
|
||||
async def create_jukebox_payment(data: CreateJukeboxPayment) -> JukeboxPayment:
|
||||
result = await db.execute(
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO jukebox.jukebox_payment (payment_hash, juke_id, song_id, paid)
|
||||
VALUES (?, ?, ?, ?)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
from sqlite3 import Row
|
||||
from typing import NamedTuple, Optional
|
||||
|
||||
from fastapi.param_functions import Query
|
||||
from pydantic import BaseModel
|
||||
from pydantic.main import BaseModel
|
||||
|
|
@ -20,19 +17,19 @@ class CreateJukeLinkData(BaseModel):
|
|||
|
||||
|
||||
class Jukebox(BaseModel):
|
||||
id: Optional[str]
|
||||
user: Optional[str]
|
||||
title: Optional[str]
|
||||
wallet: Optional[str]
|
||||
inkey: Optional[str]
|
||||
sp_user: Optional[str]
|
||||
sp_secret: Optional[str]
|
||||
sp_access_token: Optional[str]
|
||||
sp_refresh_token: Optional[str]
|
||||
sp_device: Optional[str]
|
||||
sp_playlists: Optional[str]
|
||||
price: Optional[int]
|
||||
profit: Optional[int]
|
||||
id: str
|
||||
user: str
|
||||
title: str
|
||||
wallet: str
|
||||
inkey: str
|
||||
sp_user: str
|
||||
sp_secret: str
|
||||
sp_access_token: str
|
||||
sp_refresh_token: str
|
||||
sp_device: str
|
||||
sp_playlists: str
|
||||
price: int
|
||||
profit: int
|
||||
|
||||
|
||||
class JukeboxPayment(BaseModel):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import asyncio
|
||||
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.helpers import get_current_extension_name
|
||||
from lnbits.tasks import register_invoice_listener
|
||||
|
||||
from .crud import update_jukebox_payment
|
||||
|
|
@ -8,7 +9,7 @@ from .crud import update_jukebox_payment
|
|||
|
||||
async def wait_for_paid_invoices():
|
||||
invoice_queue = asyncio.Queue()
|
||||
register_invoice_listener(invoice_queue)
|
||||
register_invoice_listener(invoice_queue, get_current_extension_name())
|
||||
|
||||
while True:
|
||||
payment = await invoice_queue.get()
|
||||
|
|
@ -16,7 +17,8 @@ async def wait_for_paid_invoices():
|
|||
|
||||
|
||||
async def on_invoice_paid(payment: Payment) -> None:
|
||||
if payment.extra.get("tag") != "jukebox":
|
||||
# not a jukebox invoice
|
||||
return
|
||||
await update_jukebox_payment(payment.payment_hash, paid=True)
|
||||
if payment.extra:
|
||||
if payment.extra.get("tag") != "jukebox":
|
||||
# not a jukebox invoice
|
||||
return
|
||||
await update_jukebox_payment(payment.payment_hash, paid=True)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@ templates = Jinja2Templates(directory="templates")
|
|||
|
||||
|
||||
@jukebox_ext.get("/", response_class=HTMLResponse)
|
||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||
async def index(
|
||||
request: Request, user: User = Depends(check_user_exists) # type: ignore
|
||||
):
|
||||
return jukebox_renderer().TemplateResponse(
|
||||
"jukebox/index.html", {"request": request, "user": user.dict()}
|
||||
)
|
||||
|
|
@ -31,6 +33,7 @@ async def connect_to_jukebox(request: Request, juke_id):
|
|||
status_code=HTTPStatus.NOT_FOUND, detail="Jukebox does not exist."
|
||||
)
|
||||
devices = await api_get_jukebox_device_check(juke_id)
|
||||
deviceConnected = False
|
||||
for device in devices["devices"]:
|
||||
if device["id"] == jukebox.sp_device.split("-")[1]:
|
||||
deviceConnected = True
|
||||
|
|
@ -48,5 +51,5 @@ async def connect_to_jukebox(request: Request, juke_id):
|
|||
else:
|
||||
return jukebox_renderer().TemplateResponse(
|
||||
"jukebox/error.html",
|
||||
{"request": request, "jukebox": jukebox.jukebox(req=request)},
|
||||
{"request": request, "jukebox": jukebox.dict()},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import json
|
|||
from http import HTTPStatus
|
||||
|
||||
import httpx
|
||||
from fastapi import Request
|
||||
from fastapi.param_functions import Query
|
||||
from fastapi.params import Depends
|
||||
from starlette.exceptions import HTTPException
|
||||
|
|
@ -29,9 +28,7 @@ from .models import CreateJukeboxPayment, CreateJukeLinkData
|
|||
|
||||
@jukebox_ext.get("/api/v1/jukebox")
|
||||
async def api_get_jukeboxs(
|
||||
req: Request,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
all_wallets: bool = Query(False),
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key), # type: ignore
|
||||
):
|
||||
wallet_user = wallet.wallet.user
|
||||
|
||||
|
|
@ -53,54 +50,52 @@ async def api_check_credentials_callbac(
|
|||
access_token: str = Query(None),
|
||||
refresh_token: str = Query(None),
|
||||
):
|
||||
sp_code = ""
|
||||
sp_access_token = ""
|
||||
sp_refresh_token = ""
|
||||
try:
|
||||
jukebox = await get_jukebox(juke_id)
|
||||
except:
|
||||
jukebox = await get_jukebox(juke_id)
|
||||
if not jukebox:
|
||||
raise HTTPException(detail="No Jukebox", status_code=HTTPStatus.FORBIDDEN)
|
||||
if code:
|
||||
jukebox.sp_access_token = code
|
||||
jukebox = await update_jukebox(jukebox, juke_id=juke_id)
|
||||
await update_jukebox(jukebox, juke_id=juke_id)
|
||||
if access_token:
|
||||
jukebox.sp_access_token = access_token
|
||||
jukebox.sp_refresh_token = refresh_token
|
||||
jukebox = await update_jukebox(jukebox, juke_id=juke_id)
|
||||
await update_jukebox(jukebox, juke_id=juke_id)
|
||||
return "<h1>Success!</h1><h2>You can close this window</h2>"
|
||||
|
||||
|
||||
@jukebox_ext.get("/api/v1/jukebox/{juke_id}")
|
||||
async def api_check_credentials_check(
|
||||
juke_id: str = Query(None), wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
):
|
||||
@jukebox_ext.get("/api/v1/jukebox/{juke_id}", dependencies=[Depends(require_admin_key)])
|
||||
async def api_check_credentials_check(juke_id: str = Query(None)):
|
||||
jukebox = await get_jukebox(juke_id)
|
||||
return jukebox
|
||||
|
||||
|
||||
@jukebox_ext.post("/api/v1/jukebox", status_code=HTTPStatus.CREATED)
|
||||
@jukebox_ext.post(
|
||||
"/api/v1/jukebox",
|
||||
status_code=HTTPStatus.CREATED,
|
||||
dependencies=[Depends(require_admin_key)],
|
||||
)
|
||||
@jukebox_ext.put("/api/v1/jukebox/{juke_id}", status_code=HTTPStatus.OK)
|
||||
async def api_create_update_jukebox(
|
||||
data: CreateJukeLinkData,
|
||||
juke_id: str = Query(None),
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
data: CreateJukeLinkData, juke_id: str = Query(None)
|
||||
):
|
||||
if juke_id:
|
||||
jukebox = await update_jukebox(data, juke_id=juke_id)
|
||||
else:
|
||||
jukebox = await create_jukebox(data, inkey=wallet.wallet.inkey)
|
||||
jukebox = await create_jukebox(data)
|
||||
return jukebox
|
||||
|
||||
|
||||
@jukebox_ext.delete("/api/v1/jukebox/{juke_id}")
|
||||
@jukebox_ext.delete(
|
||||
"/api/v1/jukebox/{juke_id}", dependencies=[Depends(require_admin_key)]
|
||||
)
|
||||
async def api_delete_item(
|
||||
juke_id=None, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
juke_id: str = Query(None),
|
||||
):
|
||||
await delete_jukebox(juke_id)
|
||||
try:
|
||||
return [{**jukebox} for jukebox in await get_jukeboxs(wallet.wallet.user)]
|
||||
except:
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT, detail="No Jukebox")
|
||||
# try:
|
||||
# return [{**jukebox} for jukebox in await get_jukeboxs(wallet.wallet.user)]
|
||||
# except:
|
||||
# raise HTTPException(status_code=HTTPStatus.NO_CONTENT, detail="No Jukebox")
|
||||
|
||||
|
||||
################JUKEBOX ENDPOINTS##################
|
||||
|
|
@ -114,9 +109,8 @@ async def api_get_jukebox_song(
|
|||
sp_playlist: str = Query(None),
|
||||
retry: bool = Query(False),
|
||||
):
|
||||
try:
|
||||
jukebox = await get_jukebox(juke_id)
|
||||
except:
|
||||
jukebox = await get_jukebox(juke_id)
|
||||
if not jukebox:
|
||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No Jukeboxes")
|
||||
tracks = []
|
||||
async with httpx.AsyncClient() as client:
|
||||
|
|
@ -152,14 +146,13 @@ async def api_get_jukebox_song(
|
|||
}
|
||||
)
|
||||
except:
|
||||
something = None
|
||||
pass
|
||||
return [track for track in tracks]
|
||||
|
||||
|
||||
async def api_get_token(juke_id=None):
|
||||
try:
|
||||
jukebox = await get_jukebox(juke_id)
|
||||
except:
|
||||
async def api_get_token(juke_id):
|
||||
jukebox = await get_jukebox(juke_id)
|
||||
if not jukebox:
|
||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No Jukeboxes")
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
|
|
@ -187,7 +180,7 @@ async def api_get_token(juke_id=None):
|
|||
jukebox.sp_access_token = r.json()["access_token"]
|
||||
await update_jukebox(jukebox, juke_id=juke_id)
|
||||
except:
|
||||
something = None
|
||||
pass
|
||||
return True
|
||||
|
||||
|
||||
|
|
@ -198,9 +191,8 @@ async def api_get_token(juke_id=None):
|
|||
async def api_get_jukebox_device_check(
|
||||
juke_id: str = Query(None), retry: bool = Query(False)
|
||||
):
|
||||
try:
|
||||
jukebox = await get_jukebox(juke_id)
|
||||
except:
|
||||
jukebox = await get_jukebox(juke_id)
|
||||
if not jukebox:
|
||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No Jukeboxes")
|
||||
async with httpx.AsyncClient() as client:
|
||||
rDevice = await client.get(
|
||||
|
|
@ -221,7 +213,7 @@ async def api_get_jukebox_device_check(
|
|||
status_code=HTTPStatus.FORBIDDEN, detail="Failed to get auth"
|
||||
)
|
||||
else:
|
||||
return api_get_jukebox_device_check(juke_id, retry=True)
|
||||
return await api_get_jukebox_device_check(juke_id, retry=True)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN, detail="No device connected"
|
||||
|
|
@ -233,10 +225,8 @@ async def api_get_jukebox_device_check(
|
|||
|
||||
@jukebox_ext.get("/api/v1/jukebox/jb/invoice/{juke_id}/{song_id}")
|
||||
async def api_get_jukebox_invoice(juke_id, song_id):
|
||||
try:
|
||||
jukebox = await get_jukebox(juke_id)
|
||||
|
||||
except:
|
||||
jukebox = await get_jukebox(juke_id)
|
||||
if not jukebox:
|
||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No jukebox")
|
||||
try:
|
||||
|
||||
|
|
@ -266,8 +256,7 @@ async def api_get_jukebox_invoice(juke_id, song_id):
|
|||
invoice=invoice[1], payment_hash=payment_hash, juke_id=juke_id, song_id=song_id
|
||||
)
|
||||
jukebox_payment = await create_jukebox_payment(data)
|
||||
|
||||
return data
|
||||
return jukebox_payment
|
||||
|
||||
|
||||
@jukebox_ext.get("/api/v1/jukebox/jb/checkinvoice/{pay_hash}/{juke_id}")
|
||||
|
|
@ -296,13 +285,12 @@ async def api_get_jukebox_invoice_paid(
|
|||
pay_hash: str = Query(None),
|
||||
retry: bool = Query(False),
|
||||
):
|
||||
try:
|
||||
jukebox = await get_jukebox(juke_id)
|
||||
except:
|
||||
jukebox = await get_jukebox(juke_id)
|
||||
if not jukebox:
|
||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No jukebox")
|
||||
await api_get_jukebox_invoice_check(pay_hash, juke_id)
|
||||
jukebox_payment = await get_jukebox_payment(pay_hash)
|
||||
if jukebox_payment.paid:
|
||||
if jukebox_payment and jukebox_payment.paid:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.get(
|
||||
"https://api.spotify.com/v1/me/player/currently-playing?market=ES",
|
||||
|
|
@ -407,9 +395,8 @@ async def api_get_jukebox_invoice_paid(
|
|||
async def api_get_jukebox_currently(
|
||||
retry: bool = Query(False), juke_id: str = Query(None)
|
||||
):
|
||||
try:
|
||||
jukebox = await get_jukebox(juke_id)
|
||||
except:
|
||||
jukebox = await get_jukebox(juke_id)
|
||||
if not jukebox:
|
||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No jukebox")
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -4,17 +4,17 @@ import json
|
|||
from loguru import logger
|
||||
|
||||
from lnbits.core import db as core_db
|
||||
from lnbits.core.crud import create_payment
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
from lnbits.tasks import internal_invoice_listener, register_invoice_listener
|
||||
from lnbits.core.services import create_invoice, pay_invoice
|
||||
from lnbits.helpers import get_current_extension_name
|
||||
from lnbits.tasks import register_invoice_listener
|
||||
|
||||
from .crud import get_livestream_by_track, get_producer, get_track
|
||||
|
||||
|
||||
async def wait_for_paid_invoices():
|
||||
invoice_queue = asyncio.Queue()
|
||||
register_invoice_listener(invoice_queue)
|
||||
register_invoice_listener(invoice_queue, get_current_extension_name())
|
||||
|
||||
while True:
|
||||
payment = await invoice_queue.get()
|
||||
|
|
@ -44,44 +44,20 @@ async def on_invoice_paid(payment: Payment) -> None:
|
|||
# now we make a special kind of internal transfer
|
||||
amount = int(payment.amount * (100 - ls.fee_pct) / 100)
|
||||
|
||||
# mark the original payment with two extra keys, "shared_with" and "received"
|
||||
# (this prevents us from doing this process again and it's informative)
|
||||
# and reduce it by the amount we're going to send to the producer
|
||||
await core_db.execute(
|
||||
"""
|
||||
UPDATE apipayments
|
||||
SET extra = ?, amount = ?
|
||||
WHERE hash = ?
|
||||
AND checking_id NOT LIKE 'internal_%'
|
||||
""",
|
||||
(
|
||||
json.dumps(
|
||||
dict(
|
||||
**payment.extra,
|
||||
shared_with=[producer.name, producer.id],
|
||||
received=payment.amount,
|
||||
)
|
||||
),
|
||||
payment.amount - amount,
|
||||
payment.payment_hash,
|
||||
),
|
||||
)
|
||||
|
||||
# perform an internal transfer using the same payment_hash to the producer wallet
|
||||
internal_checking_id = f"internal_{urlsafe_short_hash()}"
|
||||
await create_payment(
|
||||
wallet_id=producer.wallet,
|
||||
checking_id=internal_checking_id,
|
||||
payment_request="",
|
||||
payment_hash=payment.payment_hash,
|
||||
amount=amount,
|
||||
payment_hash, payment_request = await create_invoice(
|
||||
wallet_id=tpos.tip_wallet,
|
||||
amount=amount, # sats
|
||||
internal=True,
|
||||
memo=f"Revenue from '{track.name}'.",
|
||||
pending=False,
|
||||
)
|
||||
logger.debug(f"livestream: producer invoice created: {payment_hash}")
|
||||
|
||||
# manually send this for now
|
||||
# await internal_invoice_paid.send(internal_checking_id)
|
||||
await internal_invoice_listener.put(internal_checking_id)
|
||||
checking_id = await pay_invoice(
|
||||
payment_request=payment_request,
|
||||
wallet_id=payment.wallet_id,
|
||||
extra={"tag": "livestream"},
|
||||
)
|
||||
logger.debug(f"livestream: producer invoice paid: {checking_id}")
|
||||
|
||||
# so the flow is the following:
|
||||
# - we receive, say, 1000 satoshis
|
||||
|
|
|
|||
|
|
@ -60,14 +60,14 @@ async def api_update_track(track_id, g: WalletTypeInfo = Depends(get_key_type)):
|
|||
|
||||
ls = await get_or_create_livestream_by_wallet(g.wallet.id)
|
||||
await update_current_track(ls.id, id)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
@livestream_ext.put("/api/v1/livestream/fee/{fee_pct}")
|
||||
async def api_update_fee(fee_pct, g: WalletTypeInfo = Depends(get_key_type)):
|
||||
ls = await get_or_create_livestream_by_wallet(g.wallet.id)
|
||||
await update_livestream_fee(ls.id, int(fee_pct))
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
@livestream_ext.post("/api/v1/livestream/tracks")
|
||||
|
|
@ -93,8 +93,8 @@ async def api_add_track(
|
|||
return
|
||||
|
||||
|
||||
@livestream_ext.route("/api/v1/livestream/tracks/{track_id}")
|
||||
@livestream_ext.delete("/api/v1/livestream/tracks/{track_id}")
|
||||
async def api_delete_track(track_id, g: WalletTypeInfo = Depends(get_key_type)):
|
||||
ls = await get_or_create_livestream_by_wallet(g.wallet.id)
|
||||
await delete_track_from_livestream(ls.id, track_id)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import asyncio
|
|||
import httpx
|
||||
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.helpers import get_current_extension_name
|
||||
from lnbits.tasks import register_invoice_listener
|
||||
|
||||
from .crud import get_address, get_domain, set_address_paid, set_address_renewed
|
||||
|
|
@ -10,7 +11,7 @@ from .crud import get_address, get_domain, set_address_paid, set_address_renewed
|
|||
|
||||
async def wait_for_paid_invoices():
|
||||
invoice_queue = asyncio.Queue()
|
||||
register_invoice_listener(invoice_queue)
|
||||
register_invoice_listener(invoice_queue, get_current_extension_name())
|
||||
|
||||
while True:
|
||||
payment = await invoice_queue.get()
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ async def api_domain_delete(domain_id, g: WalletTypeInfo = Depends(get_key_type)
|
|||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your domain")
|
||||
|
||||
await delete_domain(domain_id)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
# ADDRESSES
|
||||
|
|
@ -253,4 +253,4 @@ async def api_address_delete(address_id, g: WalletTypeInfo = Depends(get_key_typ
|
|||
)
|
||||
|
||||
await delete_address(address_id)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import asyncio
|
||||
import json
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import asyncio
|
|||
from loguru import logger
|
||||
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.helpers import get_current_extension_name
|
||||
from lnbits.tasks import register_invoice_listener
|
||||
|
||||
from .crud import get_ticket, set_ticket_paid
|
||||
|
|
@ -10,7 +11,7 @@ from .crud import get_ticket, set_ticket_paid
|
|||
|
||||
async def wait_for_paid_invoices():
|
||||
invoice_queue = asyncio.Queue()
|
||||
register_invoice_listener(invoice_queue)
|
||||
register_invoice_listener(invoice_queue, get_current_extension_name())
|
||||
|
||||
while True:
|
||||
payment = await invoice_queue.get()
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ async def api_form_delete(form_id, wallet: WalletTypeInfo = Depends(get_key_type
|
|||
|
||||
await delete_form(form_id)
|
||||
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
#########tickets##########
|
||||
|
|
@ -160,4 +160,4 @@ async def api_ticket_delete(ticket_id, wallet: WalletTypeInfo = Depends(get_key_
|
|||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your ticket.")
|
||||
|
||||
await delete_ticket(ticket_id)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import asyncio
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from lnbits.db import Database
|
||||
from lnbits.helpers import template_renderer
|
||||
from lnbits.tasks import catch_everything_and_restart
|
||||
|
||||
db = Database("ext_lnurldevice")
|
||||
|
||||
|
|
@ -13,5 +16,11 @@ def lnurldevice_renderer():
|
|||
|
||||
|
||||
from .lnurl import * # noqa
|
||||
from .tasks import wait_for_paid_invoices
|
||||
from .views import * # noqa
|
||||
from .views_api import * # noqa
|
||||
|
||||
|
||||
def lnurldevice_start():
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
|
||||
|
|
|
|||
|
|
@ -22,9 +22,23 @@ async def create_lnurldevice(
|
|||
wallet,
|
||||
currency,
|
||||
device,
|
||||
profit
|
||||
profit,
|
||||
amount,
|
||||
pin,
|
||||
profit1,
|
||||
amount1,
|
||||
pin1,
|
||||
profit2,
|
||||
amount2,
|
||||
pin2,
|
||||
profit3,
|
||||
amount3,
|
||||
pin3,
|
||||
profit4,
|
||||
amount4,
|
||||
pin4
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
lnurldevice_id,
|
||||
|
|
@ -34,6 +48,20 @@ async def create_lnurldevice(
|
|||
data.currency,
|
||||
data.device,
|
||||
data.profit,
|
||||
data.amount,
|
||||
data.pin,
|
||||
data.profit1,
|
||||
data.amount1,
|
||||
data.pin1,
|
||||
data.profit2,
|
||||
data.amount2,
|
||||
data.pin2,
|
||||
data.profit3,
|
||||
data.amount3,
|
||||
data.pin3,
|
||||
data.profit4,
|
||||
data.amount4,
|
||||
data.pin4,
|
||||
),
|
||||
)
|
||||
return await get_lnurldevice(lnurldevice_id)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from typing import Optional
|
|||
from embit import bech32, compact
|
||||
from fastapi import Request
|
||||
from fastapi.param_functions import Query
|
||||
from loguru import logger
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from lnbits.core.services import create_invoice
|
||||
|
|
@ -91,6 +92,9 @@ async def lnurl_v1_params(
|
|||
device_id: str = Query(None),
|
||||
p: str = Query(None),
|
||||
atm: str = Query(None),
|
||||
gpio: str = Query(None),
|
||||
profit: str = Query(None),
|
||||
amount: str = Query(None),
|
||||
):
|
||||
device = await get_lnurldevice(device_id)
|
||||
if not device:
|
||||
|
|
@ -101,8 +105,41 @@ async def lnurl_v1_params(
|
|||
paymentcheck = await get_lnurlpayload(p)
|
||||
if device.device == "atm":
|
||||
if paymentcheck:
|
||||
return {"status": "ERROR", "reason": f"Payment already claimed"}
|
||||
if paymentcheck.payhash != "payment_hash":
|
||||
return {"status": "ERROR", "reason": f"Payment already claimed"}
|
||||
if device.device == "switch":
|
||||
price_msat = (
|
||||
await fiat_amount_as_satoshis(float(profit), device.currency)
|
||||
if device.currency != "sat"
|
||||
else amount_in_cent
|
||||
) * 1000
|
||||
|
||||
# Check they're not trying to trick the switch!
|
||||
check = False
|
||||
for switch in device.switches(request):
|
||||
if switch[0] == gpio and switch[1] == profit and switch[2] == amount:
|
||||
check = True
|
||||
if not check:
|
||||
return {"status": "ERROR", "reason": f"Switch params wrong"}
|
||||
|
||||
lnurldevicepayment = await create_lnurldevicepayment(
|
||||
deviceid=device.id,
|
||||
payload=amount,
|
||||
sats=price_msat,
|
||||
pin=gpio,
|
||||
payhash="bla",
|
||||
)
|
||||
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,
|
||||
"maxSendable": price_msat,
|
||||
"metadata": device.lnurlpay_metadata,
|
||||
}
|
||||
if len(p) % 4 > 0:
|
||||
p += "=" * (4 - (len(p) % 4))
|
||||
|
||||
|
|
@ -140,7 +177,7 @@ async def lnurl_v1_params(
|
|||
"callback": request.url_for(
|
||||
"lnurldevice.lnurl_callback", paymentid=lnurldevicepayment.id
|
||||
),
|
||||
"k1": lnurldevicepayment.id,
|
||||
"k1": p,
|
||||
"minWithdrawable": price_msat * 1000,
|
||||
"maxWithdrawable": price_msat * 1000,
|
||||
"defaultDescription": device.title,
|
||||
|
|
@ -163,7 +200,7 @@ async def lnurl_v1_params(
|
|||
),
|
||||
"minSendable": price_msat * 1000,
|
||||
"maxSendable": price_msat * 1000,
|
||||
"metadata": await device.lnurlpay_metadata(),
|
||||
"metadata": device.lnurlpay_metadata,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -184,28 +221,53 @@ async def lnurl_callback(
|
|||
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"}
|
||||
if device.device == "atm":
|
||||
if not pr:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN, detail="No payment request"
|
||||
)
|
||||
else:
|
||||
if lnurldevicepayment.payload != 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(
|
||||
await pay_invoice(
|
||||
wallet_id=device.wallet,
|
||||
payment_request=pr,
|
||||
max_sat=lnurldevicepayment.sats / 1000,
|
||||
extra={"tag": "withdraw"},
|
||||
)
|
||||
return {"status": "OK"}
|
||||
if device.device == "switch":
|
||||
payment_hash, payment_request = await create_invoice(
|
||||
wallet_id=device.wallet,
|
||||
payment_request=pr,
|
||||
max_sat=lnurldevicepayment.sats / 1000,
|
||||
extra={"tag": "withdraw"},
|
||||
amount=int(lnurldevicepayment.sats / 1000),
|
||||
memo=device.id + " PIN " + str(lnurldevicepayment.pin),
|
||||
unhashed_description=device.lnurlpay_metadata.encode("utf-8"),
|
||||
extra={
|
||||
"tag": "Switch",
|
||||
"pin": str(lnurldevicepayment.pin),
|
||||
"amount": str(lnurldevicepayment.payload),
|
||||
"id": paymentid,
|
||||
},
|
||||
)
|
||||
return {"status": "OK"}
|
||||
|
||||
lnurldevicepayment = await update_lnurldevicepayment(
|
||||
lnurldevicepayment_id=paymentid, payhash=payment_hash
|
||||
)
|
||||
return {
|
||||
"pr": payment_request,
|
||||
"routes": [],
|
||||
}
|
||||
|
||||
payment_hash, payment_request = await create_invoice(
|
||||
wallet_id=device.wallet,
|
||||
amount=lnurldevicepayment.sats / 1000,
|
||||
amount=int(lnurldevicepayment.sats / 1000),
|
||||
memo=device.title,
|
||||
unhashed_description=(await device.lnurlpay_metadata()).encode("utf-8"),
|
||||
unhashed_description=device.lnurlpay_metadata.encode("utf-8"),
|
||||
extra={"tag": "PoS"},
|
||||
)
|
||||
lnurldevicepayment = await update_lnurldevicepayment(
|
||||
|
|
@ -221,5 +283,3 @@ async def lnurl_callback(
|
|||
},
|
||||
"routes": [],
|
||||
}
|
||||
|
||||
return resp.dict()
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ async def m001_initial(db):
|
|||
payhash TEXT,
|
||||
payload TEXT NOT NULL,
|
||||
pin INT,
|
||||
sats INT,
|
||||
sats {db.big_int},
|
||||
timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
||||
);
|
||||
"""
|
||||
|
|
@ -79,3 +79,61 @@ async def m002_redux(db):
|
|||
)
|
||||
except:
|
||||
return
|
||||
|
||||
|
||||
async def m003_redux(db):
|
||||
"""
|
||||
Add 'meta' for storing various metadata about the wallet
|
||||
"""
|
||||
await db.execute(
|
||||
"ALTER TABLE lnurldevice.lnurldevices ADD COLUMN amount INT DEFAULT 0;"
|
||||
)
|
||||
|
||||
|
||||
async def m004_redux(db):
|
||||
"""
|
||||
Add 'meta' for storing various metadata about the wallet
|
||||
"""
|
||||
await db.execute(
|
||||
"ALTER TABLE lnurldevice.lnurldevices ADD COLUMN pin INT DEFAULT 0"
|
||||
)
|
||||
|
||||
await db.execute(
|
||||
"ALTER TABLE lnurldevice.lnurldevices ADD COLUMN profit1 FLOAT DEFAULT 0"
|
||||
)
|
||||
await db.execute(
|
||||
"ALTER TABLE lnurldevice.lnurldevices ADD COLUMN amount1 INT DEFAULT 0"
|
||||
)
|
||||
await db.execute(
|
||||
"ALTER TABLE lnurldevice.lnurldevices ADD COLUMN pin1 INT DEFAULT 0"
|
||||
)
|
||||
|
||||
await db.execute(
|
||||
"ALTER TABLE lnurldevice.lnurldevices ADD COLUMN profit2 FLOAT DEFAULT 0"
|
||||
)
|
||||
await db.execute(
|
||||
"ALTER TABLE lnurldevice.lnurldevices ADD COLUMN amount2 INT DEFAULT 0"
|
||||
)
|
||||
await db.execute(
|
||||
"ALTER TABLE lnurldevice.lnurldevices ADD COLUMN pin2 INT DEFAULT 0"
|
||||
)
|
||||
|
||||
await db.execute(
|
||||
"ALTER TABLE lnurldevice.lnurldevices ADD COLUMN profit3 FLOAT DEFAULT 0"
|
||||
)
|
||||
await db.execute(
|
||||
"ALTER TABLE lnurldevice.lnurldevices ADD COLUMN amount3 INT DEFAULT 0"
|
||||
)
|
||||
await db.execute(
|
||||
"ALTER TABLE lnurldevice.lnurldevices ADD COLUMN pin3 INT DEFAULT 0"
|
||||
)
|
||||
|
||||
await db.execute(
|
||||
"ALTER TABLE lnurldevice.lnurldevices ADD COLUMN profit4 FLOAT DEFAULT 0"
|
||||
)
|
||||
await db.execute(
|
||||
"ALTER TABLE lnurldevice.lnurldevices ADD COLUMN amount4 INT DEFAULT 0"
|
||||
)
|
||||
await db.execute(
|
||||
"ALTER TABLE lnurldevice.lnurldevices ADD COLUMN pin4 INT DEFAULT 0"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import json
|
||||
from sqlite3 import Row
|
||||
from typing import Optional
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import Request
|
||||
from lnurl import Lnurl
|
||||
from lnurl import encode as lnurl_encode # type: ignore
|
||||
from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore
|
||||
from lnurl.types import LnurlPayMetadata # type: ignore
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel
|
||||
from pydantic.main import BaseModel
|
||||
|
||||
|
|
@ -17,6 +18,20 @@ class createLnurldevice(BaseModel):
|
|||
currency: str
|
||||
device: str
|
||||
profit: float
|
||||
amount: int
|
||||
pin: int = 0
|
||||
profit1: float = 0
|
||||
amount1: int = 0
|
||||
pin1: int = 0
|
||||
profit2: float = 0
|
||||
amount2: int = 0
|
||||
pin2: int = 0
|
||||
profit3: float = 0
|
||||
amount3: int = 0
|
||||
pin3: int = 0
|
||||
profit4: float = 0
|
||||
amount4: int = 0
|
||||
pin4: int = 0
|
||||
|
||||
|
||||
class lnurldevices(BaseModel):
|
||||
|
|
@ -27,20 +42,123 @@ class lnurldevices(BaseModel):
|
|||
currency: str
|
||||
device: str
|
||||
profit: float
|
||||
amount: int
|
||||
pin: int
|
||||
profit1: float
|
||||
amount1: int
|
||||
pin1: int
|
||||
profit2: float
|
||||
amount2: int
|
||||
pin2: int
|
||||
profit3: float
|
||||
amount3: int
|
||||
pin3: int
|
||||
profit4: float
|
||||
amount4: int
|
||||
pin4: int
|
||||
timestamp: str
|
||||
|
||||
def from_row(cls, row: Row) -> "lnurldevices":
|
||||
return cls(**dict(row))
|
||||
|
||||
def lnurl(self, req: Request) -> Lnurl:
|
||||
url = req.url_for(
|
||||
"lnurldevice.lnurl_response", device_id=self.id, _external=True
|
||||
)
|
||||
return lnurl_encode(url)
|
||||
|
||||
async def lnurlpay_metadata(self) -> LnurlPayMetadata:
|
||||
@property
|
||||
def lnurlpay_metadata(self) -> LnurlPayMetadata:
|
||||
return LnurlPayMetadata(json.dumps([["text/plain", self.title]]))
|
||||
|
||||
def switches(self, req: Request) -> List:
|
||||
switches = []
|
||||
if self.profit > 0:
|
||||
url = req.url_for("lnurldevice.lnurl_v1_params", device_id=self.id)
|
||||
switches.append(
|
||||
[
|
||||
str(self.pin),
|
||||
str(self.profit),
|
||||
str(self.amount),
|
||||
lnurl_encode(
|
||||
url
|
||||
+ "?gpio="
|
||||
+ str(self.pin)
|
||||
+ "&profit="
|
||||
+ str(self.profit)
|
||||
+ "&amount="
|
||||
+ str(self.amount)
|
||||
),
|
||||
]
|
||||
)
|
||||
if self.profit1 > 0:
|
||||
url = req.url_for("lnurldevice.lnurl_v1_params", device_id=self.id)
|
||||
switches.append(
|
||||
[
|
||||
str(self.pin1),
|
||||
str(self.profit1),
|
||||
str(self.amount1),
|
||||
lnurl_encode(
|
||||
url
|
||||
+ "?gpio="
|
||||
+ str(self.pin1)
|
||||
+ "&profit="
|
||||
+ str(self.profit1)
|
||||
+ "&amount="
|
||||
+ str(self.amount1)
|
||||
),
|
||||
]
|
||||
)
|
||||
if self.profit2 > 0:
|
||||
url = req.url_for("lnurldevice.lnurl_v1_params", device_id=self.id)
|
||||
switches.append(
|
||||
[
|
||||
str(self.pin2),
|
||||
str(self.profit2),
|
||||
str(self.amount2),
|
||||
lnurl_encode(
|
||||
url
|
||||
+ "?gpio="
|
||||
+ str(self.pin2)
|
||||
+ "&profit="
|
||||
+ str(self.profit2)
|
||||
+ "&amount="
|
||||
+ str(self.amount2)
|
||||
),
|
||||
]
|
||||
)
|
||||
if self.profit3 > 0:
|
||||
url = req.url_for("lnurldevice.lnurl_v1_params", device_id=self.id)
|
||||
switches.append(
|
||||
[
|
||||
str(self.pin3),
|
||||
str(self.profit3),
|
||||
str(self.amount3),
|
||||
lnurl_encode(
|
||||
url
|
||||
+ "?gpio="
|
||||
+ str(self.pin3)
|
||||
+ "&profit="
|
||||
+ str(self.profit3)
|
||||
+ "&amount="
|
||||
+ str(self.amount3)
|
||||
),
|
||||
]
|
||||
)
|
||||
if self.profit4 > 0:
|
||||
url = req.url_for("lnurldevice.lnurl_v1_params", device_id=self.id)
|
||||
switches.append(
|
||||
[
|
||||
str(self.pin4),
|
||||
str(self.profit4),
|
||||
str(self.amount4),
|
||||
lnurl_encode(
|
||||
url
|
||||
+ "?gpio="
|
||||
+ str(self.pin4)
|
||||
+ "&profit="
|
||||
+ str(self.profit4)
|
||||
+ "&amount="
|
||||
+ str(self.amount4)
|
||||
),
|
||||
]
|
||||
)
|
||||
return switches
|
||||
|
||||
|
||||
class lnurldevicepayment(BaseModel):
|
||||
id: str
|
||||
|
|
|
|||
44
lnbits/extensions/lnurldevice/tasks.py
Normal file
44
lnbits/extensions/lnurldevice/tasks.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import asyncio
|
||||
import json
|
||||
from http import HTTPStatus
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import httpx
|
||||
from fastapi import HTTPException
|
||||
|
||||
from lnbits import bolt11
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.core.services import pay_invoice
|
||||
from lnbits.helpers import get_current_extension_name
|
||||
from lnbits.tasks import register_invoice_listener
|
||||
|
||||
from .crud import get_lnurldevice, get_lnurldevicepayment, update_lnurldevicepayment
|
||||
from .views import updater
|
||||
|
||||
|
||||
async def wait_for_paid_invoices():
|
||||
invoice_queue = asyncio.Queue()
|
||||
register_invoice_listener(invoice_queue, get_current_extension_name())
|
||||
|
||||
while True:
|
||||
payment = await invoice_queue.get()
|
||||
await on_invoice_paid(payment)
|
||||
|
||||
|
||||
async def on_invoice_paid(payment: Payment) -> None:
|
||||
# (avoid loops)
|
||||
if "Switch" == payment.extra.get("tag"):
|
||||
lnurldevicepayment = await get_lnurldevicepayment(payment.extra.get("id"))
|
||||
if not lnurldevicepayment:
|
||||
return
|
||||
if lnurldevicepayment.payhash == "used":
|
||||
return
|
||||
lnurldevicepayment = await update_lnurldevicepayment(
|
||||
lnurldevicepayment_id=payment.extra.get("id"), payhash="used"
|
||||
)
|
||||
return await updater(
|
||||
lnurldevicepayment.deviceid,
|
||||
lnurldevicepayment.pin,
|
||||
lnurldevicepayment.payload,
|
||||
)
|
||||
return
|
||||
|
|
@ -1,13 +1,24 @@
|
|||
<q-card>
|
||||
<q-card-section>
|
||||
<p>
|
||||
Register LNURLDevice devices to receive payments in your LNbits wallet.<br />
|
||||
Build your own here
|
||||
<a href="https://github.com/arcbtc/bitcoinpos"
|
||||
>https://github.com/arcbtc/bitcoinpos</a
|
||||
For LNURL based Points of Sale, ATMs, and relay devices<br />
|
||||
Use with: <br />
|
||||
LNPoS
|
||||
<a href="https://lnbits.github.io/lnpos">
|
||||
https://lnbits.github.io/lnpos</a
|
||||
><br />
|
||||
bitcoinSwitch
|
||||
<a href="https://github.com/lnbits/bitcoinSwitch">
|
||||
https://github.com/lnbits/bitcoinSwitch</a
|
||||
><br />
|
||||
FOSSA
|
||||
<a href="https://github.com/lnbits/fossa">
|
||||
https://github.com/lnbits/fossa</a
|
||||
><br />
|
||||
<small>
|
||||
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
|
||||
Created by, <a href="https://github.com/benarc">Ben Arc</a>,
|
||||
<a href="https://github.com/blackcoffeexbt">BC</a>,
|
||||
<a href="https://github.com/motorina0">Vlad Stan</a></small
|
||||
>
|
||||
</p>
|
||||
</q-card-section>
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@
|
|||
<q-tr :props="props">
|
||||
<q-th style="width: 5%"></q-th>
|
||||
<q-th style="width: 5%"></q-th>
|
||||
<q-th style="width: 5%"></q-th>
|
||||
|
||||
<q-th
|
||||
v-for="col in props.cols"
|
||||
|
|
@ -91,6 +92,22 @@
|
|||
<q-tooltip> LNURLDevice Settings </q-tooltip>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
<q-td>
|
||||
<q-btn
|
||||
v-if="props.row.device == 'switch'"
|
||||
:disable="protocol == 'http:'"
|
||||
flat
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="visibility"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
@click="openQrCodeDialog(props.row.id)"
|
||||
><q-tooltip v-if="protocol == 'http:'">
|
||||
LNURLs only work over HTTPS </q-tooltip
|
||||
><q-tooltip v-else> view LNURLS </q-tooltip></q-btn
|
||||
>
|
||||
</q-td>
|
||||
<q-td
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
|
|
@ -132,20 +149,33 @@
|
|||
class="q-pa-lg q-pt-xl lnbits__dialog-card"
|
||||
>
|
||||
<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 + ',' +
|
||||
<center>
|
||||
<q-btn
|
||||
v-if="settingsDialog.data.device == 'switch'"
|
||||
dense
|
||||
outline
|
||||
unelevated
|
||||
color="primary"
|
||||
size="md"
|
||||
@click="copyText(wslocation + '/lnurldevice/ws/' + settingsDialog.data.id, 'Link copied to clipboard!')"
|
||||
>{% raw %}{{wslocation}}/lnurldevice/ws/{{settingsDialog.data.id}}{%
|
||||
endraw %}<q-tooltip> Click to copy URL </q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
v-else
|
||||
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>
|
||||
|
||||
>{% 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>
|
||||
</center>
|
||||
<div class="text-subtitle2">
|
||||
<small> </small>
|
||||
</div>
|
||||
|
|
@ -191,6 +221,7 @@
|
|||
label="Type of device"
|
||||
></q-option-group>
|
||||
<q-input
|
||||
v-if="formDialoglnurldevice.data.device != 'switch'"
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialoglnurldevice.data.profit"
|
||||
|
|
@ -198,7 +229,222 @@
|
|||
max="90"
|
||||
label="Profit margin (% added to invoices/deducted from faucets)"
|
||||
></q-input>
|
||||
<div v-else>
|
||||
<q-btn
|
||||
unelevated
|
||||
class="q-mb-lg"
|
||||
round
|
||||
size="sm"
|
||||
icon="add"
|
||||
@click="addSwitch"
|
||||
v-model="switches"
|
||||
color="primary"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
unelevated
|
||||
class="q-mb-lg"
|
||||
round
|
||||
size="sm"
|
||||
icon="remove"
|
||||
@click="removeSwitch"
|
||||
v-model="switches"
|
||||
color="primary"
|
||||
></q-btn>
|
||||
|
||||
<div v-if="switches >= 0">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<q-input
|
||||
ref="setAmount"
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialoglnurldevice.data.profit"
|
||||
class="q-pb-md"
|
||||
:label="'Amount (' + formDialoglnurldevice.data.currency + ') *'"
|
||||
:mask="'#.##'"
|
||||
fill-mask="0"
|
||||
reverse-fill-mask
|
||||
:step="'0.01'"
|
||||
value="0.00"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col q-ml-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialoglnurldevice.data.amount"
|
||||
type="number"
|
||||
value="1000"
|
||||
label="milesecs to turn Switch on for (1sec = 1000ms)"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col q-ml-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialoglnurldevice.data.pin"
|
||||
type="number"
|
||||
label="GPIO to turn on"
|
||||
></q-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="switches >= 1">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<q-input
|
||||
ref="setAmount"
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialoglnurldevice.data.profit1"
|
||||
class="q-pb-md"
|
||||
:label="'Amount (' + formDialoglnurldevice.data.currency + ') *'"
|
||||
:mask="'#.##'"
|
||||
fill-mask="0"
|
||||
reverse-fill-mask
|
||||
:step="'0.01'"
|
||||
value="0.00"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col q-ml-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialoglnurldevice.data.amount1"
|
||||
type="number"
|
||||
value="1000"
|
||||
label="milesecs to turn Switch on for (1sec = 1000ms)"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col q-ml-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialoglnurldevice.data.pin1"
|
||||
type="number"
|
||||
label="GPIO to turn on"
|
||||
></q-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="switches >= 2">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<q-input
|
||||
ref="setAmount"
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialoglnurldevice.data.profit2"
|
||||
class="q-pb-md"
|
||||
:label="'Amount (' + formDialoglnurldevice.data.currency + ') *'"
|
||||
:mask="'#.##'"
|
||||
fill-mask="0"
|
||||
reverse-fill-mask
|
||||
:step="'0.01'"
|
||||
value="0.00"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col q-ml-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialoglnurldevice.data.amount2"
|
||||
type="number"
|
||||
value="1000"
|
||||
label="milesecs to turn Switch on for (1sec = 1000ms)"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col q-ml-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialoglnurldevice.data.pin2"
|
||||
type="number"
|
||||
label="GPIO to turn on"
|
||||
></q-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="switches >= 3">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<q-input
|
||||
ref="setAmount"
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialoglnurldevice.data.profit3"
|
||||
class="q-pb-md"
|
||||
:label="'Amount (' + formDialoglnurldevice.data.currency + ') *'"
|
||||
:mask="'#.##'"
|
||||
fill-mask="0"
|
||||
reverse-fill-mask
|
||||
:step="'0.01'"
|
||||
value="0.00"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col q-ml-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialoglnurldevice.data.amount3"
|
||||
type="number"
|
||||
value="1000"
|
||||
label="milesecs to turn Switch on for (1sec = 1000ms)"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col q-ml-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialoglnurldevice.data.pin3"
|
||||
type="number"
|
||||
label="GPIO to turn on"
|
||||
></q-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="switches >= 4">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<q-input
|
||||
ref="setAmount"
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialoglnurldevice.data.profit4"
|
||||
class="q-pb-md"
|
||||
:label="'Amount (' + formDialoglnurldevice.data.currency + ') *'"
|
||||
:mask="'#.##'"
|
||||
fill-mask="0"
|
||||
reverse-fill-mask
|
||||
:step="'0.01'"
|
||||
value="0.00"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col q-ml-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialoglnurldevice.data.amount4"
|
||||
type="number"
|
||||
value="1000"
|
||||
label="milesecs to turn Switch on for (1sec = 1000ms)"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col q-ml-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialoglnurldevice.data.pin4"
|
||||
type="number"
|
||||
label="GPIO to turn on"
|
||||
></q-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
v-if="formDialoglnurldevice.data.id"
|
||||
|
|
@ -225,6 +471,35 @@
|
|||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="qrCodeDialog.show" position="top">
|
||||
<q-card v-if="qrCodeDialog.data" class="q-pa-lg lnbits__dialog-card">
|
||||
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
||||
<qrcode
|
||||
:value="lnurlValue"
|
||||
:options="{width: 800}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
</q-responsive>
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
@click="copyText(lnurlValue, 'LNURL copied to clipboard!')"
|
||||
>Copy LNURL</q-btn
|
||||
>
|
||||
<br />
|
||||
<div class="row q-mt-lg q-gutter-sm">
|
||||
<q-btn
|
||||
v-for="switch_ in qrCodeDialog.data.switches"
|
||||
outline
|
||||
color="primary"
|
||||
:label="'Switch PIN:' + switch_[0]"
|
||||
@click="lnurlValueFetch(switch_[3])"
|
||||
></q-btn>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
|
||||
|
|
@ -252,9 +527,14 @@
|
|||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
tab: 'mails',
|
||||
protocol: window.location.protocol,
|
||||
location: window.location.hostname,
|
||||
wslocation: window.location.hostname,
|
||||
filter: '',
|
||||
currency: 'USD',
|
||||
lnurlValue: '',
|
||||
switches: 0,
|
||||
lnurldeviceLinks: [],
|
||||
lnurldeviceLinksObj: [],
|
||||
devices: [
|
||||
|
|
@ -265,6 +545,10 @@
|
|||
{
|
||||
label: 'ATM',
|
||||
value: 'atm'
|
||||
},
|
||||
{
|
||||
label: 'Switch',
|
||||
value: 'switch'
|
||||
}
|
||||
],
|
||||
lnurldevicesTable: {
|
||||
|
|
@ -299,12 +583,6 @@
|
|||
label: 'device',
|
||||
field: 'device'
|
||||
},
|
||||
{
|
||||
name: 'profit',
|
||||
align: 'left',
|
||||
label: 'profit',
|
||||
field: 'profit'
|
||||
},
|
||||
{
|
||||
name: 'currency',
|
||||
align: 'left',
|
||||
|
|
@ -333,7 +611,8 @@
|
|||
show_ack: false,
|
||||
show_price: 'None',
|
||||
device: 'pos',
|
||||
profit: 2,
|
||||
profit: 0,
|
||||
amount: 1,
|
||||
title: ''
|
||||
}
|
||||
},
|
||||
|
|
@ -344,6 +623,28 @@
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
openQrCodeDialog: function (lnurldevice_id) {
|
||||
var lnurldevice = _.findWhere(this.lnurldeviceLinks, {
|
||||
id: lnurldevice_id
|
||||
})
|
||||
console.log(lnurldevice)
|
||||
this.qrCodeDialog.data = _.clone(lnurldevice)
|
||||
this.qrCodeDialog.data.url =
|
||||
window.location.protocol + '//' + window.location.host
|
||||
this.lnurlValueFetch(this.qrCodeDialog.data.switches[0][3])
|
||||
this.qrCodeDialog.show = true
|
||||
},
|
||||
lnurlValueFetch: function (lnurl) {
|
||||
this.lnurlValue = lnurl
|
||||
},
|
||||
addSwitch: function () {
|
||||
var self = this
|
||||
self.switches = self.switches + 1
|
||||
},
|
||||
removeSwitch: function () {
|
||||
var self = this
|
||||
self.switches = self.switches - 1
|
||||
},
|
||||
cancellnurldevice: function (data) {
|
||||
var self = this
|
||||
self.formDialoglnurldevice.show = false
|
||||
|
|
@ -400,6 +701,9 @@
|
|||
.then(function (response) {
|
||||
if (response.data) {
|
||||
self.lnurldeviceLinks = response.data.map(maplnurldevice)
|
||||
console.log('response.data')
|
||||
console.log(response.data)
|
||||
console.log('response.data')
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
|
|
@ -519,6 +823,7 @@
|
|||
'//',
|
||||
window.location.host
|
||||
].join('')
|
||||
self.wslocation = ['ws://', window.location.host].join('')
|
||||
LNbits.api
|
||||
.request('GET', '/api/v1/currencies')
|
||||
.then(response => {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
from http import HTTPStatus
|
||||
from io import BytesIO
|
||||
|
||||
from fastapi import Request
|
||||
import pyqrcode
|
||||
from fastapi import Request, WebSocket, WebSocketDisconnect
|
||||
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 starlette.responses import HTMLResponse, StreamingResponse
|
||||
|
||||
from lnbits.core.crud import update_payment_status
|
||||
from lnbits.core.models import User
|
||||
|
|
@ -51,3 +53,60 @@ async def displaypin(request: Request, paymentid: str = Query(None)):
|
|||
"lnurldevice/error.html",
|
||||
{"request": request, "pin": "filler", "not_paid": True},
|
||||
)
|
||||
|
||||
|
||||
@lnurldevice_ext.get("/img/{lnurldevice_id}", response_class=StreamingResponse)
|
||||
async def img(request: Request, lnurldevice_id):
|
||||
lnurldevice = await get_lnurldevice(lnurldevice_id)
|
||||
if not lnurldevice:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="LNURLDevice does not exist."
|
||||
)
|
||||
return lnurldevice.lnurl(request)
|
||||
|
||||
|
||||
##################WEBSOCKET ROUTES########################
|
||||
|
||||
|
||||
class ConnectionManager:
|
||||
def __init__(self):
|
||||
self.active_connections: List[WebSocket] = []
|
||||
|
||||
async def connect(self, websocket: WebSocket, lnurldevice_id: str):
|
||||
await websocket.accept()
|
||||
websocket.id = lnurldevice_id
|
||||
self.active_connections.append(websocket)
|
||||
|
||||
def disconnect(self, websocket: WebSocket):
|
||||
self.active_connections.remove(websocket)
|
||||
|
||||
async def send_personal_message(self, message: str, lnurldevice_id: str):
|
||||
for connection in self.active_connections:
|
||||
if connection.id == lnurldevice_id:
|
||||
await connection.send_text(message)
|
||||
|
||||
async def broadcast(self, message: str):
|
||||
for connection in self.active_connections:
|
||||
await connection.send_text(message)
|
||||
|
||||
|
||||
manager = ConnectionManager()
|
||||
|
||||
|
||||
@lnurldevice_ext.websocket("/ws/{lnurldevice_id}", name="lnurldevice.lnurldevice_by_id")
|
||||
async def websocket_endpoint(websocket: WebSocket, lnurldevice_id: str):
|
||||
await manager.connect(websocket, lnurldevice_id)
|
||||
try:
|
||||
while True:
|
||||
data = await websocket.receive_text()
|
||||
except WebSocketDisconnect:
|
||||
manager.disconnect(websocket)
|
||||
|
||||
|
||||
async def updater(lnurldevice_id, lnurldevice_pin, lnurldevice_amount):
|
||||
lnurldevice = await get_lnurldevice(lnurldevice_id)
|
||||
if not lnurldevice:
|
||||
return
|
||||
return await manager.send_personal_message(
|
||||
f"{lnurldevice_pin}-{lnurldevice_amount}", lnurldevice_id
|
||||
)
|
||||
|
|
|
|||
|
|
@ -32,32 +32,42 @@ async def api_list_currencies_available():
|
|||
@lnurldevice_ext.post("/api/v1/lnurlpos")
|
||||
@lnurldevice_ext.put("/api/v1/lnurlpos/{lnurldevice_id}")
|
||||
async def api_lnurldevice_create_or_update(
|
||||
req: Request,
|
||||
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()
|
||||
return {**lnurldevice.dict(), **{"switches": lnurldevice.switches(req)}}
|
||||
else:
|
||||
lnurldevice = await update_lnurldevice(data, lnurldevice_id=lnurldevice_id)
|
||||
return lnurldevice.dict()
|
||||
return {**lnurldevice.dict(), **{"switches": lnurldevice.switches(req)}}
|
||||
|
||||
|
||||
@lnurldevice_ext.get("/api/v1/lnurlpos")
|
||||
async def api_lnurldevices_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
async def api_lnurldevices_retrieve(
|
||||
req: Request, 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)
|
||||
{**lnurldevice.dict(), **{"switches": lnurldevice.switches(req)}}
|
||||
for lnurldevice in await get_lnurldevices(wallet_ids)
|
||||
]
|
||||
except:
|
||||
return ""
|
||||
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,
|
||||
req: Request,
|
||||
wallet: WalletTypeInfo = Depends(get_key_type),
|
||||
lnurldevice_id: str = Query(None),
|
||||
):
|
||||
|
|
@ -68,7 +78,7 @@ async def api_lnurldevice_retrieve(
|
|||
)
|
||||
if not lnurldevice.lnurl_toggle:
|
||||
return {**lnurldevice.dict()}
|
||||
return {**lnurldevice.dict(), **{"lnurl": lnurldevice.lnurl(request=request)}}
|
||||
return {**lnurldevice.dict(), **{"switches": lnurldevice.switches(req)}}
|
||||
|
||||
|
||||
@lnurldevice_ext.delete("/api/v1/lnurlpos/{lnurldevice_id}")
|
||||
|
|
|
|||
|
|
@ -21,13 +21,15 @@ async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink:
|
|||
served_meta,
|
||||
served_pr,
|
||||
webhook_url,
|
||||
webhook_headers,
|
||||
webhook_body,
|
||||
success_text,
|
||||
success_url,
|
||||
comment_chars,
|
||||
currency,
|
||||
fiat_base_multiplier
|
||||
)
|
||||
VALUES (?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
{returning}
|
||||
""",
|
||||
(
|
||||
|
|
@ -36,6 +38,8 @@ async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink:
|
|||
data.min,
|
||||
data.max,
|
||||
data.webhook_url,
|
||||
data.webhook_headers,
|
||||
data.webhook_body,
|
||||
data.success_text,
|
||||
data.success_url,
|
||||
data.comment_chars,
|
||||
|
|
|
|||
|
|
@ -60,3 +60,11 @@ async def m004_fiat_base_multiplier(db):
|
|||
await db.execute(
|
||||
"ALTER TABLE lnurlp.pay_links ADD COLUMN fiat_base_multiplier INTEGER DEFAULT 1;"
|
||||
)
|
||||
|
||||
|
||||
async def m005_webhook_headers_and_body(db):
|
||||
"""
|
||||
Add headers and body to webhooks
|
||||
"""
|
||||
await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN webhook_headers TEXT;")
|
||||
await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN webhook_body TEXT;")
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ class CreatePayLinkData(BaseModel):
|
|||
currency: str = Query(None)
|
||||
comment_chars: int = Query(0, ge=0, lt=800)
|
||||
webhook_url: str = Query(None)
|
||||
webhook_headers: str = Query(None)
|
||||
webhook_body: str = Query(None)
|
||||
success_text: str = Query(None)
|
||||
success_url: str = Query(None)
|
||||
fiat_base_multiplier: int = Query(100, ge=1)
|
||||
|
|
@ -31,6 +33,8 @@ class PayLink(BaseModel):
|
|||
served_meta: int
|
||||
served_pr: int
|
||||
webhook_url: Optional[str]
|
||||
webhook_headers: Optional[str]
|
||||
webhook_body: Optional[str]
|
||||
success_text: Optional[str]
|
||||
success_url: Optional[str]
|
||||
currency: Optional[str]
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import httpx
|
|||
|
||||
from lnbits.core import db as core_db
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.helpers import get_current_extension_name
|
||||
from lnbits.tasks import register_invoice_listener
|
||||
|
||||
from .crud import get_pay_link
|
||||
|
|
@ -12,7 +13,7 @@ from .crud import get_pay_link
|
|||
|
||||
async def wait_for_paid_invoices():
|
||||
invoice_queue = asyncio.Queue()
|
||||
register_invoice_listener(invoice_queue)
|
||||
register_invoice_listener(invoice_queue, get_current_extension_name())
|
||||
|
||||
while True:
|
||||
payment = await invoice_queue.get()
|
||||
|
|
@ -32,17 +33,22 @@ async def on_invoice_paid(payment: Payment) -> None:
|
|||
if pay_link and pay_link.webhook_url:
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
r = await client.post(
|
||||
pay_link.webhook_url,
|
||||
json={
|
||||
kwargs = {
|
||||
"json": {
|
||||
"payment_hash": payment.payment_hash,
|
||||
"payment_request": payment.bolt11,
|
||||
"amount": payment.amount,
|
||||
"comment": payment.extra.get("comment"),
|
||||
"lnurlp": pay_link.id,
|
||||
},
|
||||
timeout=40,
|
||||
)
|
||||
"timeout": 40,
|
||||
}
|
||||
if pay_link.webhook_body:
|
||||
kwargs["json"]["body"] = json.loads(pay_link.webhook_body)
|
||||
if pay_link.webhook_headers:
|
||||
kwargs["headers"] = json.loads(pay_link.webhook_headers)
|
||||
|
||||
r = await client.post(pay_link.webhook_url, **kwargs)
|
||||
await mark_webhook_sent(payment, r.status_code)
|
||||
except (httpx.ConnectError, httpx.RequestError):
|
||||
await mark_webhook_sent(payment, -1)
|
||||
|
|
|
|||
|
|
@ -213,6 +213,24 @@
|
|||
label="Webhook URL (optional)"
|
||||
hint="A URL to be called whenever this link receives a payment."
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-if="formDialog.data.webhook_url"
|
||||
v-model="formDialog.data.webhook_headers"
|
||||
type="text"
|
||||
label="Webhook headers (optional)"
|
||||
hint="Custom data as JSON string, send headers along with the webhook."
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-if="formDialog.data.webhook_url"
|
||||
v-model="formDialog.data.webhook_body"
|
||||
type="text"
|
||||
label="Webhook custom data (optional)"
|
||||
hint="Custom data as JSON string, will get posted along with webhook 'body' field."
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import json
|
||||
from http import HTTPStatus
|
||||
|
||||
from fastapi import Request
|
||||
|
|
@ -90,6 +91,24 @@ async def api_link_create_or_update(
|
|||
detail="Must use full satoshis.", status_code=HTTPStatus.BAD_REQUEST
|
||||
)
|
||||
|
||||
if data.webhook_headers:
|
||||
try:
|
||||
json.loads(data.webhook_headers)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
detail="Invalid JSON in webhook_headers.",
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
)
|
||||
|
||||
if data.webhook_body:
|
||||
try:
|
||||
json.loads(data.webhook_body)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
detail="Invalid JSON in webhook_body.",
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
)
|
||||
|
||||
# database only allows int4 entries for min and max. For fiat currencies,
|
||||
# we multiply by data.fiat_base_multiplier (usually 100) to save the value in cents.
|
||||
if data.currency and data.fiat_base_multiplier:
|
||||
|
|
|
|||
|
|
@ -3,14 +3,14 @@ async def m001_initial(db):
|
|||
Initial lnurlpayouts table.
|
||||
"""
|
||||
await db.execute(
|
||||
"""
|
||||
f"""
|
||||
CREATE TABLE lnurlpayout.lnurlpayouts (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
wallet TEXT NOT NULL,
|
||||
admin_key TEXT NOT NULL,
|
||||
lnurlpay TEXT NOT NULL,
|
||||
threshold INT NOT NULL
|
||||
threshold {db.big_int} NOT NULL
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from lnbits.core.crud import get_wallet
|
|||
from lnbits.core.models import Payment
|
||||
from lnbits.core.services import pay_invoice
|
||||
from lnbits.core.views.api import api_payments_decode
|
||||
from lnbits.helpers import get_current_extension_name
|
||||
from lnbits.tasks import register_invoice_listener
|
||||
|
||||
from .crud import get_lnurlpayout_from_wallet
|
||||
|
|
@ -17,7 +18,7 @@ from .crud import get_lnurlpayout_from_wallet
|
|||
|
||||
async def wait_for_paid_invoices():
|
||||
invoice_queue = asyncio.Queue()
|
||||
register_invoice_listener(invoice_queue)
|
||||
register_invoice_listener(invoice_queue, get_current_extension_name())
|
||||
|
||||
while True:
|
||||
payment = await invoice_queue.get()
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ async def api_lnurlpayout_delete(
|
|||
)
|
||||
|
||||
await delete_lnurlpayout(lnurlpayout_id)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
@lnurlpayout_ext.get("/api/v1/lnurlpayouts/{lnurlpayout_id}", status_code=HTTPStatus.OK)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
# type: ignore
|
||||
from os import getenv
|
||||
|
||||
from fastapi import Request
|
||||
|
|
@ -34,7 +35,9 @@ ngrok_tunnel = ngrok.connect(port)
|
|||
|
||||
|
||||
@ngrok_ext.get("/")
|
||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||
async def index(
|
||||
request: Request, user: User = Depends(check_user_exists) # type: ignore
|
||||
):
|
||||
return ngrok_renderer().TemplateResponse(
|
||||
"ngrok/index.html", {"request": request, "ngrok": string5, "user": user.dict()}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@
|
|||
|
||||
LNBits Offline Shop allows for merchants to receive Bitcoin payments while offline and without any electronic device.
|
||||
|
||||
Merchant will create items and associate a QR code ([a LNURLp](https://github.com/lnbits/lnbits/blob/master/lnbits/extensions/lnurlp/README.md)) with a price. He can then print the QR codes and display them on their shop. When a costumer chooses an item, scans the QR code, gets the description and price. After payment, the costumer gets a confirmation code that the merchant can validate to be sure the payment was successful.
|
||||
Merchant will create items and associate a QR code ([a LNURLp](https://github.com/lnbits/lnbits/blob/master/lnbits/extensions/lnurlp/README.md)) with a price. He can then print the QR codes and display them on their shop. When a customer chooses an item, scans the QR code, gets the description and price. After payment, the customer gets a confirmation code that the merchant can validate to be sure the payment was successful.
|
||||
|
||||
Costumers must use an LNURL pay capable wallet.
|
||||
Customers must use an LNURL pay capable wallet.
|
||||
|
||||
[**Wallets supporting LNURL**](https://github.com/fiatjaf/awesome-lnurl#wallets)
|
||||
|
||||
|
|
@ -18,18 +18,18 @@ Costumers must use an LNURL pay capable wallet.
|
|||

|
||||
2. Begin by creating an item, click "ADD NEW ITEM"
|
||||
- set the item name and a small description
|
||||
- you can set an optional, preferably square image, that will show up on the costumer wallet - _depending on wallet_
|
||||
- set the item price, if you choose a fiat currency the bitcoin conversion will happen at the time costumer scans to pay\
|
||||
- you can set an optional, preferably square image, that will show up on the customer wallet - _depending on wallet_
|
||||
- set the item price, if you choose a fiat currency the bitcoin conversion will happen at the time customer scans to pay\
|
||||

|
||||
3. After creating some products, click on "PRINT QR CODES"\
|
||||

|
||||
4. You'll see a QR code for each product in your LNBits Offline Shop with a title and price ready for printing\
|
||||

|
||||
5. Place the printed QR codes on your shop, or at the fair stall, or have them as a menu style laminated sheet
|
||||
6. Choose what type of confirmation do you want costumers to report to merchant after a successful payment\
|
||||
6. Choose what type of confirmation do you want customers to report to merchant after a successful payment\
|
||||

|
||||
|
||||
- Wordlist is the default option: after a successful payment the costumer will receive a word from this list, **sequentially**. Starting in _albatross_ as costumers pay for the items they will get the next word in the list until _zebra_, then it starts at the top again. The list can be changed, for example if you think A-Z is a big list to track, you can use _apple_, _banana_, _coconut_\
|
||||
- Wordlist is the default option: after a successful payment the customer will receive a word from this list, **sequentially**. Starting in _albatross_ as customers pay for the items they will get the next word in the list until _zebra_, then it starts at the top again. The list can be changed, for example if you think A-Z is a big list to track, you can use _apple_, _banana_, _coconut_\
|
||||

|
||||
- TOTP (time-based one time password) can be used instead. If you use Google Authenticator just scan the presented QR with the app and after a successful payment the user will get the password that you can check with GA\
|
||||

|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ async def api_add_or_update_item(
|
|||
async def api_delete_item(item_id, wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
shop = await get_or_create_shop_by_wallet(wallet.wallet.id)
|
||||
await delete_item_from_shop(shop.id, item_id)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
class CreateMethodData(BaseModel):
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ async def api_paywall_delete(
|
|||
)
|
||||
|
||||
await delete_paywall(paywall_id)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
@paywall_ext.post("/api/v1/paywalls/invoice/{paywall_id}")
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ from .models import (
|
|||
CreateSatsDiceLink,
|
||||
CreateSatsDicePayment,
|
||||
CreateSatsDiceWithdraw,
|
||||
HashCheck,
|
||||
satsdiceLink,
|
||||
satsdicePayment,
|
||||
satsdiceWithdraw,
|
||||
|
|
@ -76,7 +75,7 @@ async def get_satsdice_pays(wallet_ids: Union[str, List[str]]) -> List[satsdiceL
|
|||
return [satsdiceLink(**row) for row in rows]
|
||||
|
||||
|
||||
async def update_satsdice_pay(link_id: int, **kwargs) -> Optional[satsdiceLink]:
|
||||
async def update_satsdice_pay(link_id: str, **kwargs) -> satsdiceLink:
|
||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||
await db.execute(
|
||||
f"UPDATE satsdice.satsdice_pay SET {q} WHERE id = ?",
|
||||
|
|
@ -85,10 +84,10 @@ async def update_satsdice_pay(link_id: int, **kwargs) -> Optional[satsdiceLink]:
|
|||
row = await db.fetchone(
|
||||
"SELECT * FROM satsdice.satsdice_pay WHERE id = ?", (link_id,)
|
||||
)
|
||||
return satsdiceLink(**row) if row else None
|
||||
return satsdiceLink(**row)
|
||||
|
||||
|
||||
async def increment_satsdice_pay(link_id: int, **kwargs) -> Optional[satsdiceLink]:
|
||||
async def increment_satsdice_pay(link_id: str, **kwargs) -> Optional[satsdiceLink]:
|
||||
q = ", ".join([f"{field[0]} = {field[0]} + ?" for field in kwargs.items()])
|
||||
await db.execute(
|
||||
f"UPDATE satsdice.satsdice_pay SET {q} WHERE id = ?",
|
||||
|
|
@ -100,7 +99,7 @@ async def increment_satsdice_pay(link_id: int, **kwargs) -> Optional[satsdiceLin
|
|||
return satsdiceLink(**row) if row else None
|
||||
|
||||
|
||||
async def delete_satsdice_pay(link_id: int) -> None:
|
||||
async def delete_satsdice_pay(link_id: str) -> None:
|
||||
await db.execute("DELETE FROM satsdice.satsdice_pay WHERE id = ?", (link_id,))
|
||||
|
||||
|
||||
|
|
@ -119,9 +118,15 @@ async def create_satsdice_payment(data: CreateSatsDicePayment) -> satsdicePaymen
|
|||
)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""",
|
||||
(data["payment_hash"], data["satsdice_pay"], data["value"], False, False),
|
||||
(
|
||||
data.payment_hash,
|
||||
data.satsdice_pay,
|
||||
data.value,
|
||||
False,
|
||||
False,
|
||||
),
|
||||
)
|
||||
payment = await get_satsdice_payment(data["payment_hash"])
|
||||
payment = await get_satsdice_payment(data.payment_hash)
|
||||
assert payment, "Newly created withdraw couldn't be retrieved"
|
||||
return payment
|
||||
|
||||
|
|
@ -134,9 +139,7 @@ async def get_satsdice_payment(payment_hash: str) -> Optional[satsdicePayment]:
|
|||
return satsdicePayment(**row) if row else None
|
||||
|
||||
|
||||
async def update_satsdice_payment(
|
||||
payment_hash: int, **kwargs
|
||||
) -> Optional[satsdicePayment]:
|
||||
async def update_satsdice_payment(payment_hash: str, **kwargs) -> satsdicePayment:
|
||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||
|
||||
await db.execute(
|
||||
|
|
@ -147,7 +150,7 @@ async def update_satsdice_payment(
|
|||
"SELECT * FROM satsdice.satsdice_payment WHERE payment_hash = ?",
|
||||
(payment_hash,),
|
||||
)
|
||||
return satsdicePayment(**row) if row else None
|
||||
return satsdicePayment(**row)
|
||||
|
||||
|
||||
##################SATSDICE WITHDRAW LINKS
|
||||
|
|
@ -168,16 +171,16 @@ async def create_satsdice_withdraw(data: CreateSatsDiceWithdraw) -> satsdiceWith
|
|||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
data["payment_hash"],
|
||||
data["satsdice_pay"],
|
||||
data["value"],
|
||||
data.payment_hash,
|
||||
data.satsdice_pay,
|
||||
data.value,
|
||||
urlsafe_short_hash(),
|
||||
urlsafe_short_hash(),
|
||||
int(datetime.now().timestamp()),
|
||||
data["used"],
|
||||
data.used,
|
||||
),
|
||||
)
|
||||
withdraw = await get_satsdice_withdraw(data["payment_hash"], 0)
|
||||
withdraw = await get_satsdice_withdraw(data.payment_hash, 0)
|
||||
assert withdraw, "Newly created withdraw couldn't be retrieved"
|
||||
return withdraw
|
||||
|
||||
|
|
@ -247,7 +250,7 @@ async def delete_satsdice_withdraw(withdraw_id: str) -> None:
|
|||
)
|
||||
|
||||
|
||||
async def create_withdraw_hash_check(the_hash: str, lnurl_id: str) -> HashCheck:
|
||||
async def create_withdraw_hash_check(the_hash: str, lnurl_id: str):
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO satsdice.hash_checkw (
|
||||
|
|
@ -262,19 +265,15 @@ async def create_withdraw_hash_check(the_hash: str, lnurl_id: str) -> HashCheck:
|
|||
return hashCheck
|
||||
|
||||
|
||||
async def get_withdraw_hash_checkw(the_hash: str, lnurl_id: str) -> Optional[HashCheck]:
|
||||
async def get_withdraw_hash_checkw(the_hash: str, lnurl_id: str):
|
||||
rowid = await db.fetchone(
|
||||
"SELECT * FROM satsdice.hash_checkw WHERE id = ?", (the_hash,)
|
||||
)
|
||||
rowlnurl = await db.fetchone(
|
||||
"SELECT * FROM satsdice.hash_checkw WHERE lnurl_id = ?", (lnurl_id,)
|
||||
)
|
||||
if not rowlnurl:
|
||||
if not rowlnurl or not rowid:
|
||||
await create_withdraw_hash_check(the_hash, lnurl_id)
|
||||
return {"lnurl": True, "hash": False}
|
||||
else:
|
||||
if not rowid:
|
||||
await create_withdraw_hash_check(the_hash, lnurl_id)
|
||||
return {"lnurl": True, "hash": False}
|
||||
else:
|
||||
return {"lnurl": True, "hash": True}
|
||||
return {"lnurl": True, "hash": True}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import hashlib
|
||||
import json
|
||||
import math
|
||||
from http import HTTPStatus
|
||||
|
|
@ -83,15 +82,18 @@ async def api_lnurlp_callback(
|
|||
|
||||
success_action = link.success_action(payment_hash=payment_hash, req=req)
|
||||
|
||||
data: CreateSatsDicePayment = {
|
||||
"satsdice_pay": link.id,
|
||||
"value": amount_received / 1000,
|
||||
"payment_hash": payment_hash,
|
||||
}
|
||||
data = CreateSatsDicePayment(
|
||||
satsdice_pay=link.id,
|
||||
value=amount_received / 1000,
|
||||
payment_hash=payment_hash,
|
||||
)
|
||||
|
||||
await create_satsdice_payment(data)
|
||||
payResponse = {"pr": payment_request, "successAction": success_action, "routes": []}
|
||||
|
||||
payResponse: dict = {
|
||||
"pr": payment_request,
|
||||
"successAction": success_action,
|
||||
"routes": [],
|
||||
}
|
||||
return json.dumps(payResponse)
|
||||
|
||||
|
||||
|
|
@ -133,9 +135,7 @@ async def api_lnurlw_response(req: Request, unique_hash: str = Query(None)):
|
|||
name="satsdice.api_lnurlw_callback",
|
||||
)
|
||||
async def api_lnurlw_callback(
|
||||
req: Request,
|
||||
unique_hash: str = Query(None),
|
||||
k1: str = Query(None),
|
||||
pr: str = Query(None),
|
||||
):
|
||||
|
||||
|
|
@ -146,12 +146,13 @@ async def api_lnurlw_callback(
|
|||
return {"status": "ERROR", "reason": "spent"}
|
||||
paylink = await get_satsdice_pay(link.satsdice_pay)
|
||||
|
||||
await update_satsdice_withdraw(link.id, used=1)
|
||||
await pay_invoice(
|
||||
wallet_id=paylink.wallet,
|
||||
payment_request=pr,
|
||||
max_sat=link.value,
|
||||
extra={"tag": "withdraw"},
|
||||
)
|
||||
if paylink:
|
||||
await update_satsdice_withdraw(link.id, used=1)
|
||||
await pay_invoice(
|
||||
wallet_id=paylink.wallet,
|
||||
payment_request=pr,
|
||||
max_sat=link.value,
|
||||
extra={"tag": "withdraw"},
|
||||
)
|
||||
|
||||
return {"status": "OK"}
|
||||
return {"status": "OK"}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from typing import Dict, Optional
|
|||
|
||||
from fastapi import Request
|
||||
from fastapi.param_functions import Query
|
||||
from lnurl import Lnurl, LnurlWithdrawResponse
|
||||
from lnurl import Lnurl
|
||||
from lnurl import encode as lnurl_encode # type: ignore
|
||||
from lnurl.types import LnurlPayMetadata # type: ignore
|
||||
from pydantic import BaseModel
|
||||
|
|
@ -80,8 +80,7 @@ class satsdiceWithdraw(BaseModel):
|
|||
def is_spent(self) -> bool:
|
||||
return self.used >= 1
|
||||
|
||||
@property
|
||||
def lnurl_response(self, req: Request) -> LnurlWithdrawResponse:
|
||||
def lnurl_response(self, req: Request):
|
||||
url = req.url_for("satsdice.api_lnurlw_callback", unique_hash=self.unique_hash)
|
||||
withdrawResponse = {
|
||||
"tag": "withdrawRequest",
|
||||
|
|
@ -99,7 +98,7 @@ class HashCheck(BaseModel):
|
|||
lnurl_id: str
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "Hash":
|
||||
def from_row(cls, row: Row):
|
||||
return cls(**dict(row))
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import random
|
||||
from http import HTTPStatus
|
||||
from io import BytesIO
|
||||
|
||||
import pyqrcode
|
||||
from fastapi import Request
|
||||
from fastapi.param_functions import Query
|
||||
from fastapi.params import Depends
|
||||
|
|
@ -20,13 +22,15 @@ from .crud import (
|
|||
get_satsdice_withdraw,
|
||||
update_satsdice_payment,
|
||||
)
|
||||
from .models import CreateSatsDiceWithdraw, satsdiceLink
|
||||
from .models import CreateSatsDiceWithdraw
|
||||
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
|
||||
@satsdice_ext.get("/", response_class=HTMLResponse)
|
||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||
async def index(
|
||||
request: Request, user: User = Depends(check_user_exists) # type: ignore
|
||||
):
|
||||
return satsdice_renderer().TemplateResponse(
|
||||
"satsdice/index.html", {"request": request, "user": user.dict()}
|
||||
)
|
||||
|
|
@ -67,7 +71,7 @@ async def displaywin(
|
|||
)
|
||||
withdrawLink = await get_satsdice_withdraw(payment_hash)
|
||||
payment = await get_satsdice_payment(payment_hash)
|
||||
if payment.lost:
|
||||
if not payment or payment.lost:
|
||||
return satsdice_renderer().TemplateResponse(
|
||||
"satsdice/error.html",
|
||||
{"request": request, "link": satsdicelink.id, "paid": False, "lost": True},
|
||||
|
|
@ -96,13 +100,18 @@ async def displaywin(
|
|||
)
|
||||
await update_satsdice_payment(payment_hash, paid=1)
|
||||
paylink = await get_satsdice_payment(payment_hash)
|
||||
if not paylink:
|
||||
return satsdice_renderer().TemplateResponse(
|
||||
"satsdice/error.html",
|
||||
{"request": request, "link": satsdicelink.id, "paid": False, "lost": True},
|
||||
)
|
||||
|
||||
data: CreateSatsDiceWithdraw = {
|
||||
"satsdice_pay": satsdicelink.id,
|
||||
"value": paylink.value * satsdicelink.multiplier,
|
||||
"payment_hash": payment_hash,
|
||||
"used": 0,
|
||||
}
|
||||
data = CreateSatsDiceWithdraw(
|
||||
satsdice_pay=satsdicelink.id,
|
||||
value=paylink.value * satsdicelink.multiplier,
|
||||
payment_hash=payment_hash,
|
||||
used=0,
|
||||
)
|
||||
|
||||
withdrawLink = await create_satsdice_withdraw(data)
|
||||
return satsdice_renderer().TemplateResponse(
|
||||
|
|
@ -121,9 +130,12 @@ async def displaywin(
|
|||
|
||||
@satsdice_ext.get("/img/{link_id}", response_class=HTMLResponse)
|
||||
async def img(link_id):
|
||||
link = await get_satsdice_pay(link_id) or abort(
|
||||
HTTPStatus.NOT_FOUND, "satsdice link does not exist."
|
||||
)
|
||||
link = await get_satsdice_pay(link_id)
|
||||
if not link:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="satsdice link does not exist."
|
||||
)
|
||||
|
||||
qr = pyqrcode.create(link.lnurl)
|
||||
stream = BytesIO()
|
||||
qr.svg(stream, scale=3)
|
||||
|
|
|
|||
|
|
@ -15,9 +15,10 @@ from .crud import (
|
|||
delete_satsdice_pay,
|
||||
get_satsdice_pay,
|
||||
get_satsdice_pays,
|
||||
get_withdraw_hash_checkw,
|
||||
update_satsdice_pay,
|
||||
)
|
||||
from .models import CreateSatsDiceLink, CreateSatsDiceWithdraws, satsdiceLink
|
||||
from .models import CreateSatsDiceLink
|
||||
|
||||
################LNURL pay
|
||||
|
||||
|
|
@ -25,13 +26,15 @@ from .models import CreateSatsDiceLink, CreateSatsDiceWithdraws, satsdiceLink
|
|||
@satsdice_ext.get("/api/v1/links")
|
||||
async def api_links(
|
||||
request: Request,
|
||||
wallet: WalletTypeInfo = Depends(get_key_type),
|
||||
wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
|
||||
all_wallets: bool = Query(False),
|
||||
):
|
||||
wallet_ids = [wallet.wallet.id]
|
||||
|
||||
if all_wallets:
|
||||
wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
|
||||
user = await get_user(wallet.wallet.user)
|
||||
if user:
|
||||
wallet_ids = user.wallet_ids
|
||||
|
||||
try:
|
||||
links = await get_satsdice_pays(wallet_ids)
|
||||
|
|
@ -46,7 +49,7 @@ async def api_links(
|
|||
|
||||
@satsdice_ext.get("/api/v1/links/{link_id}")
|
||||
async def api_link_retrieve(
|
||||
link_id: str = Query(None), wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
link_id: str = Query(None), wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||
):
|
||||
link = await get_satsdice_pay(link_id)
|
||||
|
||||
|
|
@ -67,7 +70,7 @@ async def api_link_retrieve(
|
|||
@satsdice_ext.put("/api/v1/links/{link_id}", status_code=HTTPStatus.OK)
|
||||
async def api_link_create_or_update(
|
||||
data: CreateSatsDiceLink,
|
||||
wallet: WalletTypeInfo = Depends(get_key_type),
|
||||
wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
|
||||
link_id: str = Query(None),
|
||||
):
|
||||
if data.min_bet > data.max_bet:
|
||||
|
|
@ -95,10 +98,10 @@ async def api_link_create_or_update(
|
|||
|
||||
@satsdice_ext.delete("/api/v1/links/{link_id}")
|
||||
async def api_link_delete(
|
||||
wallet: WalletTypeInfo = Depends(get_key_type), link_id: str = Query(None)
|
||||
wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
|
||||
link_id: str = Query(None),
|
||||
):
|
||||
link = await get_satsdice_pay(link_id)
|
||||
|
||||
if not link:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Pay link does not exist."
|
||||
|
|
@ -117,11 +120,12 @@ async def api_link_delete(
|
|||
##########LNURL withdraw
|
||||
|
||||
|
||||
@satsdice_ext.get("/api/v1/withdraws/{the_hash}/{lnurl_id}")
|
||||
@satsdice_ext.get(
|
||||
"/api/v1/withdraws/{the_hash}/{lnurl_id}", dependencies=[Depends(get_key_type)]
|
||||
)
|
||||
async def api_withdraw_hash_retrieve(
|
||||
wallet: WalletTypeInfo = Depends(get_key_type),
|
||||
lnurl_id: str = Query(None),
|
||||
the_hash: str = Query(None),
|
||||
):
|
||||
hashCheck = await get_withdraw_hash_check(the_hash, lnurl_id)
|
||||
hashCheck = await get_withdraw_hash_checkw(the_hash, lnurl_id)
|
||||
return hashCheck
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ Easilly create invoices that support Lightning Network and on-chain BTC payment.
|
|||

|
||||
3. The charge will appear on the _Charges_ section\
|
||||

|
||||
4. Your costumer/payee will get the payment page
|
||||
4. Your customer/payee will get the payment page
|
||||
- they can choose to pay on LN\
|
||||

|
||||
- or pay on chain\
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ async def check_address_balance(charge_id: str) -> List[Charges]:
|
|||
charge = await get_charge(charge_id)
|
||||
if not charge.paid:
|
||||
if charge.onchainaddress:
|
||||
config = await get_config(charge.user)
|
||||
config = await get_charge_config(charge_id)
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.get(
|
||||
|
|
@ -122,3 +122,10 @@ async def check_address_balance(charge_id: str) -> List[Charges]:
|
|||
return await update_charge(charge_id=charge_id, balance=charge.amount)
|
||||
row = await db.fetchone("SELECT * FROM satspay.charges WHERE id = ?", (charge_id,))
|
||||
return Charges.from_row(row) if row else None
|
||||
|
||||
|
||||
async def get_charge_config(charge_id: str):
|
||||
row = await db.fetchone(
|
||||
"""SELECT "user" FROM satspay.charges WHERE id = ?""", (charge_id,)
|
||||
)
|
||||
return await get_config(row.user)
|
||||
|
|
|
|||
17
lnbits/extensions/satspay/helpers.py
Normal file
17
lnbits/extensions/satspay/helpers.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
from .models import Charges
|
||||
|
||||
|
||||
def compact_charge(charge: Charges):
|
||||
return {
|
||||
"id": charge.id,
|
||||
"description": charge.description,
|
||||
"onchainaddress": charge.onchainaddress,
|
||||
"payment_request": charge.payment_request,
|
||||
"payment_hash": charge.payment_hash,
|
||||
"time": charge.time,
|
||||
"amount": charge.amount,
|
||||
"balance": charge.balance,
|
||||
"paid": charge.paid,
|
||||
"timestamp": charge.timestamp,
|
||||
"completelink": charge.completelink, # should be secret?
|
||||
}
|
||||
|
|
@ -19,7 +19,6 @@ class CreateCharge(BaseModel):
|
|||
|
||||
class Charges(BaseModel):
|
||||
id: str
|
||||
user: str
|
||||
description: Optional[str]
|
||||
onchainwallet: Optional[str]
|
||||
onchainaddress: Optional[str]
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from loguru import logger
|
|||
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.extensions.satspay.crud import check_address_balance, get_charge
|
||||
from lnbits.helpers import get_current_extension_name
|
||||
from lnbits.tasks import register_invoice_listener
|
||||
|
||||
# from .crud import get_ticket, set_ticket_paid
|
||||
|
|
@ -11,7 +12,7 @@ from lnbits.tasks import register_invoice_listener
|
|||
|
||||
async def wait_for_paid_invoices():
|
||||
invoice_queue = asyncio.Queue()
|
||||
register_invoice_listener(invoice_queue)
|
||||
register_invoice_listener(invoice_queue, get_current_extension_name())
|
||||
|
||||
while True:
|
||||
payment = await invoice_queue.get()
|
||||
|
|
|
|||
|
|
@ -328,7 +328,7 @@
|
|||
)
|
||||
},
|
||||
checkBalances: async function () {
|
||||
if (!this.charge.hasStaleBalance) await this.refreshCharge()
|
||||
if (this.charge.hasStaleBalance) return
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
|
|
@ -339,18 +339,9 @@
|
|||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
refreshCharge: async function () {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
`/satspay/api/v1/charge/${this.charge.id}`
|
||||
)
|
||||
this.charge = mapCharge(data, this.charge)
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
checkPendingOnchain: async function () {
|
||||
if (!this.charge.onchainaddress) return
|
||||
|
||||
const {
|
||||
bitcoin: {addresses: addressesAPI}
|
||||
} = mempoolJS({
|
||||
|
|
|
|||
|
|
@ -9,10 +9,9 @@ from starlette.responses import HTMLResponse
|
|||
from lnbits.core.crud import get_wallet
|
||||
from lnbits.core.models import User
|
||||
from lnbits.decorators import check_user_exists
|
||||
from lnbits.extensions.watchonly.crud import get_config
|
||||
|
||||
from . import satspay_ext, satspay_renderer
|
||||
from .crud import get_charge
|
||||
from .crud import get_charge, get_charge_config
|
||||
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
|
|
@ -32,7 +31,7 @@ async def display(request: Request, charge_id: str):
|
|||
status_code=HTTPStatus.NOT_FOUND, detail="Charge link does not exist."
|
||||
)
|
||||
wallet = await get_wallet(charge.lnbitswallet)
|
||||
onchainwallet_config = await get_config(charge.user)
|
||||
onchainwallet_config = await get_charge_config(charge_id)
|
||||
inkey = wallet.inkey if wallet else None
|
||||
mempool_endpoint = (
|
||||
onchainwallet_config.mempool_endpoint if onchainwallet_config else None
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ from .crud import (
|
|||
get_charges,
|
||||
update_charge,
|
||||
)
|
||||
from .helpers import compact_charge
|
||||
from .models import CreateCharge
|
||||
|
||||
#############################CHARGES##########################
|
||||
|
|
@ -93,7 +94,7 @@ async def api_charge_delete(charge_id, wallet: WalletTypeInfo = Depends(get_key_
|
|||
)
|
||||
|
||||
await delete_charge(charge_id)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
#############################BALANCE##########################
|
||||
|
|
@ -123,25 +124,13 @@ async def api_charge_balance(charge_id):
|
|||
try:
|
||||
r = await client.post(
|
||||
charge.webhook,
|
||||
json={
|
||||
"id": charge.id,
|
||||
"description": charge.description,
|
||||
"onchainaddress": charge.onchainaddress,
|
||||
"payment_request": charge.payment_request,
|
||||
"payment_hash": charge.payment_hash,
|
||||
"time": charge.time,
|
||||
"amount": charge.amount,
|
||||
"balance": charge.balance,
|
||||
"paid": charge.paid,
|
||||
"timestamp": charge.timestamp,
|
||||
"completelink": charge.completelink,
|
||||
},
|
||||
json=compact_charge(charge),
|
||||
timeout=40,
|
||||
)
|
||||
except AssertionError:
|
||||
charge.webhook = None
|
||||
return {
|
||||
**charge.dict(),
|
||||
**compact_charge(charge),
|
||||
**{"time_elapsed": charge.time_elapsed},
|
||||
**{"time_left": charge.time_left},
|
||||
**{"paid": charge.paid},
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
|
||||
SCRUB is a small but handy extension that allows a user to take advantage of all the functionalities inside **LNbits** and upon a payment received to your LNbits wallet, automatically forward it to your desired wallet via LNURL or LNAddress!
|
||||
|
||||
<small>Only whole values, integers, are Scrubbed, amounts will be rounded down (example: 6.3 will be 6)! The decimals, if existing, will be kept in your wallet!</small>
|
||||
|
||||
[**Wallets supporting LNURL**](https://github.com/fiatjaf/awesome-lnurl#wallets)
|
||||
|
||||
## Usage
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import asyncio
|
||||
import json
|
||||
from http import HTTPStatus
|
||||
from math import floor
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import httpx
|
||||
|
|
@ -9,6 +10,7 @@ from fastapi import HTTPException
|
|||
from lnbits import bolt11
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.core.services import pay_invoice
|
||||
from lnbits.helpers import get_current_extension_name
|
||||
from lnbits.tasks import register_invoice_listener
|
||||
|
||||
from .crud import get_scrub_by_wallet
|
||||
|
|
@ -16,7 +18,7 @@ from .crud import get_scrub_by_wallet
|
|||
|
||||
async def wait_for_paid_invoices():
|
||||
invoice_queue = asyncio.Queue()
|
||||
register_invoice_listener(invoice_queue)
|
||||
register_invoice_listener(invoice_queue, get_current_extension_name())
|
||||
|
||||
while True:
|
||||
payment = await invoice_queue.get()
|
||||
|
|
@ -25,7 +27,7 @@ async def wait_for_paid_invoices():
|
|||
|
||||
async def on_invoice_paid(payment: Payment) -> None:
|
||||
# (avoid loops)
|
||||
if "scrubed" == payment.extra.get("tag"):
|
||||
if payment.extra.get("tag") == "scrubed":
|
||||
# already scrubbed
|
||||
return
|
||||
|
||||
|
|
@ -41,12 +43,13 @@ async def on_invoice_paid(payment: Payment) -> None:
|
|||
|
||||
# I REALLY HATE THIS DUPLICATION OF CODE!! CORE/VIEWS/API.PY, LINE 267
|
||||
domain = urlparse(data["callback"]).netloc
|
||||
rounded_amount = floor(payment.amount / 1000) * 1000
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
r = await client.get(
|
||||
data["callback"],
|
||||
params={"amount": payment.amount},
|
||||
params={"amount": rounded_amount},
|
||||
timeout=40,
|
||||
)
|
||||
if r.is_error:
|
||||
|
|
@ -65,7 +68,8 @@ async def on_invoice_paid(payment: Payment) -> None:
|
|||
)
|
||||
|
||||
invoice = bolt11.decode(params["pr"])
|
||||
if invoice.amount_msat != payment.amount:
|
||||
|
||||
if invoice.amount_msat != rounded_amount:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=f"{domain} returned an invalid invoice. Expected {payment.amount} msat, got {invoice.amount_msat}.",
|
||||
|
|
|
|||
|
|
@ -68,6 +68,21 @@
|
|||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-my-none">{{SITE_TITLE}} Scrub extension</h6>
|
||||
<p>
|
||||
Automatically forward funds (Scrub) that get paid to the LNbits
|
||||
wallet, to an LNURLpay or Lightning Address.
|
||||
<br />
|
||||
More info in Scrub's
|
||||
<a
|
||||
href="https://github.com/lnbits/lnbits/blob/main/lnbits/extensions/scrub/README.md#scrub"
|
||||
target="_blank"
|
||||
>readme</a
|
||||
>.
|
||||
</p>
|
||||
<p style="font-size: 90%">
|
||||
<strong>Important: </strong>wallet will need a float to account for
|
||||
any fees, before being able to push a payment
|
||||
</p>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
|
|
|
|||
|
|
@ -109,4 +109,4 @@ async def api_link_delete(link_id, wallet: WalletTypeInfo = Depends(require_admi
|
|||
)
|
||||
|
||||
await delete_scrub_link(link_id)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue