Merge remote-tracking branch 'origin/diagon-alley' into diagon-alley
This commit is contained in:
commit
5dbcbb7489
110 changed files with 3353 additions and 904 deletions
24
.env.example
24
.env.example
|
|
@ -9,8 +9,11 @@ LNBITS_ADMIN_USERS=""
|
|||
LNBITS_ADMIN_EXTENSIONS="nostradmin"
|
||||
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 +28,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,
|
||||
|
|
@ -87,3 +92,8 @@ LNBITS_DENOMINATION=sats
|
|||
# EclairWallet
|
||||
ECLAIR_URL=http://127.0.0.1:8283
|
||||
ECLAIR_PASS=eclairpw
|
||||
|
||||
# LnTipsWallet
|
||||
# Enter /api in LightningTipBot to get your key
|
||||
LNTIPS_API_KEY=LNTIPS_ADMIN_KEY
|
||||
LNTIPS_API_ENDPOINT=https://ln.tips
|
||||
|
|
|
|||
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
|
||||
|
|
|
|||
12
Dockerfile
12
Dockerfile
|
|
@ -1,13 +1,23 @@
|
|||
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
|
||||
|
||||
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,6 +44,8 @@ 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
|
||||
|
|
@ -292,6 +298,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
|
||||
# ---------------
|
||||
|
||||
|
|
@ -328,7 +333,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 +343,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(
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -186,9 +186,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}")
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
+ """
|
||||
|
|
|
|||
|
|
@ -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 create_refund, get_hit
|
||||
|
|
@ -12,7 +13,7 @@ from .crud import create_refund, 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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,5 +2,5 @@
|
|||
"name": "Diagon Alley",
|
||||
"short_description": "Nostr shop system",
|
||||
"icon": "add_shopping_cart",
|
||||
"contributors": ["benarc"]
|
||||
"contributors": ["benarc", "talvasconcelos"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ from lnbits.settings import WALLET
|
|||
|
||||
from . import db
|
||||
from .models import (
|
||||
ChatMessage,
|
||||
CreateChatMessage,
|
||||
CreateMarket,
|
||||
CreateMarketStalls,
|
||||
Market,
|
||||
|
|
@ -190,7 +192,6 @@ async def get_diagonalley_stall(stall_id: str) -> Optional[Stalls]:
|
|||
row = await db.fetchone(
|
||||
"SELECT * FROM diagonalley.stalls WHERE id = ?", (stall_id,)
|
||||
)
|
||||
print("ROW", row)
|
||||
return Stalls(**row) if row else None
|
||||
|
||||
|
||||
|
|
@ -303,6 +304,20 @@ async def set_diagonalley_order_paid(payment_hash: str) -> Orders:
|
|||
)
|
||||
|
||||
|
||||
async def set_diagonalley_order_pubkey(payment_hash: str, pubkey: str):
|
||||
await db.execute(
|
||||
"""
|
||||
UPDATE diagonalley.orders
|
||||
SET pubkey = ?
|
||||
WHERE invoiceid = ?
|
||||
""",
|
||||
(
|
||||
pubkey,
|
||||
payment_hash,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def update_diagonalley_product_stock(products):
|
||||
|
||||
q = "\n".join(
|
||||
|
|
@ -405,3 +420,48 @@ async def create_diagonalley_market_stalls(
|
|||
|
||||
async def update_diagonalley_market(market_id):
|
||||
pass
|
||||
|
||||
|
||||
### CHAT / MESSAGES
|
||||
|
||||
|
||||
async def create_chat_message(data: CreateChatMessage):
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO diagonalley.messages (msg, pubkey, id_conversation)
|
||||
VALUES (?, ?, ?)
|
||||
""",
|
||||
(
|
||||
data.msg,
|
||||
data.pubkey,
|
||||
data.room_name,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def get_diagonalley_latest_chat_messages(room_name: str):
|
||||
rows = await db.fetchall(
|
||||
"SELECT * FROM diagonalley.messages WHERE id_conversation = ? ORDER BY timestamp DESC LIMIT 20",
|
||||
(room_name,),
|
||||
)
|
||||
|
||||
return [ChatMessage(**row) for row in rows]
|
||||
|
||||
|
||||
async def get_diagonalley_chat_messages(room_name: str):
|
||||
rows = await db.fetchall(
|
||||
"SELECT * FROM diagonalley.messages WHERE id_conversation = ? ORDER BY timestamp DESC",
|
||||
(room_name,),
|
||||
)
|
||||
|
||||
return [ChatMessage(**row) for row in rows]
|
||||
|
||||
|
||||
async def get_diagonalley_chat_by_merchant(ids: List[str]) -> List[ChatMessage]:
|
||||
|
||||
q = ",".join(["?"] * len(ids))
|
||||
rows = await db.fetchall(
|
||||
f"SELECT * FROM diagonalley.messages WHERE id_conversation IN ({q})",
|
||||
(*ids,),
|
||||
)
|
||||
return [ChatMessage(**row) for row in rows]
|
||||
|
|
|
|||
|
|
@ -113,3 +113,33 @@ async def m001_initial(db):
|
|||
);
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
async def m002_add_chat_messages(db):
|
||||
"""
|
||||
Initial chat messages table.
|
||||
"""
|
||||
await db.execute(
|
||||
f"""
|
||||
CREATE TABLE diagonalley.messages (
|
||||
id {db.serial_primary_key},
|
||||
msg TEXT NOT NULL,
|
||||
pubkey TEXT NOT NULL,
|
||||
id_conversation TEXT NOT NULL,
|
||||
timestamp TIMESTAMP NOT NULL DEFAULT """
|
||||
+ db.timestamp_now
|
||||
+ """
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
if db.type != "SQLITE":
|
||||
"""
|
||||
Create indexes for message fetching
|
||||
"""
|
||||
await db.execute(
|
||||
"CREATE INDEX idx_messages_timestamp ON diagonalley.messages (timestamp DESC)"
|
||||
)
|
||||
await db.execute(
|
||||
"CREATE INDEX idx_messages_conversations ON diagonalley.messages (id_conversation)"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ class createOrderDetails(BaseModel):
|
|||
|
||||
class createOrder(BaseModel):
|
||||
wallet: str = Query(...)
|
||||
username: str = Query(None)
|
||||
pubkey: str = Query(None)
|
||||
shippingzone: str = Query(...)
|
||||
address: str = Query(...)
|
||||
|
|
@ -107,3 +108,17 @@ class Market(BaseModel):
|
|||
|
||||
class CreateMarketStalls(BaseModel):
|
||||
stallid: str
|
||||
|
||||
|
||||
class ChatMessage(BaseModel):
|
||||
id: str
|
||||
msg: str
|
||||
pubkey: str
|
||||
id_conversation: str
|
||||
timestamp: int
|
||||
|
||||
|
||||
class CreateChatMessage(BaseModel):
|
||||
msg: str = Query(..., min_length=1)
|
||||
pubkey: str = Query(...)
|
||||
room_name: str = Query(...)
|
||||
|
|
|
|||
91
lnbits/extensions/diagonalley/notifier.py
Normal file
91
lnbits/extensions/diagonalley/notifier.py
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
## adapted from https://github.com/Sentymental/chat-fastapi-websocket
|
||||
"""
|
||||
Create a class Notifier that will handle messages
|
||||
and delivery to the specific person
|
||||
"""
|
||||
|
||||
import json
|
||||
from collections import defaultdict
|
||||
|
||||
from fastapi import WebSocket
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.extensions.diagonalley.crud import create_chat_message
|
||||
from lnbits.extensions.diagonalley.models import CreateChatMessage
|
||||
|
||||
|
||||
class Notifier:
|
||||
"""
|
||||
Manages chatrooms, sessions and members.
|
||||
|
||||
Methods:
|
||||
- get_notification_generator(self): async generator with notification messages
|
||||
- get_members(self, room_name: str): get members in room
|
||||
- push(message: str, room_name: str): push message
|
||||
- connect(websocket: WebSocket, room_name: str): connect to room
|
||||
- remove(websocket: WebSocket, room_name: str): remove
|
||||
- _notify(message: str, room_name: str): notifier
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# Create sessions as a dict:
|
||||
self.sessions: dict = defaultdict(dict)
|
||||
|
||||
# Create notification generator:
|
||||
self.generator = self.get_notification_generator()
|
||||
|
||||
async def get_notification_generator(self):
|
||||
"""Notification Generator"""
|
||||
|
||||
while True:
|
||||
message = yield
|
||||
msg = message["message"]
|
||||
room_name = message["room_name"]
|
||||
await self._notify(msg, room_name)
|
||||
|
||||
def get_members(self, room_name: str):
|
||||
"""Get all members in a room"""
|
||||
|
||||
try:
|
||||
logger.info(f"Looking for members in room: {room_name}")
|
||||
return self.sessions[room_name]
|
||||
|
||||
except Exception:
|
||||
logger.exception(f"There is no member in room: {room_name}")
|
||||
return None
|
||||
|
||||
async def push(self, message: str, room_name: str = None):
|
||||
"""Push a message"""
|
||||
|
||||
message_body = {"message": message, "room_name": room_name}
|
||||
await self.generator.asend(message_body)
|
||||
|
||||
async def connect(self, websocket: WebSocket, room_name: str):
|
||||
"""Connect to room"""
|
||||
|
||||
await websocket.accept()
|
||||
if self.sessions[room_name] == {} or len(self.sessions[room_name]) == 0:
|
||||
self.sessions[room_name] = []
|
||||
|
||||
self.sessions[room_name].append(websocket)
|
||||
print(f"Connections ...: {self.sessions[room_name]}")
|
||||
|
||||
def remove(self, websocket: WebSocket, room_name: str):
|
||||
"""Remove websocket from room"""
|
||||
|
||||
self.sessions[room_name].remove(websocket)
|
||||
print(f"Connection removed...\nOpen connections...: {self.sessions[room_name]}")
|
||||
|
||||
async def _notify(self, message: str, room_name: str):
|
||||
"""Notifier"""
|
||||
d = json.loads(message)
|
||||
d["room_name"] = room_name
|
||||
db_msg = CreateChatMessage.parse_obj(d)
|
||||
await create_chat_message(data=db_msg)
|
||||
|
||||
remaining_sessions = []
|
||||
while len(self.sessions[room_name]) > 0:
|
||||
websocket = self.sessions[room_name].pop()
|
||||
await websocket.send_text(message)
|
||||
remaining_sessions.append(websocket)
|
||||
self.sessions[room_name] = remaining_sessions
|
||||
|
|
@ -252,29 +252,33 @@
|
|||
label="Wallet *"
|
||||
>
|
||||
</q-select>
|
||||
<!-- NOSTR -->
|
||||
<!-- <div class="row">
|
||||
<div class="col-5">
|
||||
<q-btn unelevated @onclick="generateKeys" color="primary">Generate keys</q-btn>
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<q-btn unelevated @onclick="restoreKeys" color="primary">Restore keys</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<q-input
|
||||
v-if="stallDialog.restorekeys"
|
||||
v-if="keys"
|
||||
filled
|
||||
dense
|
||||
v-model.trim="stallDialog.data.publickey"
|
||||
label="Public Key"
|
||||
></q-input>
|
||||
<q-input
|
||||
v-if="stallDialog.restorekeys"
|
||||
v-if="keys"
|
||||
filled
|
||||
dense
|
||||
v-model.trim="stallDialog.data.privatekey"
|
||||
label="Private Key"
|
||||
></q-input> -->
|
||||
></q-input>
|
||||
<!-- NOSTR -->
|
||||
<div class="row">
|
||||
<div class="col-5">
|
||||
<q-btn unelevated @click="generateKeys" color="primary"
|
||||
>Generate keys</q-btn
|
||||
>
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<q-btn unelevated @click="restoreKeys" color="primary"
|
||||
>Restore keys</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<q-select
|
||||
:options="zoneOptions"
|
||||
filled
|
||||
|
|
@ -314,16 +318,18 @@
|
|||
unelevated
|
||||
color="primary"
|
||||
type="submit"
|
||||
>Update Store</q-btn
|
||||
>Update Stall</q-btn
|
||||
>
|
||||
<q-btn
|
||||
v-else
|
||||
unelevated
|
||||
color="primary"
|
||||
:disable="stallDialog.data.wallet == null
|
||||
|| stallDialog.data.shippingzones == null"
|
||||
|| stallDialog.data.shippingzones == null
|
||||
|| stallDialog.data.publickey == null
|
||||
|| stallDialog.data.privatekey == null"
|
||||
type="submit"
|
||||
>Create Store</q-btn
|
||||
>Create Stall</q-btn
|
||||
>
|
||||
<q-btn
|
||||
v-close-popup
|
||||
|
|
@ -341,6 +347,29 @@
|
|||
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-btn unelevated color="primary" @click="zoneDialog.show = true"
|
||||
>+ Shipping Zone<q-tooltip> Create a shipping zone </q-tooltip></q-btn
|
||||
>
|
||||
<q-btn
|
||||
unelevated
|
||||
v-if="zones.length > 0"
|
||||
color="primary"
|
||||
@click="openStallDialog()"
|
||||
>+ Stall
|
||||
<q-tooltip>
|
||||
Create a market stall to list products on
|
||||
</q-tooltip></q-btn
|
||||
>
|
||||
<q-btn
|
||||
unelevated
|
||||
v-else
|
||||
color="primary"
|
||||
@click="errorMessage('First set shipping zone(s).')"
|
||||
>+ Stall
|
||||
<q-tooltip>
|
||||
Create a market stall to list products on
|
||||
</q-tooltip></q-btn
|
||||
>
|
||||
<q-btn
|
||||
unelevated
|
||||
v-if="stalls.length > 0"
|
||||
|
|
@ -355,29 +384,15 @@
|
|||
@click="errorMessage('First set shipping zone(s), then create a stall.')"
|
||||
>+ Product <q-tooltip> List a product </q-tooltip></q-btn
|
||||
>
|
||||
<q-btn unelevated color="primary" @click="zoneDialog.show = true"
|
||||
>+ Shipping Zone<q-tooltip> Create a shipping zone </q-tooltip></q-btn
|
||||
>
|
||||
<q-btn
|
||||
class="float-right"
|
||||
unelevated
|
||||
v-if="zones.length > 0"
|
||||
flat
|
||||
color="primary"
|
||||
@click="openStallDialog()"
|
||||
>+ Store
|
||||
<q-tooltip> Create a stall to list products on </q-tooltip></q-btn
|
||||
>
|
||||
<q-btn
|
||||
unelevated
|
||||
v-else
|
||||
color="primary"
|
||||
@click="errorMessage('First set shipping zone(s).')"
|
||||
>+ Store
|
||||
<q-tooltip> Create a store to list products on </q-tooltip></q-btn
|
||||
>
|
||||
<q-btn unelevated color="primary" @click="marketDialog.show = true"
|
||||
>Launch frontend shop (not Nostr)
|
||||
@click="marketDialog.show = true"
|
||||
>Create Market
|
||||
<q-tooltip>
|
||||
Makes a simple frontend shop for your stalls</q-tooltip
|
||||
Makes a simple frontend shop for your stalls (not NOSTR)</q-tooltip
|
||||
></q-btn
|
||||
>
|
||||
</q-card-section>
|
||||
|
|
@ -407,6 +422,7 @@
|
|||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th auto-width></q-th>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
|
|
@ -426,6 +442,23 @@
|
|||
:icon="props.expand ? 'remove' : 'add'"
|
||||
/>
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
size="sm"
|
||||
color="green"
|
||||
dense
|
||||
icon="chat"
|
||||
@click="chatRoom(props.row.invoiceid)"
|
||||
>
|
||||
<q-badge
|
||||
v-if="props.row.unread"
|
||||
color="red"
|
||||
rounded
|
||||
floating
|
||||
style="padding: 6px; border-radius: 6px"
|
||||
/>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
|
|
@ -549,6 +582,7 @@
|
|||
<q-tr :props="props">
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
disabled
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
|
|
@ -596,11 +630,11 @@
|
|||
</q-card>
|
||||
|
||||
<q-card>
|
||||
<!-- STORES TABLE -->
|
||||
<!-- STALLS TABLE -->
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none">Stores</h5>
|
||||
<h5 class="text-subtitle1 q-my-none">Market Stalls</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exportStallsCSV"
|
||||
|
|
@ -636,10 +670,10 @@
|
|||
icon="storefront"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
type="a"
|
||||
:href="'/diagonalley/' + props.row.id"
|
||||
:href="'/diagonalley/stalls/' + props.row.id"
|
||||
target="_blank"
|
||||
></q-btn>
|
||||
<q-tooltip> Link to pass to stall relay </q-tooltip>
|
||||
<q-tooltip> Stall simple UI shopping cart </q-tooltip>
|
||||
</q-td>
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
|
|
@ -802,6 +836,52 @@
|
|||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<!-- KEYS -->
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none">Keys</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exportKeysCSV"
|
||||
>Export to CSV</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<div v-if="keys" class="row">
|
||||
<div
|
||||
class="col-6"
|
||||
v-for="type in ['pubkey', 'privkey']"
|
||||
v-bind:key="type"
|
||||
>
|
||||
<div class="text-center q-mb-lg">
|
||||
{% raw %}
|
||||
<q-responsive
|
||||
:ratio="1"
|
||||
class="q-mx-xl"
|
||||
@click="copyText(keys[type])"
|
||||
>
|
||||
<qrcode
|
||||
:value="keys[type]"
|
||||
:options="{width: 250}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
<q-tooltip>{{ keys[type] }}</q-tooltip>
|
||||
</q-responsive>
|
||||
<p>
|
||||
{{ type == 'pubkey' ? 'Public Key' : 'Private Key' }}<br /><small
|
||||
>Click to copy</small
|
||||
>
|
||||
</p>
|
||||
{% endraw %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
|
||||
|
|
@ -816,22 +896,115 @@
|
|||
<q-list> {% include "diagonalley/_api_docs.html" %} </q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<!-- CHAT BOX -->
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-my-none">Messages</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
|
||||
<div class="column q-ma-md q-pb-lg" style="height: 350px">
|
||||
<div class="col q-pb-md">
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-select
|
||||
v-model="customerKey"
|
||||
:options="Object.keys(messages).map(k => ({label: `${k.slice(0, 25)}...`, value: k}))"
|
||||
label="Customers"
|
||||
@input="chatRoom(customerKey)"
|
||||
emit-value
|
||||
></q-select>
|
||||
</q-card-section>
|
||||
<div class="chat-container q-pa-md" ref="chatCard">
|
||||
<div class="chat-box">
|
||||
<!-- <p v-if="Object.keys(messages).length === 0">No messages yet</p> -->
|
||||
<div class="chat-messages">
|
||||
<q-chat-message
|
||||
:key="index"
|
||||
v-for="(message, index) in orderMessages"
|
||||
:name="message.pubkey == keys.pubkey ? 'me' : 'customer'"
|
||||
:text="[message.msg]"
|
||||
:sent="message.pubkey == keys.pubkey ? true : false"
|
||||
:bg-color="message.pubkey == keys.pubkey ? 'white' : 'light-green-2'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<q-form @submit="sendMessage" class="full-width chat-input">
|
||||
<q-input
|
||||
ref="newMessage"
|
||||
v-model="newMessage"
|
||||
placeholder="Message"
|
||||
class="full-width"
|
||||
dense
|
||||
outlined
|
||||
>
|
||||
<template>
|
||||
<q-btn
|
||||
round
|
||||
dense
|
||||
flat
|
||||
type="submit"
|
||||
icon="send"
|
||||
color="primary"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-form>
|
||||
</div>
|
||||
</q-card>
|
||||
<!-- <q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-my-none">Messages</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<div
|
||||
ref="chatCard"
|
||||
class="q-ma-md q-pb-lg"
|
||||
style="height: 350px"
|
||||
>
|
||||
<div class="q-pb-md">
|
||||
<q-select
|
||||
v-model="customerKey"
|
||||
style="width: 80%"
|
||||
:options="customerKeys"
|
||||
:options="Object.keys(messages)"
|
||||
label="Customers"
|
||||
@input="getMessages(customerKey)"
|
||||
@input="chatRoom(customerKey)"
|
||||
></q-select>
|
||||
<div class="chat-container q-pa-md">
|
||||
<div class="chat-box">
|
||||
<p v-if="Object.keys(messages).length === 0">No messages yet</p>
|
||||
<div class="chat-messages">
|
||||
<q-chat-message
|
||||
:key="index"
|
||||
v-for="(message, index) in orderMessages"
|
||||
:name="message.pubkey == keys.pubkey ? 'me' : 'customer'"
|
||||
:text="[message.msg]"
|
||||
:sent="message.pubkey == keys.pubkey ? true : false"
|
||||
:bg-color="message.pubkey == keys.pubkey ? 'white' : 'light-green-2'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<q-form @submit="sendMessage" class="full-width chat-input">
|
||||
<q-input
|
||||
ref="newMessage"
|
||||
v-model="newMessage"
|
||||
placeholder="Message"
|
||||
class="full-width"
|
||||
dense
|
||||
outlined
|
||||
>
|
||||
<template>
|
||||
<q-btn
|
||||
round
|
||||
dense
|
||||
flat
|
||||
type="submit"
|
||||
icon="send"
|
||||
color="primary"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-8 q-px-md">
|
||||
<div v-for="message in customerMessages">
|
||||
|
|
@ -852,18 +1025,73 @@
|
|||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-card> -->
|
||||
</div>
|
||||
<q-dialog v-model="onboarding.show">
|
||||
<q-card class="q-pa-lg">
|
||||
<h6 class="q-my-md text-primary">How to use Diagon Alley</h6>
|
||||
<q-stepper v-model="step" color="primary" vertical animated>
|
||||
<q-step
|
||||
:name="1"
|
||||
title="Create a Shipping Zone"
|
||||
icon="settings"
|
||||
:done="step > 1"
|
||||
>
|
||||
Create Shipping Zones you're willing to ship to. You can define
|
||||
different values for different zones.
|
||||
<q-stepper-navigation>
|
||||
<q-btn @click="step = step + 1" color="primary" label="Next" />
|
||||
</q-stepper-navigation>
|
||||
</q-step>
|
||||
<q-step
|
||||
:name="2"
|
||||
title="Create a Stall"
|
||||
icon="create_new_folder"
|
||||
:done="step > 2"
|
||||
>
|
||||
Create a Stall and provide private and public keys to use for
|
||||
communication. If you don't have one, LNbits will create a key pair
|
||||
for you. It will be saved and can be used on other stalls.
|
||||
<q-stepper-navigation>
|
||||
<q-btn @click="step = step + 1" color="primary" label="Next" />
|
||||
</q-stepper-navigation>
|
||||
</q-step>
|
||||
|
||||
<q-step :name="3" title="Create Products" icon="assignment">
|
||||
Create your products, add a small description and an image. Choose to
|
||||
what stall, if you have more than one, it belongs to
|
||||
<q-stepper-navigation>
|
||||
<q-btn @click="onboarding.finish" color="primary" label="Finish" />
|
||||
</q-stepper-navigation>
|
||||
<div>
|
||||
<q-checkbox
|
||||
v-model="onboarding.showAgain"
|
||||
label="Show this again?"
|
||||
/>
|
||||
</div>
|
||||
</q-step>
|
||||
</q-stepper>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
<!-- </div>
|
||||
</div> -->
|
||||
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script src="https://cdn.jsdelivr.net/npm/pica@6.1.1/dist/pica.min.js"></script>
|
||||
|
||||
<script>
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
|
||||
const pica = window.pica()
|
||||
|
||||
function imgSizeFit(img, maxWidth = 1024, maxHeight = 768) {
|
||||
let ratio = Math.min(
|
||||
1,
|
||||
maxWidth / img.naturalWidth,
|
||||
maxHeight / img.naturalHeight
|
||||
)
|
||||
return {width: img.naturalWidth * ratio, height: img.naturalHeight * ratio}
|
||||
}
|
||||
|
||||
const mapStalls = obj => {
|
||||
obj._data = _.clone(obj)
|
||||
return obj
|
||||
|
|
@ -882,6 +1110,7 @@
|
|||
new Date(obj.time * 1000),
|
||||
'YYYY-MM-DD HH:mm'
|
||||
)
|
||||
// obj.unread = false
|
||||
return obj
|
||||
}
|
||||
const mapKeys = obj => {
|
||||
|
|
@ -914,6 +1143,19 @@
|
|||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
step: 1,
|
||||
onboarding: {
|
||||
show: true,
|
||||
showAgain: false,
|
||||
finish: () => {
|
||||
this.$q.localStorage.set(
|
||||
'lnbits.DAOnboarding',
|
||||
this.onboarding.showAgain
|
||||
)
|
||||
this.onboarding.show = false
|
||||
}
|
||||
},
|
||||
keys: null,
|
||||
products: [],
|
||||
orders: [],
|
||||
stalls: [],
|
||||
|
|
@ -923,8 +1165,12 @@
|
|||
customerKeys: [],
|
||||
customerKey: '',
|
||||
customerMessages: {},
|
||||
messages: {},
|
||||
newMessage: '',
|
||||
orderMessages: {},
|
||||
shippedModel: false,
|
||||
shippingZoneOptions: [
|
||||
'Free (digital)',
|
||||
'Worldwide',
|
||||
'Europe',
|
||||
'Australia',
|
||||
|
|
@ -989,17 +1235,17 @@
|
|||
ordersTable: {
|
||||
columns: [
|
||||
/*{
|
||||
name: 'product',
|
||||
align: 'left',
|
||||
label: 'Product',
|
||||
field: 'product'
|
||||
},
|
||||
{
|
||||
name: 'quantity',
|
||||
align: 'left',
|
||||
label: 'Quantity',
|
||||
field: 'quantity'
|
||||
},*/
|
||||
name: 'product',
|
||||
align: 'left',
|
||||
label: 'Product',
|
||||
field: 'product'
|
||||
},
|
||||
{
|
||||
name: 'quantity',
|
||||
align: 'left',
|
||||
label: 'Quantity',
|
||||
field: 'quantity'
|
||||
},*/
|
||||
{
|
||||
name: 'id',
|
||||
align: 'left',
|
||||
|
|
@ -1030,7 +1276,7 @@
|
|||
{
|
||||
name: 'stall',
|
||||
align: 'left',
|
||||
label: 'Store',
|
||||
label: 'Stall',
|
||||
field: 'stall'
|
||||
},
|
||||
{
|
||||
|
|
@ -1118,7 +1364,7 @@
|
|||
{
|
||||
name: 'stores',
|
||||
align: 'left',
|
||||
label: 'Stores',
|
||||
label: 'Stalls',
|
||||
field: 'stores'
|
||||
}
|
||||
],
|
||||
|
|
@ -1193,24 +1439,60 @@
|
|||
this[dialog].show = false
|
||||
this[dialog].data = {}
|
||||
},
|
||||
generateKeys: function () {
|
||||
var self = this
|
||||
generateKeys() {
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/diagonalley/api/v1/keys',
|
||||
self.g.user.wallets[0].adminkey
|
||||
this.g.user.wallets[0].adminkey
|
||||
)
|
||||
.then(function (response) {
|
||||
.then(response => {
|
||||
if (response.data) {
|
||||
self.keys = response.data.map(mapKeys)
|
||||
this.keys = response.data
|
||||
this.stallDialog.data.publickey = this.keys.pubkey
|
||||
this.stallDialog.data.privatekey = this.keys.privkey
|
||||
this.$q.localStorage.set(
|
||||
`lnbits.diagonalley.${this.g.user.id}`,
|
||||
this.keys
|
||||
)
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
restoreKeys: function () {},
|
||||
restoreKeys() {
|
||||
let keys = this.$q.localStorage.getItem(
|
||||
`lnbits.diagonalley.${this.g.user.id}`
|
||||
)
|
||||
if (keys) {
|
||||
this.keys = keys
|
||||
this.stallDialog.data.publickey = this.keys.pubkey
|
||||
this.stallDialog.data.privatekey = this.keys.privkey
|
||||
} else {
|
||||
this.$q.notify({
|
||||
type: 'warning',
|
||||
message: 'No keys found.'
|
||||
})
|
||||
}
|
||||
},
|
||||
exportKeysCSV: function () {
|
||||
let colls = [
|
||||
{
|
||||
name: 'privatekey',
|
||||
align: 'left',
|
||||
label: 'Private Key',
|
||||
field: 'privkey'
|
||||
},
|
||||
{
|
||||
name: 'publickey',
|
||||
align: 'left',
|
||||
label: 'Public Key',
|
||||
field: 'pubkey'
|
||||
}
|
||||
]
|
||||
LNbits.utils.exportCSV(colls, [this.keys])
|
||||
},
|
||||
capitalizeFirstLetter: function (string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1)
|
||||
},
|
||||
|
|
@ -1414,9 +1696,10 @@
|
|||
let image = new Image()
|
||||
image.src = blobURL
|
||||
image.onload = async () => {
|
||||
let fit = imgSizeFit(image)
|
||||
let canvas = document.createElement('canvas')
|
||||
canvas.setAttribute('width', 760)
|
||||
canvas.setAttribute('height', 490)
|
||||
canvas.setAttribute('width', fit.width)
|
||||
canvas.setAttribute('height', fit.height)
|
||||
await pica.resize(image, canvas, {
|
||||
quality: 0,
|
||||
alpha: true,
|
||||
|
|
@ -1628,7 +1911,6 @@
|
|||
.then(response => {
|
||||
if (response.data) {
|
||||
this.markets = response.data.map(mapMarkets)
|
||||
console.log(this.markets)
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
|
|
@ -1727,10 +2009,10 @@
|
|||
////////////////////////////////////////
|
||||
////////////////ORDERS//////////////////
|
||||
////////////////////////////////////////
|
||||
getOrders: function () {
|
||||
getOrders: async function () {
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
await LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/diagonalley/api/v1/orders?all_wallets=true',
|
||||
|
|
@ -1739,14 +2021,13 @@
|
|||
.then(function (response) {
|
||||
if (response.data) {
|
||||
self.orders = response.data.map(mapOrders)
|
||||
console.log(self.orders)
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
createOrder: function () {
|
||||
/*createOrder: function () {
|
||||
var data = {
|
||||
address: this.orderDialog.data.address,
|
||||
email: this.orderDialog.data.email,
|
||||
|
|
@ -1771,7 +2052,7 @@
|
|||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
},*/
|
||||
deleteOrder: function (orderId) {
|
||||
var self = this
|
||||
var order = _.findWhere(self.orders, {id: orderId})
|
||||
|
|
@ -1783,7 +2064,7 @@
|
|||
.request(
|
||||
'DELETE',
|
||||
'/diagonalley/api/v1/orders/' + orderId,
|
||||
_.findWhere(self.g.user.wallets, {id: order.wallet}).inkey
|
||||
_.findWhere(self.g.user.wallets, {id: order.wallet}).adminkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.orders = _.reject(self.orders, function (obj) {
|
||||
|
|
@ -1795,37 +2076,202 @@
|
|||
})
|
||||
})
|
||||
},
|
||||
shipOrder: function (order_id) {
|
||||
var self = this
|
||||
|
||||
shipOrder(order_id) {
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/diagonalley/api/v1/orders/shipped/' + order_id,
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.orders.push(mapOrders(response.data))
|
||||
.then(response => {
|
||||
this.orders = _.reject(this.orders, obj => {
|
||||
return obj.id == order_id
|
||||
})
|
||||
this.orders.push(mapOrders(response.data))
|
||||
})
|
||||
.catch(error => {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
exportOrdersCSV: function () {
|
||||
LNbits.utils.exportCSV(this.ordersTable.columns, this.orders)
|
||||
},
|
||||
/// CHAT
|
||||
async getAllMessages() {
|
||||
await LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
`/diagonalley/api/v1/chat/messages/merchant?orders=${this.orders
|
||||
.map(o => o.invoiceid)
|
||||
.toString()}`,
|
||||
this.g.user.wallets[0].adminkey
|
||||
)
|
||||
.then(res => {
|
||||
this.messages = _.groupBy(res.data, 'id_conversation')
|
||||
this.checkUnreadMessages()
|
||||
})
|
||||
.catch(error => {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
updateLastSeenMsg(id) {
|
||||
let data = this.$q.localStorage.getItem(
|
||||
`lnbits.diagonalley.${this.g.user.id}`
|
||||
)
|
||||
let chat = {
|
||||
...data.chat,
|
||||
[`${id}`]: {
|
||||
timestamp: Object.keys(this.orderMessages)[
|
||||
Object.keys(this.orderMessages).length - 1
|
||||
]
|
||||
}
|
||||
}
|
||||
this.$q.localStorage.set(`lnbits.diagonalley.${this.g.user.id}`, {
|
||||
...data,
|
||||
chat
|
||||
})
|
||||
this.checkUnreadMessages()
|
||||
},
|
||||
checkUnreadMessages() {
|
||||
let lastMsgs = this.$q.localStorage.getItem(
|
||||
`lnbits.diagonalley.${this.g.user.id}`
|
||||
).chat
|
||||
for (let key in this.messages) {
|
||||
let idx = this.orders.findIndex(f => f.invoiceid == key)
|
||||
if (!lastMsgs[key]) {
|
||||
this.updateLastSeenMsg(key)
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
lastMsgs[key].timestamp <
|
||||
Math.max(...this.messages[key].map(c => c.timestamp))
|
||||
) {
|
||||
this.$set(this.orders[idx], 'unread', true)
|
||||
} else {
|
||||
this.$set(this.orders[idx], 'unread', false)
|
||||
}
|
||||
}
|
||||
},
|
||||
clearMessage() {
|
||||
this.newMessage = ''
|
||||
this.$refs.newMessage.focus()
|
||||
},
|
||||
sendMessage() {
|
||||
let message = {
|
||||
msg: this.newMessage,
|
||||
pubkey: this.keys.pubkey
|
||||
}
|
||||
this.ws.send(JSON.stringify(message))
|
||||
|
||||
this.clearMessage()
|
||||
},
|
||||
chatRoom(id) {
|
||||
this.startChat(id)
|
||||
this.orderMessages = {}
|
||||
this.messages[id].map(m => {
|
||||
this.$set(this.orderMessages, m.timestamp, {
|
||||
msg: m.msg,
|
||||
pubkey: m.pubkey
|
||||
})
|
||||
})
|
||||
this.$refs.chatCard.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
inline: 'nearest'
|
||||
})
|
||||
this.updateLastSeenMsg(id)
|
||||
//"ea2fbf6c91aa228603681e2cc34bb06e34e6d1375fa4d6c35756182b2fa3307f"
|
||||
//"c7435a04875c26e28db91a377bd6e991dbfefeefea8258415f3ae0c716ed2335"
|
||||
},
|
||||
startChat(room_name) {
|
||||
if (this.ws) {
|
||||
this.ws.close()
|
||||
}
|
||||
if (location.protocol == 'https:') {
|
||||
ws_scheme = 'wss://'
|
||||
} else {
|
||||
ws_scheme = 'ws://'
|
||||
}
|
||||
ws = new WebSocket(
|
||||
ws_scheme + location.host + '/diagonalley/ws/' + room_name
|
||||
)
|
||||
|
||||
function checkWebSocket(event) {
|
||||
if (ws.readyState === WebSocket.CLOSED) {
|
||||
console.log('WebSocket CLOSED: Reopening')
|
||||
ws = new WebSocket(
|
||||
ws_scheme + location.host + '/diagonalley/ws/' + room_name
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ws.onmessage = event => {
|
||||
let event_data = JSON.parse(event.data)
|
||||
|
||||
this.$set(this.orderMessages, Date.now(), event_data)
|
||||
this.updateLastSeenMsg(room_name)
|
||||
}
|
||||
|
||||
ws.onclose = event => {
|
||||
this.updateLastSeenMsg(room_name)
|
||||
}
|
||||
|
||||
this.ws = ws
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
async created() {
|
||||
if (this.g.user.wallets.length) {
|
||||
let showOnboard = this.$q.localStorage.getItem('lnbits.DAOnboarding')
|
||||
this.onboarding.show = showOnboard === true || showOnboard == null
|
||||
this.onboarding.showAgain = showOnboard || false
|
||||
this.getStalls()
|
||||
this.getProducts()
|
||||
this.getZones()
|
||||
this.getOrders()
|
||||
await this.getOrders()
|
||||
this.getMarkets()
|
||||
this.customerKeys = [
|
||||
'cb4c0164fe03fcdadcbfb4f76611c71620790944c24f21a1cd119395cdedfe1b',
|
||||
'a9c17358a6dc4ceb3bb4d883eb87967a66b3453a0f3199f0b1c8eef8070c6a07'
|
||||
]
|
||||
console.log(_.pick(this.g.user, 'id'))
|
||||
await this.getAllMessages()
|
||||
let keys = this.$q.localStorage.getItem(
|
||||
`lnbits.diagonalley.${this.g.user.id}`
|
||||
)
|
||||
if (keys) {
|
||||
this.keys = keys
|
||||
}
|
||||
setInterval(() => {
|
||||
this.getAllMessages()
|
||||
}, 300000)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
.q-field__native span {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.chat-container {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-rows: 1fr auto;
|
||||
height: calc(100vh - 140px);
|
||||
}
|
||||
|
||||
.chat-box {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: end;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
|
|
|||
511
lnbits/extensions/diagonalley/templates/diagonalley/order.html
Normal file
511
lnbits/extensions/diagonalley/templates/diagonalley/order.html
Normal file
|
|
@ -0,0 +1,511 @@
|
|||
{% extends "public.html" %} {% block page %}
|
||||
<div class="row q-col-gutter-md flex">
|
||||
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
|
||||
<q-card>
|
||||
<div class="chat-container q-pa-md">
|
||||
<div class="chat-box">
|
||||
<!-- <p v-if="Object.keys(messages).length === 0">No messages yet</p> -->
|
||||
<div class="chat-messages">
|
||||
<q-chat-message
|
||||
:key="index"
|
||||
v-for="(message, index) in messages"
|
||||
:name="message.pubkey == user.keys.publickey ? 'me' : 'merchant'"
|
||||
:text="[message.msg]"
|
||||
:sent="message.pubkey == user.keys.publickey ? true : false"
|
||||
:bg-color="message.pubkey == user.keys.publickey ? 'white' : 'light-green-2'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<q-form @submit="sendMessage" class="full-width chat-input">
|
||||
<q-input
|
||||
ref="newMessage"
|
||||
v-model="newMessage"
|
||||
placeholder="Message"
|
||||
class="full-width"
|
||||
dense
|
||||
outlined
|
||||
>
|
||||
<template>
|
||||
<q-btn
|
||||
round
|
||||
dense
|
||||
flat
|
||||
type="submit"
|
||||
icon="send"
|
||||
color="primary"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-form>
|
||||
</div>
|
||||
</q-card>
|
||||
</div>
|
||||
<div class="col-12 col-md-5 col-lg-6 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
{% raw %}
|
||||
<h6 class="text-subtitle1 q-my-none">{{ stall.name }}</h6>
|
||||
<p @click="copyText(stall.publickey)" style="width: max-content">
|
||||
Public Key: {{ sliceKey(stall.publickey) }}
|
||||
<q-tooltip>Click to copy</q-tooltip>
|
||||
</p>
|
||||
{% endraw %}
|
||||
</q-card-section>
|
||||
<q-card-section v-if="user">
|
||||
<q-form @submit="" class="q-gutter-md">
|
||||
<!-- <q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="model"
|
||||
:options="mockMerch"
|
||||
label="Merchant"
|
||||
hint="Select a merchant you've opened an order to"
|
||||
></q-select>
|
||||
<br /> -->
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="selectedOrder"
|
||||
:options="Object.keys(user.orders).map(o => ({label: `${o.slice(0, 25)}...`, value: o}))"
|
||||
label="Order"
|
||||
hint="Select an order from this merchant"
|
||||
@input="val => { changeOrder() }"
|
||||
emit-value
|
||||
></q-select>
|
||||
</q-form>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-list>
|
||||
{% raw %}
|
||||
<q-item clickable :key="p.id" v-for="p in products">
|
||||
<q-item-section side>
|
||||
<span>{{p.quantity}} x </span>
|
||||
</q-item-section>
|
||||
<q-item-section avatar>
|
||||
<q-avatar color="primary">
|
||||
<img size="sm" :src="p.image" />
|
||||
</q-avatar>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ p.name }}</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section side>
|
||||
<span> {{p.price}} sats</span>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
{% endraw %}
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-separator></q-separator>
|
||||
<q-list>
|
||||
<q-expansion-item group="extras" icon="vpn_key" label="Keys"
|
||||
><p>
|
||||
Bellow are the keys needed to contact the merchant. They are
|
||||
stored in the browser!
|
||||
</p>
|
||||
<div v-if="user?.keys" class="row q-col-gutter-md">
|
||||
<div
|
||||
class="col-12 col-sm-6"
|
||||
v-for="type in ['publickey', 'privatekey']"
|
||||
v-bind:key="type"
|
||||
>
|
||||
<div class="text-center q-mb-lg">
|
||||
{% raw %}
|
||||
<q-responsive
|
||||
:ratio="1"
|
||||
class="q-mx-auto"
|
||||
style="max-width: 250px"
|
||||
>
|
||||
<qrcode
|
||||
:value="user.keys[type]"
|
||||
:options="{width: 500}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
<q-tooltip>{{ user.keys[type] }}</q-tooltip>
|
||||
</q-responsive>
|
||||
<p>
|
||||
{{ type == 'publickey' ? 'Public Key' : 'Private Key' }}
|
||||
</p>
|
||||
{% endraw %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<q-separator></q-separator>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn outline color="grey" @click="downloadKeys"
|
||||
>Backup keys
|
||||
<q-tooltip>Download your keys</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
class="q-mx-sm"
|
||||
@click="keysDialog.show = true"
|
||||
:disabled="this.user.keys"
|
||||
>Restore keys
|
||||
<q-tooltip>Restore keys</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
@click="deleteData"
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
>Delete data
|
||||
<q-tooltip>Delete all data from browser</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</q-expansion-item>
|
||||
</q-list>
|
||||
<q-expansion-item icon="qr_code" label="Export page">
|
||||
<p>Export, or send, this page to another device</p>
|
||||
<div class="text-center q-mb-lg">
|
||||
<q-responsive
|
||||
:ratio="1"
|
||||
class="q-my-xl q-mx-auto"
|
||||
style="max-width: 250px"
|
||||
@click="copyText(exportURL)"
|
||||
>
|
||||
<qrcode
|
||||
:value="exportURL"
|
||||
:options="{width: 500}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
<q-tooltip>Click to copy</q-tooltip>
|
||||
</q-responsive>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
@click="copyText(exportURL)"
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
>Copy URL
|
||||
<q-tooltip
|
||||
>Export, or send, this page to another device</q-tooltip
|
||||
>
|
||||
</q-btn>
|
||||
</div>
|
||||
</q-expansion-item>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
<!-- RESTORE KEYS DIALOG -->
|
||||
<q-dialog
|
||||
v-model="keysDialog.show"
|
||||
position="top"
|
||||
@hide="clearRestoreKeyDialog"
|
||||
>
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card"> </q-card>
|
||||
<q-card class="q-pa-lg lnbits__dialog-card">
|
||||
<q-form @submit="restoreKeys" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="keysDialog.data.publickey"
|
||||
label="Public Key"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="keysDialog.data.privatekey"
|
||||
label="Private Key *optional"
|
||||
></q-input>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
:disable="keysDialog.data.publickey == null"
|
||||
type="submit"
|
||||
label="Submit"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
@click="clearRestoreKeyDialog"
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
label="Cancel"
|
||||
></q-btn>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
{% endblock %} {% block scripts %}
|
||||
<script>
|
||||
const mapChatMsg = msg => {
|
||||
let obj = {}
|
||||
obj.timestamp = {
|
||||
msg: msg,
|
||||
pubkey: pubkey
|
||||
}
|
||||
return obj
|
||||
}
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
newMessage: '',
|
||||
showMessages: false,
|
||||
messages: {},
|
||||
stall: null,
|
||||
selectedOrder: null,
|
||||
products: [],
|
||||
orders: [],
|
||||
user: {
|
||||
keys: {}
|
||||
},
|
||||
keysDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
exportURL() {
|
||||
return (
|
||||
'{{request.url}}' +
|
||||
`&keys=${this.user.keys.publickey},${this.user.keys.privatekey}`
|
||||
)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clearMessage() {
|
||||
this.newMessage = ''
|
||||
this.$refs.newMessage.focus()
|
||||
},
|
||||
clearRestoreKeyDialog() {
|
||||
this.keysDialog = {
|
||||
show: false,
|
||||
data: {}
|
||||
}
|
||||
},
|
||||
sendMessage() {
|
||||
let message = {
|
||||
msg: this.newMessage,
|
||||
pubkey: this.user.keys.publickey
|
||||
}
|
||||
this.ws.send(JSON.stringify(message))
|
||||
|
||||
this.clearMessage()
|
||||
},
|
||||
sliceKey(key) {
|
||||
if (!key) return ''
|
||||
return `${key.slice(0, 4)}...${key.slice(-4)}`
|
||||
},
|
||||
downloadKeys() {
|
||||
const file = new File(
|
||||
[JSON.stringify(this.user.keys)],
|
||||
'backup_keys.json',
|
||||
{
|
||||
type: 'text/json'
|
||||
}
|
||||
)
|
||||
const link = document.createElement('a')
|
||||
const url = URL.createObjectURL(file)
|
||||
|
||||
link.href = url
|
||||
link.download = file.name
|
||||
link.click()
|
||||
|
||||
window.URL.revokeObjectURL(url)
|
||||
},
|
||||
restoreKeys() {
|
||||
this.user.keys = this.keysDialog.data
|
||||
let data = this.$q.localStorage.getItem(`lnbits.diagonalley.data`)
|
||||
this.$q.localStorage.set(`lnbits.diagonalley.data`, {
|
||||
...data,
|
||||
keys: this.user.keys
|
||||
})
|
||||
|
||||
this.clearRestoreKeyDialog()
|
||||
},
|
||||
deleteData() {
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete your stored data?')
|
||||
.onOk(() => {
|
||||
this.$q.localStorage.remove('lnbits.diagonalley.data')
|
||||
this.user = null
|
||||
})
|
||||
},
|
||||
async generateKeys(payment_hash) {
|
||||
//check if the keys are set
|
||||
if ('publickey' in this.user.keys && 'privatekey' in this.user.keys)
|
||||
return
|
||||
|
||||
return await LNbits.api
|
||||
.request('GET', `/diagonalley/api/v1/keys/${payment_hash}`, null)
|
||||
.then(response => {
|
||||
if (response.data) {
|
||||
let data = {
|
||||
keys: {
|
||||
privatekey: response.data.privkey,
|
||||
publickey: response.data.pubkey
|
||||
}
|
||||
}
|
||||
this.user.keys = data.keys
|
||||
return
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
async getMessages(room_name, all = false) {
|
||||
await LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
`/diagonalley/api/v1/chat/messages/${room_name}${
|
||||
all ? '?all_messages=true' : ''
|
||||
}`
|
||||
)
|
||||
.then(response => {
|
||||
if (response.data) {
|
||||
response.data.reverse().map(m => {
|
||||
this.$set(this.messages, m.timestamp * 1000, {
|
||||
msg: m.msg,
|
||||
pubkey: m.pubkey
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
async changeOrder() {
|
||||
this.products = this.user.orders[this.selectedOrder]
|
||||
this.messages = {}
|
||||
await this.getMessages(this.selectedOrder)
|
||||
this.startChat(this.selectedOrder)
|
||||
},
|
||||
startChat(room_name) {
|
||||
if (this.ws) {
|
||||
this.ws.close()
|
||||
}
|
||||
if (location.protocol == 'https:') {
|
||||
ws_scheme = 'wss://'
|
||||
} else {
|
||||
ws_scheme = 'ws://'
|
||||
}
|
||||
ws = new WebSocket(
|
||||
ws_scheme + location.host + '/diagonalley/ws/' + room_name
|
||||
)
|
||||
|
||||
function checkWebSocket(event) {
|
||||
if (ws.readyState === WebSocket.CLOSED) {
|
||||
console.log('WebSocket CLOSED: Reopening')
|
||||
ws = new WebSocket(
|
||||
ws_scheme + location.host + '/diagonalley/ws/' + room_name
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ws.onmessage = event => {
|
||||
let event_data = JSON.parse(event.data)
|
||||
|
||||
this.$set(this.messages, Date.now(), event_data)
|
||||
}
|
||||
|
||||
this.ws = ws
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
console.log('{{request.url}}')
|
||||
let order_details = JSON.parse('{{ order | tojson }}')
|
||||
let products = JSON.parse('{{ products | tojson }}')
|
||||
let order_id = '{{ order_id }}'
|
||||
let hasKeys = Boolean(
|
||||
JSON.parse('{{ publickey | tojson }}') &&
|
||||
JSON.parse('{{ privatekey | tojson }}')
|
||||
)
|
||||
|
||||
if (hasKeys) {
|
||||
this.user.keys = {
|
||||
privatekey: '{{ privatekey }}',
|
||||
publickey: '{{ publickey }}'
|
||||
}
|
||||
}
|
||||
|
||||
this.stall = JSON.parse('{{ stall | tojson }}')
|
||||
this.products = order_details.map(o => {
|
||||
let product = products.find(p => p.id == o.product_id)
|
||||
return {
|
||||
quantity: o.quantity,
|
||||
name: product.product,
|
||||
image: product.image,
|
||||
price: product.price
|
||||
}
|
||||
})
|
||||
|
||||
let data =
|
||||
this.$q.localStorage.getItem(`lnbits.diagonalley.data`) || false
|
||||
|
||||
if (data) {
|
||||
this.user = data
|
||||
//add chat key (merchant pubkey) if not set
|
||||
if (!this.user.orders[`${order_id}`]) {
|
||||
this.$set(this.user.orders, order_id, this.products)
|
||||
}
|
||||
} else {
|
||||
// generate keys
|
||||
this.generateKeys(order_id)
|
||||
// populate user data
|
||||
this.user.orders = {
|
||||
[`${order_id}`]: this.products
|
||||
}
|
||||
}
|
||||
|
||||
this.selectedOrder = order_id
|
||||
|
||||
await this.getMessages(order_id)
|
||||
|
||||
this.$q.localStorage.set(`lnbits.diagonalley.data`, this.user)
|
||||
this.startChat(order_id)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
.q-field__native span {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-rows: 1fr auto;
|
||||
height: calc(100vh - 133px);
|
||||
}
|
||||
|
||||
.chat-box {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.chat-other {
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: end;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
{% extends "public.html" %} {% block page %}
|
||||
<h1>Product page</h1>
|
||||
{% endblock %} {% block scripts %}
|
||||
<script>
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
@ -125,7 +125,7 @@
|
|||
>
|
||||
</div>
|
||||
<div v-if="item.categories" class="text-subtitle1">
|
||||
<q-chip v-for="cat in item.categories.split(',')" dense
|
||||
<q-chip v-for="(cat, i) in item.categories.split(',')" :key="i" dense
|
||||
>{{cat}}</q-chip
|
||||
>
|
||||
</div>
|
||||
|
|
@ -162,6 +162,17 @@
|
|||
v-model.trim="checkoutDialog.data.username"
|
||||
label="Name *optional"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="checkoutDialog.data.pubkey"
|
||||
label="Public key *optional"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<q-icon @click="getPubkey" name="settings_backup_restore" />
|
||||
<q-tooltip>Click to restore saved public key</q-tooltip>
|
||||
</template>
|
||||
</q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
|
|
@ -206,7 +217,7 @@
|
|||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
@click="checkoutDialog = {show: false, data: {}}"
|
||||
@click="checkoutDialog = {show: false, data: {pubkey: ''}}"
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
|
|
@ -276,7 +287,9 @@
|
|||
cartMenu: [],
|
||||
checkoutDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
data: {
|
||||
pubkey: ''
|
||||
}
|
||||
},
|
||||
qrCodeDialog: {
|
||||
data: {
|
||||
|
|
@ -344,7 +357,18 @@
|
|||
this.cartMenu = Array.from(this.cart.products, item => {
|
||||
return {id: item[0], ...item[1]}
|
||||
})
|
||||
console.log(this.cartMenu, this.cart)
|
||||
},
|
||||
getPubkey() {
|
||||
let data = this.$q.localStorage.getItem(`lnbits.diagonalley.data`)
|
||||
if (data && data.keys.publickey) {
|
||||
this.checkoutDialog.data.pubkey = data.keys.publickey
|
||||
} else {
|
||||
this.$q.notify({
|
||||
type: 'warning',
|
||||
message: 'No public key stored!',
|
||||
icon: 'settings_backup_restore'
|
||||
})
|
||||
}
|
||||
},
|
||||
placeOrder() {
|
||||
let dialog = this.checkoutDialog.data
|
||||
|
|
@ -384,12 +408,25 @@
|
|||
if (res.data.paid) {
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Sats received, thanks!',
|
||||
icon: 'thumb_up'
|
||||
multiLine: true,
|
||||
message:
|
||||
"Sats received, thanks! You'l be redirected to the order page...",
|
||||
icon: 'thumb_up',
|
||||
actions: [
|
||||
{
|
||||
label: 'See Order',
|
||||
handler: () => {
|
||||
window.location.href = `/diagonalley/order/?merch=${this.stall.id}&invoice_id=${this.qrCodeDialog.data.payment_hash}`
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
clearInterval(this.qrCodeDialog.paymentChecker)
|
||||
this.resetCart()
|
||||
this.closeQrCodeDialog()
|
||||
setTimeout(() => {
|
||||
window.location.href = `/diagonalley/order/?merch=${this.stall.id}&invoice_id=${this.qrCodeDialog.data.payment_hash}`
|
||||
}, 5000)
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
|
|
@ -407,8 +444,6 @@
|
|||
created() {
|
||||
this.stall = JSON.parse('{{ stall | tojson }}')
|
||||
this.products = JSON.parse('{{ products | tojson }}')
|
||||
|
||||
console.log(this.stall, this.products)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import json
|
||||
from http import HTTPStatus
|
||||
from typing import List
|
||||
|
||||
from fastapi import Request
|
||||
from fastapi import BackgroundTasks, Query, Request, WebSocket, WebSocketDisconnect
|
||||
from fastapi.params import Depends
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from loguru import logger
|
||||
|
|
@ -10,11 +12,15 @@ from starlette.responses import HTMLResponse
|
|||
from lnbits.core.models import User
|
||||
from lnbits.decorators import check_user_exists # type: ignore
|
||||
from lnbits.extensions.diagonalley import diagonalley_ext, diagonalley_renderer
|
||||
from lnbits.extensions.diagonalley.models import CreateChatMessage
|
||||
from lnbits.extensions.diagonalley.notifier import Notifier
|
||||
|
||||
from ...core.crud import get_wallet
|
||||
from .crud import (
|
||||
create_chat_message,
|
||||
get_diagonalley_market,
|
||||
get_diagonalley_market_stalls,
|
||||
get_diagonalley_order_details,
|
||||
get_diagonalley_order_invoiceid,
|
||||
get_diagonalley_products,
|
||||
get_diagonalley_stall,
|
||||
get_diagonalley_zone,
|
||||
|
|
@ -32,7 +38,7 @@ async def index(request: Request, user: User = Depends(check_user_exists)):
|
|||
)
|
||||
|
||||
|
||||
@diagonalley_ext.get("/{stall_id}", response_class=HTMLResponse)
|
||||
@diagonalley_ext.get("/stalls/{stall_id}", response_class=HTMLResponse)
|
||||
async def display(request: Request, stall_id):
|
||||
stall = await get_diagonalley_stall(stall_id)
|
||||
products = await get_diagonalley_products(stall_id)
|
||||
|
|
@ -85,3 +91,102 @@ async def display(request: Request, market_id):
|
|||
"products": products,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@diagonalley_ext.get("/order", response_class=HTMLResponse)
|
||||
async def chat_page(
|
||||
request: Request,
|
||||
merch: str = Query(...),
|
||||
invoice_id: str = Query(...),
|
||||
keys: str = Query(None),
|
||||
):
|
||||
stall = await get_diagonalley_stall(merch)
|
||||
order = await get_diagonalley_order_invoiceid(invoice_id)
|
||||
_order = await get_diagonalley_order_details(order.id)
|
||||
products = await get_diagonalley_products(stall.id)
|
||||
|
||||
return diagonalley_renderer().TemplateResponse(
|
||||
"diagonalley/order.html",
|
||||
{
|
||||
"request": request,
|
||||
"stall": {
|
||||
"id": stall.id,
|
||||
"name": stall.name,
|
||||
"publickey": stall.publickey,
|
||||
"wallet": stall.wallet,
|
||||
},
|
||||
"publickey": keys.split(",")[0] if keys else None,
|
||||
"privatekey": keys.split(",")[1] if keys else None,
|
||||
"order_id": order.invoiceid,
|
||||
"order": [details.dict() for details in _order],
|
||||
"products": [product.dict() for product in products],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
##################WEBSOCKET ROUTES########################
|
||||
|
||||
# Initialize Notifier:
|
||||
notifier = Notifier()
|
||||
|
||||
|
||||
# class ConnectionManager:
|
||||
# def __init__(self):
|
||||
# self.active_connections: List[WebSocket] = []
|
||||
|
||||
# async def connect(self, websocket: WebSocket, room_name: str):
|
||||
# await websocket.accept()
|
||||
# websocket.id = room_name
|
||||
# self.active_connections.append(websocket)
|
||||
|
||||
# def disconnect(self, websocket: WebSocket):
|
||||
# self.active_connections.remove(websocket)
|
||||
|
||||
# async def send_personal_message(self, message: str, room_name: str):
|
||||
# for connection in self.active_connections:
|
||||
# if connection.id == room_name:
|
||||
# await connection.send_text(message)
|
||||
|
||||
# async def broadcast(self, message: str):
|
||||
# for connection in self.active_connections:
|
||||
# await connection.send_text(message)
|
||||
|
||||
|
||||
# manager = ConnectionManager()
|
||||
|
||||
|
||||
# @diagonalley_ext.websocket("/ws/{room_name}")
|
||||
# async def websocket_endpoint(websocket: WebSocket, room_name: str):
|
||||
# await manager.connect(websocket, room_name)
|
||||
# try:
|
||||
# while True:
|
||||
# data = await websocket.receive_text()
|
||||
# except WebSocketDisconnect:
|
||||
# manager.disconnect(websocket)
|
||||
|
||||
|
||||
@diagonalley_ext.websocket("/ws/{room_name}")
|
||||
async def websocket_endpoint(
|
||||
websocket: WebSocket, room_name: str, background_tasks: BackgroundTasks
|
||||
):
|
||||
await notifier.connect(websocket, room_name)
|
||||
try:
|
||||
while True:
|
||||
data = await websocket.receive_text()
|
||||
d = json.loads(data)
|
||||
d["room_name"] = room_name
|
||||
|
||||
room_members = (
|
||||
notifier.get_members(room_name)
|
||||
if notifier.get_members(room_name) is not None
|
||||
else []
|
||||
)
|
||||
|
||||
if websocket not in room_members:
|
||||
print("Sender not in room member: Reconnecting...")
|
||||
await notifier.connect(websocket, room_name)
|
||||
print("ENDPOINT", data)
|
||||
await notifier._notify(data, room_name)
|
||||
|
||||
except WebSocketDisconnect:
|
||||
notifier.remove(websocket, room_name)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
from base64 import urlsafe_b64encode
|
||||
from http import HTTPStatus
|
||||
from typing import List
|
||||
from typing import List, Union
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import Request
|
||||
from fastapi.param_functions import Query
|
||||
from fastapi.param_functions import Body, Query
|
||||
from fastapi.params import Depends
|
||||
from loguru import logger
|
||||
from secp256k1 import PrivateKey, PublicKey
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from lnbits.core.crud import get_user
|
||||
|
|
@ -33,6 +34,9 @@ from .crud import (
|
|||
delete_diagonalley_product,
|
||||
delete_diagonalley_stall,
|
||||
delete_diagonalley_zone,
|
||||
get_diagonalley_chat_by_merchant,
|
||||
get_diagonalley_chat_messages,
|
||||
get_diagonalley_latest_chat_messages,
|
||||
get_diagonalley_market,
|
||||
get_diagonalley_market_stalls,
|
||||
get_diagonalley_markets,
|
||||
|
|
@ -47,6 +51,7 @@ from .crud import (
|
|||
get_diagonalley_stalls_by_ids,
|
||||
get_diagonalley_zone,
|
||||
get_diagonalley_zones,
|
||||
set_diagonalley_order_pubkey,
|
||||
update_diagonalley_market,
|
||||
update_diagonalley_product,
|
||||
update_diagonalley_stall,
|
||||
|
|
@ -198,7 +203,6 @@ async def api_diagonalley_stall_create(
|
|||
|
||||
if stall_id:
|
||||
stall = await get_diagonalley_stall(stall_id)
|
||||
print("ID", stall_id)
|
||||
if not stall:
|
||||
return {"message": "Withdraw stall does not exist."}
|
||||
|
||||
|
|
@ -252,6 +256,14 @@ async def api_diagonalley_orders(
|
|||
return {"message": "We could not retrieve the orders."}
|
||||
|
||||
|
||||
@diagonalley_ext.get("/api/v1/orders/{order_id}")
|
||||
async def api_diagonalley_order_by_id(order_id: str):
|
||||
order = (await get_diagonalley_order(order_id)).dict()
|
||||
order["details"] = await get_diagonalley_order_details(order_id)
|
||||
|
||||
return order
|
||||
|
||||
|
||||
@diagonalley_ext.post("/api/v1/orders")
|
||||
async def api_diagonalley_order_create(data: createOrder):
|
||||
ref = urlsafe_short_hash()
|
||||
|
|
@ -274,8 +286,6 @@ async def api_diagonalley_order_create(data: createOrder):
|
|||
"payment_request": payment_request,
|
||||
"order_reference": ref,
|
||||
}
|
||||
# order = await create_diagonalley_order(wallet_id=wallet.wallet.id, data=data)
|
||||
# return order.dict()
|
||||
|
||||
|
||||
@diagonalley_ext.get("/api/v1/orders/payments/{payment_hash}")
|
||||
|
|
@ -296,7 +306,7 @@ async def api_diagonalley_check_payment(payment_hash: str):
|
|||
|
||||
@diagonalley_ext.delete("/api/v1/orders/{order_id}")
|
||||
async def api_diagonalley_order_delete(
|
||||
order_id: str, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
order_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
):
|
||||
order = await get_diagonalley_order(order_id)
|
||||
|
||||
|
|
@ -340,7 +350,7 @@ async def api_diagonalley_order_shipped(
|
|||
"SELECT * FROM diagonalley.orders WHERE id = ?", (order_id,)
|
||||
)
|
||||
|
||||
return [order.dict() for order in get_diagonalley_orders(order["wallet"])]
|
||||
return order
|
||||
|
||||
|
||||
###List products based on stall id
|
||||
|
|
@ -354,7 +364,6 @@ async def api_diagonalley_stall_products(
|
|||
rows = await db.fetchone(
|
||||
"SELECT * FROM diagonalley.stalls WHERE id = ?", (stall_id,)
|
||||
)
|
||||
print(rows[1])
|
||||
if not rows:
|
||||
return {"message": "Stall does not exist."}
|
||||
|
||||
|
|
@ -383,44 +392,44 @@ async def api_diagonalley_stall_checkshipped(
|
|||
###Place order
|
||||
|
||||
|
||||
@diagonalley_ext.post("/api/v1/stall/order/{stall_id}")
|
||||
async def api_diagonalley_stall_order(
|
||||
stall_id, data: createOrder, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
):
|
||||
product = await get_diagonalley_product(data.productid)
|
||||
shipping = await get_diagonalley_stall(stall_id)
|
||||
# @diagonalley_ext.post("/api/v1/stall/order/{stall_id}")
|
||||
# async def api_diagonalley_stall_order(
|
||||
# stall_id, data: createOrder, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
# ):
|
||||
# product = await get_diagonalley_product(data.productid)
|
||||
# shipping = await get_diagonalley_stall(stall_id)
|
||||
|
||||
if data.shippingzone == 1:
|
||||
shippingcost = shipping.zone1cost # missing in model
|
||||
else:
|
||||
shippingcost = shipping.zone2cost # missing in model
|
||||
# if data.shippingzone == 1:
|
||||
# shippingcost = shipping.zone1cost # missing in model
|
||||
# else:
|
||||
# shippingcost = shipping.zone2cost # missing in model
|
||||
|
||||
checking_id, payment_request = await create_invoice(
|
||||
wallet_id=product.wallet,
|
||||
amount=shippingcost + (data.quantity * product.price),
|
||||
memo=shipping.wallet,
|
||||
)
|
||||
selling_id = urlsafe_b64encode(uuid4().bytes_le).decode("utf-8")
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO diagonalley.orders (id, productid, wallet, product, quantity, shippingzone, address, email, invoiceid, paid, shipped)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
selling_id,
|
||||
data.productid,
|
||||
product.wallet, # doesn't exist in model
|
||||
product.product,
|
||||
data.quantity,
|
||||
data.shippingzone,
|
||||
data.address,
|
||||
data.email,
|
||||
checking_id,
|
||||
False,
|
||||
False,
|
||||
),
|
||||
)
|
||||
return {"checking_id": checking_id, "payment_request": payment_request}
|
||||
# checking_id, payment_request = await create_invoice(
|
||||
# wallet_id=product.wallet,
|
||||
# amount=shippingcost + (data.quantity * product.price),
|
||||
# memo=shipping.wallet,
|
||||
# )
|
||||
# selling_id = urlsafe_b64encode(uuid4().bytes_le).decode("utf-8")
|
||||
# await db.execute(
|
||||
# """
|
||||
# INSERT INTO diagonalley.orders (id, productid, wallet, product, quantity, shippingzone, address, email, invoiceid, paid, shipped)
|
||||
# VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
# """,
|
||||
# (
|
||||
# selling_id,
|
||||
# data.productid,
|
||||
# product.wallet, # doesn't exist in model
|
||||
# product.product,
|
||||
# data.quantity,
|
||||
# data.shippingzone,
|
||||
# data.address,
|
||||
# data.email,
|
||||
# checking_id,
|
||||
# False,
|
||||
# False,
|
||||
# ),
|
||||
# )
|
||||
# return {"checking_id": checking_id, "payment_request": payment_request}
|
||||
|
||||
|
||||
##
|
||||
|
|
@ -467,3 +476,42 @@ async def api_diagonalley_stall_create(
|
|||
await create_diagonalley_market_stalls(market_id=market.id, data=data.stalls)
|
||||
|
||||
return market.dict()
|
||||
|
||||
|
||||
## KEYS
|
||||
|
||||
|
||||
@diagonalley_ext.get("/api/v1/keys/{payment_hash}")
|
||||
async def api_diagonalley_generate_keys(payment_hash: str):
|
||||
private_key = PrivateKey()
|
||||
public_key = private_key.pubkey.serialize().hex()
|
||||
while not public_key.startswith("02"):
|
||||
private_key = PrivateKey()
|
||||
public_key = private_key.pubkey.serialize().hex()
|
||||
|
||||
# set pubkey in order
|
||||
await set_diagonalley_order_pubkey(payment_hash, pubkey=public_key[2:])
|
||||
return {"privkey": private_key.serialize(), "pubkey": public_key[2:]}
|
||||
|
||||
|
||||
## MESSAGES/CHAT
|
||||
|
||||
|
||||
@diagonalley_ext.get("/api/v1/chat/messages/merchant")
|
||||
async def api_get_merchant_messages(
|
||||
orders: str = Query(...), wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
):
|
||||
|
||||
return [
|
||||
msg.dict() for msg in await get_diagonalley_chat_by_merchant(orders.split(","))
|
||||
]
|
||||
|
||||
|
||||
@diagonalley_ext.get("/api/v1/chat/messages/{room_name}")
|
||||
async def api_get_latest_chat_msg(room_name: str, all_messages: bool = Query(False)):
|
||||
if all_messages:
|
||||
messages = await get_diagonalley_chat_messages(room_name)
|
||||
else:
|
||||
messages = await get_diagonalley_latest_chat_messages(room_name)
|
||||
|
||||
return messages
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,10 @@ async def create_lnurldevice(
|
|||
wallet,
|
||||
currency,
|
||||
device,
|
||||
profit
|
||||
profit,
|
||||
amount
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
lnurldevice_id,
|
||||
|
|
@ -34,6 +35,7 @@ async def create_lnurldevice(
|
|||
data.currency,
|
||||
data.device,
|
||||
data.profit,
|
||||
data.amount,
|
||||
),
|
||||
)
|
||||
return await get_lnurldevice(lnurldevice_id)
|
||||
|
|
|
|||
|
|
@ -102,7 +102,32 @@ async def lnurl_v1_params(
|
|||
if device.device == "atm":
|
||||
if paymentcheck:
|
||||
return {"status": "ERROR", "reason": f"Payment already claimed"}
|
||||
if device.device == "switch":
|
||||
|
||||
price_msat = (
|
||||
await fiat_amount_as_satoshis(float(device.profit), device.currency)
|
||||
if device.currency != "sat"
|
||||
else amount_in_cent
|
||||
) * 1000
|
||||
|
||||
lnurldevicepayment = await create_lnurldevicepayment(
|
||||
deviceid=device.id,
|
||||
payload="bla",
|
||||
sats=price_msat,
|
||||
pin=1,
|
||||
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": await device.lnurlpay_metadata(),
|
||||
}
|
||||
if len(p) % 4 > 0:
|
||||
p += "=" * (4 - (len(p) % 4))
|
||||
|
||||
|
|
@ -184,22 +209,42 @@ 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.id != k1:
|
||||
return {"status": "ERROR", "reason": "Bad K1"}
|
||||
if lnurldevicepayment.payhash != "payment_hash":
|
||||
return {"status": "ERROR", "reason": f"Payment already claimed"}
|
||||
lnurldevicepayment = await update_lnurldevicepayment(
|
||||
lnurldevicepayment_id=paymentid, payhash=lnurldevicepayment.payload
|
||||
)
|
||||
|
||||
await pay_invoice(
|
||||
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=lnurldevicepayment.sats / 1000,
|
||||
memo=device.title + "-" + lnurldevicepayment.id,
|
||||
unhashed_description=(await device.lnurlpay_metadata()).encode("utf-8"),
|
||||
extra={"tag": "Switch", "id": paymentid, "time": device.amount},
|
||||
)
|
||||
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,
|
||||
|
|
@ -221,5 +266,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,12 @@ 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;"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ class createLnurldevice(BaseModel):
|
|||
currency: str
|
||||
device: str
|
||||
profit: float
|
||||
amount: int
|
||||
|
||||
|
||||
class lnurldevices(BaseModel):
|
||||
|
|
@ -27,15 +28,14 @@ class lnurldevices(BaseModel):
|
|||
currency: str
|
||||
device: str
|
||||
profit: float
|
||||
amount: 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
|
||||
)
|
||||
url = req.url_for("lnurldevice.lnurl_v1_params", device_id=self.id)
|
||||
return lnurl_encode(url)
|
||||
|
||||
async def lnurlpay_metadata(self) -> LnurlPayMetadata:
|
||||
|
|
|
|||
40
lnbits/extensions/lnurldevice/tasks.py
Normal file
40
lnbits/extensions/lnurldevice/tasks.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
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)
|
||||
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 LNURL </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,6 +229,29 @@
|
|||
max="90"
|
||||
label="Profit margin (% added to invoices/deducted from faucets)"
|
||||
></q-input>
|
||||
<div v-else>
|
||||
<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>
|
||||
<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="row q-mt-lg">
|
||||
<q-btn
|
||||
|
|
@ -225,6 +279,33 @@
|
|||
</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="qrCodeDialog.data.url + '/?lightning=' + qrCodeDialog.data.lnurl"
|
||||
:options="{width: 800}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
{% raw %}
|
||||
</q-responsive>
|
||||
<p style="word-break: break-all">
|
||||
<strong>ID:</strong> {{ qrCodeDialog.data.id }}<br />
|
||||
</p>
|
||||
{% endraw %}
|
||||
<div class="row q-mt-lg q-gutter-sm">
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
@click="copyText(qrCodeDialog.data.lnurl, 'LNURL copied to clipboard!')"
|
||||
class="q-ml-sm"
|
||||
>Copy LNURL</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,7 +333,9 @@
|
|||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
protocol: window.location.protocol,
|
||||
location: window.location.hostname,
|
||||
wslocation: window.location.hostname,
|
||||
filter: '',
|
||||
currency: 'USD',
|
||||
lnurldeviceLinks: [],
|
||||
|
|
@ -265,6 +348,10 @@
|
|||
{
|
||||
label: 'ATM',
|
||||
value: 'atm'
|
||||
},
|
||||
{
|
||||
label: 'Switch',
|
||||
value: 'switch'
|
||||
}
|
||||
],
|
||||
lnurldevicesTable: {
|
||||
|
|
@ -333,7 +420,8 @@
|
|||
show_ack: false,
|
||||
show_price: 'None',
|
||||
device: 'pos',
|
||||
profit: 2,
|
||||
profit: 0,
|
||||
amount: 1,
|
||||
title: ''
|
||||
}
|
||||
},
|
||||
|
|
@ -344,6 +432,16 @@
|
|||
}
|
||||
},
|
||||
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.qrCodeDialog.show = true
|
||||
},
|
||||
cancellnurldevice: function (data) {
|
||||
var self = this
|
||||
self.formDialoglnurldevice.show = false
|
||||
|
|
@ -400,6 +498,7 @@
|
|||
.then(function (response) {
|
||||
if (response.data) {
|
||||
self.lnurldeviceLinks = response.data.map(maplnurldevice)
|
||||
console.log(response.data)
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
|
|
@ -519,6 +618,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,58 @@ 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 = await get_lnurldevice(lnurldevice_id)
|
||||
if not lnurldevice:
|
||||
return
|
||||
await manager.send_personal_message(f"{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(), **{"lnurl": lnurldevice.lnurl(req)}}
|
||||
else:
|
||||
lnurldevice = await update_lnurldevice(data, lnurldevice_id=lnurldevice_id)
|
||||
return lnurldevice.dict()
|
||||
return {**lnurldevice.dict(), **{"lnurl": lnurldevice.lnurl(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(), **{"lnurl": lnurldevice.lnurl(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(), **{"lnurl": lnurldevice.lnurl(req)}}
|
||||
|
||||
|
||||
@lnurldevice_ext.delete("/api/v1/lnurlpos/{lnurldevice_id}")
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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##########################
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ class Target(BaseModel):
|
|||
class TargetPutList(BaseModel):
|
||||
wallet: str = Query(...)
|
||||
alias: str = Query("")
|
||||
percent: float = Query(..., ge=0.01)
|
||||
percent: float = Query(..., ge=0.01, lt=100)
|
||||
|
||||
|
||||
class TargetPut(BaseModel):
|
||||
|
|
|
|||
|
|
@ -1,20 +1,18 @@
|
|||
import asyncio
|
||||
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_queue, 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_targets
|
||||
|
||||
|
||||
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()
|
||||
|
|
@ -22,59 +20,36 @@ async def wait_for_paid_invoices():
|
|||
|
||||
|
||||
async def on_invoice_paid(payment: Payment) -> None:
|
||||
if payment.extra.get("tag") == "splitpayments" or payment.extra.get("splitted"):
|
||||
# already splitted, ignore
|
||||
if payment.extra.get("tag") == "splitpayments":
|
||||
# already a splitted payment, ignore
|
||||
return
|
||||
|
||||
# now we make some special internal transfers (from no one to the receiver)
|
||||
targets = await get_targets(payment.wallet_id)
|
||||
transfers = [
|
||||
(target.wallet, int(target.percent * payment.amount / 100))
|
||||
for target in targets
|
||||
]
|
||||
transfers = [(wallet, amount) for wallet, amount in transfers if amount > 0]
|
||||
amount_left = payment.amount - sum([amount for _, amount in transfers])
|
||||
|
||||
if amount_left < 0:
|
||||
logger.error(
|
||||
"splitpayments failure: amount_left is negative.", payment.payment_hash
|
||||
)
|
||||
return
|
||||
|
||||
if not targets:
|
||||
return
|
||||
|
||||
# mark the original payment with one extra key, "splitted"
|
||||
# (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, splitted=True)),
|
||||
amount_left,
|
||||
payment.payment_hash,
|
||||
),
|
||||
)
|
||||
total_percent = sum([target.percent for target in targets])
|
||||
|
||||
# perform the internal transfer using the same payment_hash
|
||||
for wallet, amount in transfers:
|
||||
internal_checking_id = f"internal_{urlsafe_short_hash()}"
|
||||
await create_payment(
|
||||
wallet_id=wallet,
|
||||
checking_id=internal_checking_id,
|
||||
payment_request="",
|
||||
payment_hash=payment.payment_hash,
|
||||
amount=amount,
|
||||
memo=payment.memo,
|
||||
pending=False,
|
||||
if total_percent > 100:
|
||||
logger.error("splitpayment failure: total percent adds up to more than 100%")
|
||||
return
|
||||
|
||||
logger.debug(f"performing split payments to {len(targets)} targets")
|
||||
for target in targets:
|
||||
amount = int(payment.amount * target.percent / 100) # msats
|
||||
payment_hash, payment_request = await create_invoice(
|
||||
wallet_id=target.wallet,
|
||||
amount=int(amount / 1000), # sats
|
||||
internal=True,
|
||||
memo=f"split payment: {target.percent}% for {target.alias or target.wallet}",
|
||||
extra={"tag": "splitpayments"},
|
||||
)
|
||||
logger.debug(f"created split invoice: {payment_hash}")
|
||||
|
||||
# manually send this for now
|
||||
await internal_invoice_queue.put(internal_checking_id)
|
||||
return
|
||||
checking_id = await pay_invoice(
|
||||
payment_request=payment_request,
|
||||
wallet_id=payment.wallet_id,
|
||||
extra={"tag": "splitpayments"},
|
||||
)
|
||||
logger.debug(f"paid split invoice: {checking_id}")
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ async def m001_initial(db):
|
|||
name TEXT NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
cur_code TEXT NOT NULL,
|
||||
sats INT NOT NULL,
|
||||
sats {db.big_int} NOT NULL,
|
||||
amount FLOAT NOT NULL,
|
||||
service INTEGER NOT NULL,
|
||||
posted BOOLEAN NOT NULL,
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ from typing import List, Optional, Union
|
|||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
from . import db
|
||||
from .models import CreateDomain, Domains, Subdomains
|
||||
from .models import CreateDomain, CreateSubdomain, Domains, Subdomains
|
||||
|
||||
|
||||
async def create_subdomain(payment_hash, wallet, data: CreateDomain) -> Subdomains:
|
||||
async def create_subdomain(payment_hash, wallet, data: CreateSubdomain) -> Subdomains:
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO subdomains.subdomain (id, domain, email, subdomain, ip, wallet, sats, duration, paid, record_type)
|
||||
|
|
|
|||
|
|
@ -3,24 +3,24 @@ from pydantic.main import BaseModel
|
|||
|
||||
|
||||
class CreateDomain(BaseModel):
|
||||
wallet: str = Query(...)
|
||||
domain: str = Query(...)
|
||||
cf_token: str = Query(...)
|
||||
cf_zone_id: str = Query(...)
|
||||
webhook: str = Query("")
|
||||
description: str = Query(..., min_length=0)
|
||||
cost: int = Query(..., ge=0)
|
||||
allowed_record_types: str = Query(...)
|
||||
wallet: str = Query(...) # type: ignore
|
||||
domain: str = Query(...) # type: ignore
|
||||
cf_token: str = Query(...) # type: ignore
|
||||
cf_zone_id: str = Query(...) # type: ignore
|
||||
webhook: str = Query("") # type: ignore
|
||||
description: str = Query(..., min_length=0) # type: ignore
|
||||
cost: int = Query(..., ge=0) # type: ignore
|
||||
allowed_record_types: str = Query(...) # type: ignore
|
||||
|
||||
|
||||
class CreateSubdomain(BaseModel):
|
||||
domain: str = Query(...)
|
||||
subdomain: str = Query(...)
|
||||
email: str = Query(...)
|
||||
ip: str = Query(...)
|
||||
sats: int = Query(..., ge=0)
|
||||
duration: int = Query(...)
|
||||
record_type: str = Query(...)
|
||||
domain: str = Query(...) # type: ignore
|
||||
subdomain: str = Query(...) # type: ignore
|
||||
email: str = Query(...) # type: ignore
|
||||
ip: str = Query(...) # type: ignore
|
||||
sats: int = Query(..., ge=0) # type: ignore
|
||||
duration: int = Query(...) # type: ignore
|
||||
record_type: str = Query(...) # type: ignore
|
||||
|
||||
|
||||
class Domains(BaseModel):
|
||||
|
|
|
|||
|
|
@ -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 .cloudflare import cloudflare_create_subdomain
|
||||
|
|
@ -11,7 +12,7 @@ from .crud import get_domain, set_subdomain_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()
|
||||
|
|
@ -19,7 +20,7 @@ async def wait_for_paid_invoices():
|
|||
|
||||
|
||||
async def on_invoice_paid(payment: Payment) -> None:
|
||||
if payment.extra.get("tag") != "lnsubdomain":
|
||||
if not payment.extra or payment.extra.get("tag") != "lnsubdomain":
|
||||
# not an lnurlp invoice
|
||||
return
|
||||
|
||||
|
|
@ -36,7 +37,7 @@ async def on_invoice_paid(payment: Payment) -> None:
|
|||
)
|
||||
|
||||
### Use webhook to notify about cloudflare registration
|
||||
if domain.webhook:
|
||||
if domain and domain.webhook:
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
r = await client.post(
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@ templates = Jinja2Templates(directory="templates")
|
|||
|
||||
|
||||
@subdomains_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 subdomains_renderer().TemplateResponse(
|
||||
"subdomains/index.html", {"request": request, "user": user.dict()}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -29,12 +29,15 @@ from .crud import (
|
|||
|
||||
@subdomains_ext.get("/api/v1/domains")
|
||||
async def api_domains(
|
||||
g: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False)
|
||||
g: WalletTypeInfo = Depends(get_key_type), # type: ignore
|
||||
all_wallets: bool = Query(False),
|
||||
):
|
||||
wallet_ids = [g.wallet.id]
|
||||
|
||||
if all_wallets:
|
||||
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
|
||||
user = await get_user(g.wallet.user)
|
||||
if user is not None:
|
||||
wallet_ids = user.wallet_ids
|
||||
|
||||
return [domain.dict() for domain in await get_domains(wallet_ids)]
|
||||
|
||||
|
|
@ -42,7 +45,9 @@ async def api_domains(
|
|||
@subdomains_ext.post("/api/v1/domains")
|
||||
@subdomains_ext.put("/api/v1/domains/{domain_id}")
|
||||
async def api_domain_create(
|
||||
data: CreateDomain, domain_id=None, g: WalletTypeInfo = Depends(get_key_type)
|
||||
data: CreateDomain,
|
||||
domain_id=None,
|
||||
g: WalletTypeInfo = Depends(get_key_type), # type: ignore
|
||||
):
|
||||
if domain_id:
|
||||
domain = await get_domain(domain_id)
|
||||
|
|
@ -63,7 +68,9 @@ async def api_domain_create(
|
|||
|
||||
|
||||
@subdomains_ext.delete("/api/v1/domains/{domain_id}")
|
||||
async def api_domain_delete(domain_id, g: WalletTypeInfo = Depends(get_key_type)):
|
||||
async def api_domain_delete(
|
||||
domain_id, g: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||
):
|
||||
domain = await get_domain(domain_id)
|
||||
|
||||
if not domain:
|
||||
|
|
@ -82,12 +89,14 @@ async def api_domain_delete(domain_id, g: WalletTypeInfo = Depends(get_key_type)
|
|||
|
||||
@subdomains_ext.get("/api/v1/subdomains")
|
||||
async def api_subdomains(
|
||||
all_wallets: bool = Query(False), g: WalletTypeInfo = Depends(get_key_type)
|
||||
all_wallets: bool = Query(False), g: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||
):
|
||||
wallet_ids = [g.wallet.id]
|
||||
|
||||
if all_wallets:
|
||||
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
|
||||
user = await get_user(g.wallet.user)
|
||||
if user is not None:
|
||||
wallet_ids = user.wallet_ids
|
||||
|
||||
return [domain.dict() for domain in await get_subdomains(wallet_ids)]
|
||||
|
||||
|
|
@ -173,7 +182,9 @@ async def api_subdomain_send_subdomain(payment_hash):
|
|||
|
||||
|
||||
@subdomains_ext.delete("/api/v1/subdomains/{subdomain_id}")
|
||||
async def api_subdomain_delete(subdomain_id, g: WalletTypeInfo = Depends(get_key_type)):
|
||||
async def api_subdomain_delete(
|
||||
subdomain_id, g: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||
):
|
||||
subdomain = await get_subdomain(subdomain_id)
|
||||
|
||||
if not subdomain:
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ async def m001_initial(db):
|
|||
wallet TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
sats INT NOT NULL,
|
||||
tipjar INT NOT NULL,
|
||||
sats {db.big_int} NOT NULL,
|
||||
tipjar {db.big_int} NOT NULL,
|
||||
FOREIGN KEY(tipjar) REFERENCES {db.references_schema}TipJars(id)
|
||||
);
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
import asyncio
|
||||
import json
|
||||
|
||||
from lnbits.core import db as core_db
|
||||
from lnbits.core.crud import create_payment
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
from lnbits.tasks import internal_invoice_queue, 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_tpos
|
||||
|
||||
|
||||
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()
|
||||
|
|
@ -20,11 +20,9 @@ async def wait_for_paid_invoices():
|
|||
|
||||
|
||||
async def on_invoice_paid(payment: Payment) -> None:
|
||||
if payment.extra.get("tag") == "tpos" and payment.extra.get("tipSplitted"):
|
||||
# already splitted, ignore
|
||||
if payment.extra.get("tag") != "tpos":
|
||||
return
|
||||
|
||||
# now we make some special internal transfers (from no one to the receiver)
|
||||
tpos = await get_tpos(payment.extra.get("tposId"))
|
||||
tipAmount = payment.extra.get("tipAmount")
|
||||
|
||||
|
|
@ -32,39 +30,17 @@ async def on_invoice_paid(payment: Payment) -> None:
|
|||
# no tip amount
|
||||
return
|
||||
|
||||
tipAmount = tipAmount * 1000
|
||||
amount = payment.amount - tipAmount
|
||||
|
||||
# mark the original payment with one extra key, "splitted"
|
||||
# (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, tipSplitted=True)),
|
||||
amount,
|
||||
payment.payment_hash,
|
||||
),
|
||||
)
|
||||
|
||||
# perform the internal transfer using the same payment_hash
|
||||
internal_checking_id = f"internal_{urlsafe_short_hash()}"
|
||||
await create_payment(
|
||||
payment_hash, payment_request = await create_invoice(
|
||||
wallet_id=tpos.tip_wallet,
|
||||
checking_id=internal_checking_id,
|
||||
payment_request="",
|
||||
payment_hash=payment.payment_hash,
|
||||
amount=tipAmount,
|
||||
memo=f"Tip for {payment.memo}",
|
||||
pending=False,
|
||||
extra={"tipSplitted": True},
|
||||
amount=int(tipAmount), # sats
|
||||
internal=True,
|
||||
memo=f"tpos tip",
|
||||
)
|
||||
logger.debug(f"tpos: tip invoice created: {payment_hash}")
|
||||
|
||||
# manually send this for now
|
||||
await internal_invoice_queue.put(internal_checking_id)
|
||||
return
|
||||
checking_id = await pay_invoice(
|
||||
payment_request=payment_request,
|
||||
wallet_id=payment.wallet_id,
|
||||
extra={"tag": "tpos"},
|
||||
)
|
||||
logger.debug(f"tpos: tip invoice paid: {checking_id}")
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from .models import Address, Config, WalletAccount
|
|||
##########################WALLETS####################
|
||||
|
||||
|
||||
async def create_watch_wallet(w: WalletAccount) -> WalletAccount:
|
||||
async def create_watch_wallet(user: str, w: WalletAccount) -> WalletAccount:
|
||||
wallet_id = urlsafe_short_hash()
|
||||
await db.execute(
|
||||
"""
|
||||
|
|
@ -30,7 +30,7 @@ async def create_watch_wallet(w: WalletAccount) -> WalletAccount:
|
|||
""",
|
||||
(
|
||||
wallet_id,
|
||||
w.user,
|
||||
user,
|
||||
w.masterpub,
|
||||
w.fingerprint,
|
||||
w.title,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ class CreateWallet(BaseModel):
|
|||
|
||||
class WalletAccount(BaseModel):
|
||||
id: str
|
||||
user: str
|
||||
masterpub: str
|
||||
fingerprint: str
|
||||
title: str
|
||||
|
|
@ -80,6 +79,7 @@ class CreatePsbt(BaseModel):
|
|||
class ExtractPsbt(BaseModel):
|
||||
psbtBase64 = "" # // todo snake case
|
||||
inputs: List[TransactionInput]
|
||||
network = "Mainnet"
|
||||
|
||||
|
||||
class SignedTransaction(BaseModel):
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<send-to
|
||||
:data.sync="sendToList"
|
||||
:fee-rate="feeRate"
|
||||
:tx-size="txSizeNoChange"
|
||||
:tx-size="txSize"
|
||||
:selected-amount="selectedAmount"
|
||||
:sats-denominated="satsDenominated"
|
||||
@update:outputs="handleOutputsChange"
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ async function payment(path) {
|
|||
'mempool-endpoint',
|
||||
'sats-denominated',
|
||||
'serial-signer-ref',
|
||||
'adminkey'
|
||||
'adminkey',
|
||||
'network'
|
||||
],
|
||||
watch: {
|
||||
immediate: true,
|
||||
|
|
@ -279,7 +280,8 @@ async function payment(path) {
|
|||
this.adminkey,
|
||||
{
|
||||
psbtBase64,
|
||||
inputs: this.tx.inputs
|
||||
inputs: this.tx.inputs,
|
||||
network: this.network
|
||||
}
|
||||
)
|
||||
return data
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@
|
|||
<q-item
|
||||
v-for="device in pairedDevices"
|
||||
:key="device.id"
|
||||
v-if="!selectedPort"
|
||||
v-if="!selectedPort && showPairedDevices"
|
||||
clickable
|
||||
v-close-popup
|
||||
>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ async function serialSigner(path) {
|
|||
receivedData: '',
|
||||
config: {},
|
||||
decryptionKey: null,
|
||||
sharedSecret: null, // todo: store in secure local storage
|
||||
sharedSecret: null,
|
||||
|
||||
hww: {
|
||||
password: null,
|
||||
|
|
@ -51,12 +51,14 @@ async function serialSigner(path) {
|
|||
},
|
||||
tx: null, // todo: move to hww
|
||||
|
||||
showConsole: false
|
||||
showConsole: false,
|
||||
showPairedDevices: true
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
pairedDevices: {
|
||||
cache: false,
|
||||
get: function () {
|
||||
return (
|
||||
JSON.parse(window.localStorage.getItem('lnbits-paired-devices')) ||
|
||||
|
|
@ -109,7 +111,10 @@ async function serialSigner(path) {
|
|||
|
||||
// Wait for the serial port to open.
|
||||
await this.selectedPort.open(config)
|
||||
// do not await
|
||||
this.startSerialPortReading()
|
||||
// wait to init
|
||||
sleep(1000)
|
||||
|
||||
const textEncoder = new TextEncoderStream()
|
||||
this.writableStreamClosed = textEncoder.readable.pipeTo(
|
||||
|
|
@ -225,8 +230,9 @@ async function serialSigner(path) {
|
|||
while (true) {
|
||||
const {value, done} = await readStringUntil('\n')
|
||||
if (value) {
|
||||
this.handleSerialPortResponse(value)
|
||||
this.updateSerialPortConsole(value)
|
||||
const {command, commandData} = await this.extractCommand(value)
|
||||
this.handleSerialPortResponse(command, commandData)
|
||||
this.updateSerialPortConsole(command)
|
||||
}
|
||||
if (done) return
|
||||
}
|
||||
|
|
@ -240,8 +246,7 @@ async function serialSigner(path) {
|
|||
}
|
||||
}
|
||||
},
|
||||
handleSerialPortResponse: async function (value) {
|
||||
const {command, commandData} = await this.extractCommand(value)
|
||||
handleSerialPortResponse: async function (command, commandData) {
|
||||
this.logPublicCommandsResponse(command, commandData)
|
||||
|
||||
switch (command) {
|
||||
|
|
@ -282,7 +287,7 @@ async function serialSigner(path) {
|
|||
)
|
||||
break
|
||||
default:
|
||||
console.log(` %c${value}`, 'background: #222; color: red')
|
||||
console.log(` %c${command}`, 'background: #222; color: red')
|
||||
}
|
||||
},
|
||||
logPublicCommandsResponse: function (command, commandData) {
|
||||
|
|
@ -307,6 +312,8 @@ async function serialSigner(path) {
|
|||
},
|
||||
hwwPing: async function () {
|
||||
try {
|
||||
// Send an empty ping. The serial port buffer might have some jubk data. Flush it.
|
||||
await this.sendCommandClearText(COMMAND_PING)
|
||||
await this.sendCommandClearText(COMMAND_PING, [window.location.host])
|
||||
} catch (error) {
|
||||
this.$q.notify({
|
||||
|
|
@ -582,7 +589,7 @@ async function serialSigner(path) {
|
|||
hwwCheckPairing: async function () {
|
||||
const iv = window.crypto.getRandomValues(new Uint8Array(16))
|
||||
const encrypted = await this.encryptMessage(
|
||||
this.sharedSecret,
|
||||
this.sharedSecret, // todo: revisit
|
||||
iv,
|
||||
PAIRING_CONTROL_TEXT.length + ' ' + PAIRING_CONTROL_TEXT
|
||||
)
|
||||
|
|
@ -603,10 +610,10 @@ async function serialSigner(path) {
|
|||
}
|
||||
},
|
||||
handleCheckPairingResponse: async function (res = '') {
|
||||
const [statusCode, encryptedMessage] = res.split(' ')
|
||||
const [statusCode, message] = res.split(' ')
|
||||
switch (statusCode) {
|
||||
case '0':
|
||||
const controlText = await this.decryptData(encryptedMessage)
|
||||
const controlText = await this.decryptData(message)
|
||||
if (controlText == PAIRING_CONTROL_TEXT) {
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
|
|
@ -622,6 +629,16 @@ async function serialSigner(path) {
|
|||
})
|
||||
}
|
||||
break
|
||||
case '1':
|
||||
this.closeSerialPort()
|
||||
this.$q.notify({
|
||||
type: 'warning',
|
||||
message:
|
||||
'Re-pairing failed. Remove (forget) device and try again!',
|
||||
caption: `Error: ${message}`,
|
||||
timeout: 10000
|
||||
})
|
||||
break
|
||||
default:
|
||||
// noting to do here yet
|
||||
break
|
||||
|
|
@ -746,7 +763,7 @@ async function serialSigner(path) {
|
|||
} catch (error) {
|
||||
this.$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Failed to ask for help!',
|
||||
message: 'Failed to wipe!',
|
||||
caption: `${error}`,
|
||||
timeout: 10000
|
||||
})
|
||||
|
|
@ -862,6 +879,11 @@ async function serialSigner(path) {
|
|||
sendCommandSecure: async function (command, attrs = []) {
|
||||
const message = [command].concat(attrs).join(' ')
|
||||
const iv = window.crypto.getRandomValues(new Uint8Array(16))
|
||||
if (!this.sharedSecret || !this.sharedSecret.length) {
|
||||
throw new Error(
|
||||
`Secure connection not estabileshed. Tried to run command: ${command}`
|
||||
)
|
||||
}
|
||||
const encrypted = await this.encryptMessage(
|
||||
this.sharedSecret,
|
||||
iv,
|
||||
|
|
@ -901,6 +923,7 @@ async function serialSigner(path) {
|
|||
},
|
||||
decryptData: async function (value) {
|
||||
if (!this.sharedSecret) {
|
||||
console.log('/error Secure session not established!')
|
||||
return '/error Secure session not established!'
|
||||
}
|
||||
try {
|
||||
|
|
@ -921,6 +944,7 @@ async function serialSigner(path) {
|
|||
.trim()
|
||||
return command
|
||||
} catch (error) {
|
||||
console.log('/error Failed to decrypt message from device!')
|
||||
return '/error Failed to decrypt message from device!'
|
||||
}
|
||||
},
|
||||
|
|
@ -949,6 +973,11 @@ async function serialSigner(path) {
|
|||
devices.splice(deviceIndex, 1)
|
||||
}
|
||||
this.pairedDevices = devices
|
||||
this.showPairedDevices = false
|
||||
setTimeout(() => {
|
||||
// force UI refresh
|
||||
this.showPairedDevices = true
|
||||
})
|
||||
},
|
||||
addPairedDevice: function (deviceId, sharedSecretHex, config) {
|
||||
const devices = this.pairedDevices
|
||||
|
|
@ -960,6 +989,11 @@ async function serialSigner(path) {
|
|||
config
|
||||
})
|
||||
this.pairedDevices = devices
|
||||
this.showPairedDevices = false
|
||||
setTimeout(() => {
|
||||
// force UI refresh
|
||||
this.showPairedDevices = true
|
||||
})
|
||||
},
|
||||
updatePairedDeviceConfig(deviceId, config) {
|
||||
const device = this.getPairedDevice(deviceId)
|
||||
|
|
|
|||
|
|
@ -18,9 +18,21 @@
|
|||
>directly from browser</a
|
||||
>
|
||||
<small>
|
||||
<br />Created by,
|
||||
<br />Created by
|
||||
<a target="_blank" style="color: unset" href="https://github.com/arcbtc"
|
||||
>Ben Arc</a
|
||||
>,
|
||||
<a
|
||||
target="_blank"
|
||||
style="color: unset"
|
||||
href="https://github.com/talvasconcelos"
|
||||
>Tiago Vasconcelos</a
|
||||
>,
|
||||
<a
|
||||
target="_blank"
|
||||
style="color: unset"
|
||||
href="https://github.com/motorina0"
|
||||
>motorina0</a
|
||||
>
|
||||
(using,
|
||||
<a
|
||||
|
|
|
|||
|
|
@ -136,6 +136,7 @@
|
|||
:adminkey="g.user.wallets[0].adminkey"
|
||||
:serial-signer-ref="$refs.serialSigner"
|
||||
:sats-denominated="config.sats_denominated"
|
||||
:network="config.network"
|
||||
@broadcast-done="handleBroadcastSuccess"
|
||||
></payment>
|
||||
<!-- todo: no more utxos.data -->
|
||||
|
|
@ -149,7 +150,7 @@
|
|||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-my-none">
|
||||
{{SITE_TITLE}} Onchain Wallet (watch-only) Extension
|
||||
<small>(v0.2)</small>
|
||||
<small>(v0.3)</small>
|
||||
</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from http import HTTPStatus
|
|||
import httpx
|
||||
from embit import finalizer, script
|
||||
from embit.ec import PublicKey
|
||||
from embit.networks import NETWORKS
|
||||
from embit.psbt import PSBT, DerivationPath
|
||||
from embit.transaction import Transaction, TransactionInput, TransactionOutput
|
||||
from fastapi import Query, Request
|
||||
|
|
@ -85,7 +86,6 @@ async def api_wallet_create_or_update(
|
|||
|
||||
new_wallet = WalletAccount(
|
||||
id="none",
|
||||
user=w.wallet.user,
|
||||
masterpub=data.masterpub,
|
||||
fingerprint=descriptor.keys[0].fingerprint.hex(),
|
||||
type=descriptor.scriptpubkey_type(),
|
||||
|
|
@ -114,7 +114,7 @@ async def api_wallet_create_or_update(
|
|||
)
|
||||
)
|
||||
|
||||
wallet = await create_watch_wallet(new_wallet)
|
||||
wallet = await create_watch_wallet(w.wallet.user, new_wallet)
|
||||
|
||||
await api_get_addresses(wallet.id, w)
|
||||
except Exception as e:
|
||||
|
|
@ -295,6 +295,7 @@ async def api_psbt_create(
|
|||
async def api_psbt_extract_tx(
|
||||
data: ExtractPsbt, w: WalletTypeInfo = Depends(require_admin_key)
|
||||
):
|
||||
network = NETWORKS["main"] if data.network == "Mainnet" else NETWORKS["test"]
|
||||
res = SignedTransaction()
|
||||
try:
|
||||
psbt = PSBT.from_base64(data.psbtBase64)
|
||||
|
|
@ -316,7 +317,7 @@ async def api_psbt_extract_tx(
|
|||
|
||||
for out in transaction.vout:
|
||||
tx["outputs"].append(
|
||||
{"amount": out.value, "address": out.script_pubkey.address()}
|
||||
{"amount": out.value, "address": out.script_pubkey.address(network)}
|
||||
)
|
||||
res.tx_json = json.dumps(tx)
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -183,3 +183,26 @@ def template_renderer(additional_folders: List = []) -> Jinja2Templates:
|
|||
t.env.globals["VENDORED_CSS"] = ["/static/bundle.css"]
|
||||
|
||||
return t
|
||||
|
||||
|
||||
def get_current_extension_name() -> str:
|
||||
"""
|
||||
Returns the name of the extension that calls this method.
|
||||
"""
|
||||
import inspect
|
||||
import json
|
||||
import os
|
||||
|
||||
callee_filepath = inspect.stack()[1].filename
|
||||
callee_dirname, callee_filename = os.path.split(callee_filepath)
|
||||
|
||||
path = os.path.normpath(callee_dirname)
|
||||
extension_director_name = path.split(os.sep)[-1]
|
||||
try:
|
||||
config_path = os.path.join(callee_dirname, "config.json")
|
||||
with open(config_path) as json_file:
|
||||
config = json.load(json_file)
|
||||
ext_name = config["name"]
|
||||
except:
|
||||
ext_name = extension_director_name
|
||||
return ext_name
|
||||
|
|
|
|||
|
|
@ -1,95 +0,0 @@
|
|||
from functools import partial
|
||||
from typing import Callable, List, Optional
|
||||
from urllib.parse import urlparse
|
||||
from urllib.request import parse_http_list as _parse_list_header
|
||||
|
||||
from quart import Request
|
||||
from quart_trio.asgi import TrioASGIHTTPConnection
|
||||
from werkzeug.datastructures import Headers
|
||||
|
||||
|
||||
class ASGIProxyFix(TrioASGIHTTPConnection):
|
||||
def _create_request_from_scope(self, send: Callable) -> Request:
|
||||
headers = Headers()
|
||||
headers["Remote-Addr"] = (self.scope.get("client") or ["<local>"])[0]
|
||||
for name, value in self.scope["headers"]:
|
||||
headers.add(name.decode("latin1").title(), value.decode("latin1"))
|
||||
if self.scope["http_version"] < "1.1":
|
||||
headers.setdefault("Host", self.app.config["SERVER_NAME"] or "")
|
||||
|
||||
path = self.scope["path"]
|
||||
path = path if path[0] == "/" else urlparse(path).path
|
||||
|
||||
x_proto = self._get_real_value(1, headers.get("X-Forwarded-Proto"))
|
||||
if x_proto:
|
||||
self.scope["scheme"] = x_proto
|
||||
|
||||
x_host = self._get_real_value(1, headers.get("X-Forwarded-Host"))
|
||||
if x_host:
|
||||
headers["host"] = x_host.lower()
|
||||
|
||||
return self.app.request_class(
|
||||
self.scope["method"],
|
||||
self.scope["scheme"],
|
||||
path,
|
||||
self.scope["query_string"],
|
||||
headers,
|
||||
self.scope.get("root_path", ""),
|
||||
self.scope["http_version"],
|
||||
max_content_length=self.app.config["MAX_CONTENT_LENGTH"],
|
||||
body_timeout=self.app.config["BODY_TIMEOUT"],
|
||||
send_push_promise=partial(self._send_push_promise, send),
|
||||
scope=self.scope,
|
||||
)
|
||||
|
||||
def _get_real_value(self, trusted: int, value: Optional[str]) -> Optional[str]:
|
||||
"""Get the real value from a list header based on the configured
|
||||
number of trusted proxies.
|
||||
:param trusted: Number of values to trust in the header.
|
||||
:param value: Comma separated list header value to parse.
|
||||
:return: The real value, or ``None`` if there are fewer values
|
||||
than the number of trusted proxies.
|
||||
.. versionchanged:: 1.0
|
||||
Renamed from ``_get_trusted_comma``.
|
||||
.. versionadded:: 0.15
|
||||
"""
|
||||
if not (trusted and value):
|
||||
return None
|
||||
|
||||
values = self.parse_list_header(value)
|
||||
if len(values) >= trusted:
|
||||
return values[-trusted]
|
||||
|
||||
return None
|
||||
|
||||
def parse_list_header(self, value: str) -> List[str]:
|
||||
result = []
|
||||
for item in _parse_list_header(value):
|
||||
if item[:1] == item[-1:] == '"':
|
||||
item = self.unquote_header_value(item[1:-1])
|
||||
result.append(item)
|
||||
return result
|
||||
|
||||
def unquote_header_value(self, value: str, is_filename: bool = False) -> str:
|
||||
r"""Unquotes a header value. (Reversal of :func:`quote_header_value`).
|
||||
This does not use the real unquoting but what browsers are actually
|
||||
using for quoting.
|
||||
.. versionadded:: 0.5
|
||||
:param value: the header value to unquote.
|
||||
:param is_filename: The value represents a filename or path.
|
||||
"""
|
||||
if value and value[0] == value[-1] == '"':
|
||||
# this is not the real unquoting, but fixing this so that the
|
||||
# RFC is met will result in bugs with internet explorer and
|
||||
# probably some other browsers as well. IE for example is
|
||||
# uploading files with "C:\foo\bar.txt" as filename
|
||||
value = value[1:-1]
|
||||
|
||||
# if this is a filename and the starting characters look like
|
||||
# a UNC path, then just return the value without quotes. Using the
|
||||
# replace sequence below on a UNC path has the effect of turning
|
||||
# the leading double slash into a single slash and then
|
||||
# _fix_ie_filename() doesn't work correctly. See #458.
|
||||
if not is_filename or value[:2] != "\\\\":
|
||||
return value.replace("\\\\", "\\").replace('\\"', '"')
|
||||
return value
|
||||
|
|
@ -24,18 +24,21 @@ LNBITS_DATA_FOLDER = env.str(
|
|||
)
|
||||
LNBITS_DATABASE_URL = env.str("LNBITS_DATABASE_URL", default=None)
|
||||
|
||||
LNBITS_ALLOWED_USERS: List[str] = env.list(
|
||||
"LNBITS_ALLOWED_USERS", default=[], subcast=str
|
||||
)
|
||||
LNBITS_ADMIN_USERS: List[str] = env.list("LNBITS_ADMIN_USERS", default=[], subcast=str)
|
||||
LNBITS_ADMIN_EXTENSIONS: List[str] = env.list(
|
||||
"LNBITS_ADMIN_EXTENSIONS", default=[], subcast=str
|
||||
)
|
||||
LNBITS_DISABLED_EXTENSIONS: List[str] = env.list(
|
||||
"LNBITS_DISABLED_EXTENSIONS", default=[], subcast=str
|
||||
)
|
||||
LNBITS_ALLOWED_USERS: List[str] = [
|
||||
x.strip(" ") for x in env.list("LNBITS_ALLOWED_USERS", default=[], subcast=str)
|
||||
]
|
||||
LNBITS_ADMIN_USERS: List[str] = [
|
||||
x.strip(" ") for x in env.list("LNBITS_ADMIN_USERS", default=[], subcast=str)
|
||||
]
|
||||
LNBITS_ADMIN_EXTENSIONS: List[str] = [
|
||||
x.strip(" ") for x in env.list("LNBITS_ADMIN_EXTENSIONS", default=[], subcast=str)
|
||||
]
|
||||
LNBITS_DISABLED_EXTENSIONS: List[str] = [
|
||||
x.strip(" ")
|
||||
for x in env.list("LNBITS_DISABLED_EXTENSIONS", default=[], subcast=str)
|
||||
]
|
||||
|
||||
LNBITS_AD_SPACE = env.list("LNBITS_AD_SPACE", default=[])
|
||||
LNBITS_AD_SPACE = [x.strip(" ") for x in env.list("LNBITS_AD_SPACE", default=[])]
|
||||
LNBITS_HIDE_API = env.bool("LNBITS_HIDE_API", default=False)
|
||||
LNBITS_SITE_TITLE = env.str("LNBITS_SITE_TITLE", default="LNbits")
|
||||
LNBITS_DENOMINATION = env.str("LNBITS_DENOMINATION", default="sats")
|
||||
|
|
@ -43,11 +46,14 @@ LNBITS_SITE_TAGLINE = env.str(
|
|||
"LNBITS_SITE_TAGLINE", default="free and open-source lightning wallet"
|
||||
)
|
||||
LNBITS_SITE_DESCRIPTION = env.str("LNBITS_SITE_DESCRIPTION", default="")
|
||||
LNBITS_THEME_OPTIONS: List[str] = env.list(
|
||||
"LNBITS_THEME_OPTIONS",
|
||||
default="classic, flamingo, mint, salvador, monochrome, autumn",
|
||||
subcast=str,
|
||||
)
|
||||
LNBITS_THEME_OPTIONS: List[str] = [
|
||||
x.strip(" ")
|
||||
for x in env.list(
|
||||
"LNBITS_THEME_OPTIONS",
|
||||
default="classic, flamingo, mint, salvador, monochrome, autumn",
|
||||
subcast=str,
|
||||
)
|
||||
]
|
||||
LNBITS_CUSTOM_LOGO = env.str("LNBITS_CUSTOM_LOGO", default="")
|
||||
|
||||
WALLET = wallet_class()
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 9.1 KiB |
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