diff --git a/.env.example b/.env.example
index 0f24445e..01ae2195 100644
--- a/.env.example
+++ b/.env.example
@@ -37,11 +37,11 @@ LNBITS_RESERVE_FEE_PERCENT=1.0
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,
@@ -91,4 +91,9 @@ LNBITS_DENOMINATION=sats
# EclairWallet
ECLAIR_URL=http://127.0.0.1:8283
-ECLAIR_PASS=eclairpw
\ No newline at end of file
+ECLAIR_PASS=eclairpw
+
+# LnTipsWallet
+# Enter /api in LightningTipBot to get your key
+LNTIPS_API_KEY=LNTIPS_ADMIN_KEY
+LNTIPS_API_ENDPOINT=https://ln.tips
diff --git a/docs/devs/websockets.md b/docs/devs/websockets.md
new file mode 100644
index 00000000..0638e4f2
--- /dev/null
+++ b/docs/devs/websockets.md
@@ -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)
+ })
+},
+```
diff --git a/docs/guide/installation.md b/docs/guide/installation.md
index 87679ed5..6b95f93b 100644
--- a/docs/guide/installation.md
+++ b/docs/guide/installation.md
@@ -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
diff --git a/lnbits/app.py b/lnbits/app.py
index f612c32c..8b9cf798 100644
--- a/lnbits/app.py
+++ b/lnbits/app.py
@@ -34,7 +34,6 @@ from .tasks import (
check_pending_payments,
internal_invoice_listener,
invoice_listener,
- run_deferred_async,
webhook_handler,
)
@@ -127,7 +126,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 +184,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():
diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py
index cbed6292..bb1ca0c1 100644
--- a/lnbits/core/crud.py
+++ b/lnbits/core/crud.py
@@ -333,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:
@@ -343,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(
"""
diff --git a/lnbits/core/services.py b/lnbits/core/services.py
index 961eb7b2..5d993b4c 100644
--- a/lnbits/core/services.py
+++ b/lnbits/core/services.py
@@ -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}")
diff --git a/lnbits/core/tasks.py b/lnbits/core/tasks.py
index 07b8a893..b57e2625 100644
--- a/lnbits/core/tasks.py
+++ b/lnbits/core/tasks.py
@@ -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:
diff --git a/lnbits/core/templates/core/index.html b/lnbits/core/templates/core/index.html
index f769b44f..68a7b7ed 100644
--- a/lnbits/core/templates/core/index.html
+++ b/lnbits/core/templates/core/index.html
@@ -171,6 +171,17 @@
+
diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py
index 7a2bbbe6..c07df568 100644
--- a/lnbits/core/views/api.py
+++ b/lnbits/core/views/api.py
@@ -3,11 +3,13 @@ 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
@@ -16,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
@@ -366,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
@@ -405,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",
)
diff --git a/lnbits/core/views/public_api.py b/lnbits/core/views/public_api.py
index 2d2cdd66..9b0ebc98 100644
--- a/lnbits/core/views/public_api.py
+++ b/lnbits/core/views/public_api.py
@@ -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
diff --git a/lnbits/db.py b/lnbits/db.py
index 66981784..f52b0391 100644
--- a/lnbits/db.py
+++ b/lnbits/db.py
@@ -52,6 +52,12 @@ class Compat:
return ""
return ""
+ @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):
diff --git a/lnbits/extensions/boltcards/migrations.py b/lnbits/extensions/boltcards/migrations.py
index 08126013..9609e0c3 100644
--- a/lnbits/extensions/boltcards/migrations.py
+++ b/lnbits/extensions/boltcards/migrations.py
@@ -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
+ """
diff --git a/lnbits/extensions/boltcards/tasks.py b/lnbits/extensions/boltcards/tasks.py
index 1b51c98b..c1e99b76 100644
--- a/lnbits/extensions/boltcards/tasks.py
+++ b/lnbits/extensions/boltcards/tasks.py
@@ -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()
diff --git a/lnbits/extensions/boltz/boltz.py b/lnbits/extensions/boltz/boltz.py
index 4e5fecd0..ac99d4f4 100644
--- a/lnbits/extensions/boltz/boltz.py
+++ b/lnbits/extensions/boltz/boltz.py
@@ -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:
diff --git a/lnbits/extensions/boltz/mempool.py b/lnbits/extensions/boltz/mempool.py
index ee305257..a44c0f02 100644
--- a/lnbits/extensions/boltz/mempool.py
+++ b/lnbits/extensions/boltz/mempool.py
@@ -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"
diff --git a/lnbits/extensions/boltz/migrations.py b/lnbits/extensions/boltz/migrations.py
index e4026dd0..925322ec 100644
--- a/lnbits/extensions/boltz/migrations.py
+++ b/lnbits/extensions/boltz/migrations.py
@@ -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
+ """
diff --git a/lnbits/extensions/boltz/tasks.py b/lnbits/extensions/boltz/tasks.py
index d6f72edf..d1ace04b 100644
--- a/lnbits/extensions/boltz/tasks.py
+++ b/lnbits/extensions/boltz/tasks.py
@@ -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()
diff --git a/lnbits/extensions/copilot/tasks.py b/lnbits/extensions/copilot/tasks.py
index f3c5cff8..c59ef4cc 100644
--- a/lnbits/extensions/copilot/tasks.py
+++ b/lnbits/extensions/copilot/tasks.py
@@ -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()
diff --git a/lnbits/extensions/invoices/migrations.py b/lnbits/extensions/invoices/migrations.py
index c47a954a..74a0fdba 100644
--- a/lnbits/extensions/invoices/migrations.py
+++ b/lnbits/extensions/invoices/migrations.py
@@ -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},
diff --git a/lnbits/extensions/jukebox/tasks.py b/lnbits/extensions/jukebox/tasks.py
index 70a2e65d..5614d926 100644
--- a/lnbits/extensions/jukebox/tasks.py
+++ b/lnbits/extensions/jukebox/tasks.py
@@ -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()
diff --git a/lnbits/extensions/livestream/tasks.py b/lnbits/extensions/livestream/tasks.py
index 85bdd5e0..626c698c 100644
--- a/lnbits/extensions/livestream/tasks.py
+++ b/lnbits/extensions/livestream/tasks.py
@@ -6,7 +6,7 @@ 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.helpers import get_current_extension_name, urlsafe_short_hash
from lnbits.tasks import internal_invoice_listener, register_invoice_listener
from .crud import get_livestream_by_track, get_producer, get_track
@@ -14,7 +14,7 @@ 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()
diff --git a/lnbits/extensions/lnaddress/tasks.py b/lnbits/extensions/lnaddress/tasks.py
index 9abe10c3..0c377eec 100644
--- a/lnbits/extensions/lnaddress/tasks.py
+++ b/lnbits/extensions/lnaddress/tasks.py
@@ -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()
diff --git a/lnbits/extensions/lnticket/__init__.py b/lnbits/extensions/lnticket/__init__.py
index 792b1175..cb793f4d 100644
--- a/lnbits/extensions/lnticket/__init__.py
+++ b/lnbits/extensions/lnticket/__init__.py
@@ -1,4 +1,5 @@
import asyncio
+import json
from fastapi import APIRouter
diff --git a/lnbits/extensions/lnticket/tasks.py b/lnbits/extensions/lnticket/tasks.py
index 7e672115..746ebea9 100644
--- a/lnbits/extensions/lnticket/tasks.py
+++ b/lnbits/extensions/lnticket/tasks.py
@@ -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()
diff --git a/lnbits/extensions/lnurldevice/__init__.py b/lnbits/extensions/lnurldevice/__init__.py
index 54849c95..d2010c44 100644
--- a/lnbits/extensions/lnurldevice/__init__.py
+++ b/lnbits/extensions/lnurldevice/__init__.py
@@ -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))
diff --git a/lnbits/extensions/lnurldevice/crud.py b/lnbits/extensions/lnurldevice/crud.py
index 45166521..4c25e4cb 100644
--- a/lnbits/extensions/lnurldevice/crud.py
+++ b/lnbits/extensions/lnurldevice/crud.py
@@ -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)
diff --git a/lnbits/extensions/lnurldevice/lnurl.py b/lnbits/extensions/lnurldevice/lnurl.py
index df0cd4b8..79892b78 100644
--- a/lnbits/extensions/lnurldevice/lnurl.py
+++ b/lnbits/extensions/lnurldevice/lnurl.py
@@ -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()
diff --git a/lnbits/extensions/lnurldevice/migrations.py b/lnbits/extensions/lnurldevice/migrations.py
index c7899282..7305cceb 100644
--- a/lnbits/extensions/lnurldevice/migrations.py
+++ b/lnbits/extensions/lnurldevice/migrations.py
@@ -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;"
+ )
diff --git a/lnbits/extensions/lnurldevice/models.py b/lnbits/extensions/lnurldevice/models.py
index fef0aec1..01bcc2ba 100644
--- a/lnbits/extensions/lnurldevice/models.py
+++ b/lnbits/extensions/lnurldevice/models.py
@@ -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:
diff --git a/lnbits/extensions/lnurldevice/tasks.py b/lnbits/extensions/lnurldevice/tasks.py
new file mode 100644
index 00000000..c8f3db04
--- /dev/null
+++ b/lnbits/extensions/lnurldevice/tasks.py
@@ -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
diff --git a/lnbits/extensions/lnurldevice/templates/lnurldevice/_api_docs.html b/lnbits/extensions/lnurldevice/templates/lnurldevice/_api_docs.html
index 7f9afa27..f93d44d8 100644
--- a/lnbits/extensions/lnurldevice/templates/lnurldevice/_api_docs.html
+++ b/lnbits/extensions/lnurldevice/templates/lnurldevice/_api_docs.html
@@ -1,13 +1,24 @@
- Register LNURLDevice devices to receive payments in your LNbits wallet.
- Build your own here
- https://github.com/arcbtc/bitcoinpos
+ Use with:
+ LNPoS
+
+ https://lnbits.github.io/lnpos
+ bitcoinSwitch
+
+ https://github.com/lnbits/bitcoinSwitch
+ FOSSA
+
+ https://github.com/lnbits/fossa
- Created by, Ben ArcBen Arc,
+ BC,
+ Vlad Stan
diff --git a/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html b/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html
index 24d19484..028dd94b 100644
--- a/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html
+++ b/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html
@@ -51,6 +51,7 @@
+
LNURLDevice Settings
+
+
+ LNURLs only work over HTTPS view LNURL
+
LNURLDevice device string
-
+ {% raw %}{{wslocation}}/lnurldevice/ws/{{settingsDialog.data.id}}{%
+ endraw %} Click to copy URL
+
+ {% raw
- %}{{location}}/lnurldevice/api/v1/lnurl/{{settingsDialog.data.id}},
- {{settingsDialog.data.key}}, {{settingsDialog.data.currency}}{% endraw
- %} Click to copy URL
-
-
+ >{% raw
+ %}{{location}}/lnurldevice/api/v1/lnurl/{{settingsDialog.data.id}},
+ {{settingsDialog.data.key}}, {{settingsDialog.data.currency}}{% endraw
+ %} Click to copy URL
+
+
@@ -191,6 +221,7 @@
label="Type of device"
>
+
+
+
+
+
+
+
+
+
+ {% raw %}
+
+
+ ID: {{ qrCodeDialog.data.id }}
+
+ {% endraw %}
+
+ Copy LNURL
+ Close
+
+
+
{% 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 => {
diff --git a/lnbits/extensions/lnurldevice/views.py b/lnbits/extensions/lnurldevice/views.py
index 3389e17c..5c6eba24 100644
--- a/lnbits/extensions/lnurldevice/views.py
+++ b/lnbits/extensions/lnurldevice/views.py
@@ -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)
diff --git a/lnbits/extensions/lnurldevice/views_api.py b/lnbits/extensions/lnurldevice/views_api.py
index d152d210..c034f66e 100644
--- a/lnbits/extensions/lnurldevice/views_api.py
+++ b/lnbits/extensions/lnurldevice/views_api.py
@@ -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}")
diff --git a/lnbits/extensions/lnurlp/tasks.py b/lnbits/extensions/lnurlp/tasks.py
index 525d36ce..86f1579a 100644
--- a/lnbits/extensions/lnurlp/tasks.py
+++ b/lnbits/extensions/lnurlp/tasks.py
@@ -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()
diff --git a/lnbits/extensions/lnurlpayout/migrations.py b/lnbits/extensions/lnurlpayout/migrations.py
index 6af04791..7a45e495 100644
--- a/lnbits/extensions/lnurlpayout/migrations.py
+++ b/lnbits/extensions/lnurlpayout/migrations.py
@@ -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
);
"""
)
diff --git a/lnbits/extensions/lnurlpayout/tasks.py b/lnbits/extensions/lnurlpayout/tasks.py
index b621876c..71f299be 100644
--- a/lnbits/extensions/lnurlpayout/tasks.py
+++ b/lnbits/extensions/lnurlpayout/tasks.py
@@ -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()
diff --git a/lnbits/extensions/satspay/tasks.py b/lnbits/extensions/satspay/tasks.py
index d325405b..46c16bbc 100644
--- a/lnbits/extensions/satspay/tasks.py
+++ b/lnbits/extensions/satspay/tasks.py
@@ -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()
diff --git a/lnbits/extensions/scrub/README.md b/lnbits/extensions/scrub/README.md
index 680c5e6d..3b8d0b2d 100644
--- a/lnbits/extensions/scrub/README.md
+++ b/lnbits/extensions/scrub/README.md
@@ -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!
+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!
+
[**Wallets supporting LNURL**](https://github.com/fiatjaf/awesome-lnurl#wallets)
## Usage
diff --git a/lnbits/extensions/scrub/tasks.py b/lnbits/extensions/scrub/tasks.py
index 87e1364b..852f3860 100644
--- a/lnbits/extensions/scrub/tasks.py
+++ b/lnbits/extensions/scrub/tasks.py
@@ -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}.",
diff --git a/lnbits/extensions/splitpayments/tasks.py b/lnbits/extensions/splitpayments/tasks.py
index 0948e849..cfc6c226 100644
--- a/lnbits/extensions/splitpayments/tasks.py
+++ b/lnbits/extensions/splitpayments/tasks.py
@@ -6,7 +6,7 @@ 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.helpers import get_current_extension_name, urlsafe_short_hash
from lnbits.tasks import internal_invoice_queue, register_invoice_listener
from .crud import get_targets
@@ -14,7 +14,7 @@ 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()
@@ -28,6 +28,10 @@ async def on_invoice_paid(payment: Payment) -> None:
# now we make some special internal transfers (from no one to the receiver)
targets = await get_targets(payment.wallet_id)
+
+ if not targets:
+ return
+
transfers = [
(target.wallet, int(target.percent * payment.amount / 100))
for target in targets
@@ -41,9 +45,6 @@ async def on_invoice_paid(payment: Payment) -> None:
)
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
@@ -76,5 +77,5 @@ async def on_invoice_paid(payment: Payment) -> None:
)
# manually send this for now
- await internal_invoice_queue.put(internal_checking_id)
+ await internal_invoice_queue.put(internal_checking_id)
return
diff --git a/lnbits/extensions/streamalerts/migrations.py b/lnbits/extensions/streamalerts/migrations.py
index 1b0cea37..7d50e8f1 100644
--- a/lnbits/extensions/streamalerts/migrations.py
+++ b/lnbits/extensions/streamalerts/migrations.py
@@ -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,
diff --git a/lnbits/extensions/subdomains/tasks.py b/lnbits/extensions/subdomains/tasks.py
index d8f35161..04ee2dd4 100644
--- a/lnbits/extensions/subdomains/tasks.py
+++ b/lnbits/extensions/subdomains/tasks.py
@@ -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()
diff --git a/lnbits/extensions/tipjar/migrations.py b/lnbits/extensions/tipjar/migrations.py
index 6b58fbca..d8f6da3f 100644
--- a/lnbits/extensions/tipjar/migrations.py
+++ b/lnbits/extensions/tipjar/migrations.py
@@ -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)
);
"""
diff --git a/lnbits/extensions/tpos/tasks.py b/lnbits/extensions/tpos/tasks.py
index af9663cc..f18d1689 100644
--- a/lnbits/extensions/tpos/tasks.py
+++ b/lnbits/extensions/tpos/tasks.py
@@ -4,7 +4,7 @@ import json
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.helpers import get_current_extension_name, urlsafe_short_hash
from lnbits.tasks import internal_invoice_queue, register_invoice_listener
from .crud import get_tpos
@@ -12,7 +12,7 @@ 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()
diff --git a/lnbits/helpers.py b/lnbits/helpers.py
index e97fc7bb..e213240c 100644
--- a/lnbits/helpers.py
+++ b/lnbits/helpers.py
@@ -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
diff --git a/lnbits/static/images/mynode.png b/lnbits/static/images/mynode.png
index cf25bc58..390446b8 100644
Binary files a/lnbits/static/images/mynode.png and b/lnbits/static/images/mynode.png differ
diff --git a/lnbits/static/images/mynodel.png b/lnbits/static/images/mynodel.png
index b8afb9ff..344b54b6 100644
Binary files a/lnbits/static/images/mynodel.png and b/lnbits/static/images/mynodel.png differ
diff --git a/lnbits/tasks.py b/lnbits/tasks.py
index 41287ff2..94e43dcf 100644
--- a/lnbits/tasks.py
+++ b/lnbits/tasks.py
@@ -1,8 +1,9 @@
import asyncio
import time
import traceback
+import uuid
from http import HTTPStatus
-from typing import Callable, List
+from typing import Callable, Dict, List
from fastapi.exceptions import HTTPException
from loguru import logger
@@ -18,20 +19,6 @@ from lnbits.settings import WALLET
from .core import db
-deferred_async: List[Callable] = []
-
-
-def record_async(func: Callable) -> Callable:
- def recorder(state):
- deferred_async.append(func)
-
- return recorder
-
-
-async def run_deferred_async():
- for func in deferred_async:
- asyncio.create_task(catch_everything_and_restart(func))
-
async def catch_everything_and_restart(func):
try:
@@ -50,18 +37,48 @@ async def send_push_promise(a, b) -> None:
pass
-invoice_listeners: List[asyncio.Queue] = []
+class SseListenersDict(dict):
+ """
+ A dict of sse listeners.
+ """
+
+ def __init__(self, name: str = None):
+ self.name = name or f"sse_listener_{str(uuid.uuid4())[:8]}"
+
+ def __setitem__(self, key, value):
+ assert type(key) == str, f"{key} is not a string"
+ assert type(value) == asyncio.Queue, f"{value} is not an asyncio.Queue"
+ logger.trace(f"sse: adding listener {key} to {self.name}. len = {len(self)+1}")
+ return super().__setitem__(key, value)
+
+ def __delitem__(self, key):
+ logger.trace(f"sse: removing listener from {self.name}. len = {len(self)-1}")
+ return super().__delitem__(key)
+
+ _RaiseKeyError = object() # singleton for no-default behavior
+
+ def pop(self, key, v=_RaiseKeyError) -> None:
+ logger.trace(f"sse: removing listener from {self.name}. len = {len(self)-1}")
+ return super().pop(key)
-def register_invoice_listener(send_chan: asyncio.Queue):
+invoice_listeners: Dict[str, asyncio.Queue] = SseListenersDict("invoice_listeners")
+
+
+def register_invoice_listener(send_chan: asyncio.Queue, name: str = None):
"""
- A method intended for extensions to call when they want to be notified about
- new invoice payments incoming.
+ A method intended for extensions (and core/tasks.py) to call when they want to be notified about
+ new invoice payments incoming. Will emit all incoming payments.
"""
- invoice_listeners.append(send_chan)
+ name_unique = f"{name or 'no_name'}_{str(uuid.uuid4())[:8]}"
+ logger.trace(f"sse: registering invoice listener {name_unique}")
+ invoice_listeners[name_unique] = send_chan
async def webhook_handler():
+ """
+ Returns the webhook_handler for the selected wallet if present. Used by API.
+ """
handler = getattr(WALLET, "webhook_listener", None)
if handler:
return await handler()
@@ -72,18 +89,36 @@ internal_invoice_queue: asyncio.Queue = asyncio.Queue(0)
async def internal_invoice_listener():
+ """
+ internal_invoice_queue will be filled directly in core/services.py
+ after the payment was deemed to be settled internally.
+
+ Called by the app startup sequence.
+ """
while True:
checking_id = await internal_invoice_queue.get()
+ logger.info("> got internal payment notification", checking_id)
asyncio.create_task(invoice_callback_dispatcher(checking_id))
async def invoice_listener():
+ """
+ invoice_listener will collect all invoices that come directly
+ from the backend wallet.
+
+ Called by the app startup sequence.
+ """
async for checking_id in WALLET.paid_invoices_stream():
logger.info("> got a payment notification", checking_id)
asyncio.create_task(invoice_callback_dispatcher(checking_id))
async def check_pending_payments():
+ """
+ check_pending_payments is called during startup to check for pending payments with
+ the backend and also to delete expired invoices. Incoming payments will be
+ checked only once, outgoing pending payments will be checked regularly.
+ """
outgoing = True
incoming = True
@@ -133,9 +168,14 @@ async def perform_balance_checks():
async def invoice_callback_dispatcher(checking_id: str):
+ """
+ Takes incoming payments, sets pending=False, and dispatches them to
+ invoice_listeners from core and extensions.
+ """
payment = await get_standalone_payment(checking_id, incoming=True)
if payment and payment.is_in:
- logger.trace("sending invoice callback for payment", checking_id)
+ logger.trace(f"sse sending invoice callback for payment {checking_id}")
await payment.set_pending(False)
- for send_chan in invoice_listeners:
+ for chan_name, send_chan in invoice_listeners.items():
+ logger.trace(f"sse sending to chan: {chan_name}")
await send_chan.put(payment)
diff --git a/lnbits/wallets/__init__.py b/lnbits/wallets/__init__.py
index 41949652..fa533566 100644
--- a/lnbits/wallets/__init__.py
+++ b/lnbits/wallets/__init__.py
@@ -1,5 +1,6 @@
# flake8: noqa
+
from .cliche import ClicheWallet
from .cln import CoreLightningWallet # legacy .env support
from .cln import CoreLightningWallet as CLightningWallet
@@ -9,6 +10,7 @@ from .lnbits import LNbitsWallet
from .lndgrpc import LndWallet
from .lndrest import LndRestWallet
from .lnpay import LNPayWallet
+from .lntips import LnTipsWallet
from .lntxbot import LntxbotWallet
from .opennode import OpenNodeWallet
from .spark import SparkWallet
diff --git a/lnbits/wallets/fake.py b/lnbits/wallets/fake.py
index 8424001b..a07ef4d8 100644
--- a/lnbits/wallets/fake.py
+++ b/lnbits/wallets/fake.py
@@ -8,9 +8,7 @@ from typing import AsyncGenerator, Dict, Optional
from environs import Env # type: ignore
from loguru import logger
-from lnbits.helpers import urlsafe_short_hash
-
-from ..bolt11 import decode, encode
+from ..bolt11 import Invoice, decode, encode
from .base import (
InvoiceResponse,
PaymentResponse,
@@ -24,6 +22,16 @@ env.read_env()
class FakeWallet(Wallet):
+ queue: asyncio.Queue = asyncio.Queue(0)
+ secret: str = env.str("FAKE_WALLET_SECTRET", default="ToTheMoon1")
+ privkey: str = hashlib.pbkdf2_hmac(
+ "sha256",
+ secret.encode("utf-8"),
+ ("FakeWallet").encode("utf-8"),
+ 2048,
+ 32,
+ ).hex()
+
async def status(self) -> StatusResponse:
logger.info(
"FakeWallet funding source is for using LNbits as a centralised, stand-alone payment system with brrrrrr."
@@ -39,18 +47,12 @@ class FakeWallet(Wallet):
) -> InvoiceResponse:
# we set a default secret since FakeWallet is used for internal=True invoices
# and the user might not have configured a secret yet
- secret = env.str("FAKE_WALLET_SECTRET", default="ToTheMoon1")
+
data: Dict = {
"out": False,
"amount": amount,
"currency": "bc",
- "privkey": hashlib.pbkdf2_hmac(
- "sha256",
- secret.encode("utf-8"),
- ("FakeWallet").encode("utf-8"),
- 2048,
- 32,
- ).hex(),
+ "privkey": self.privkey,
"memo": None,
"description_hash": None,
"description": "",
@@ -86,8 +88,9 @@ class FakeWallet(Wallet):
invoice = decode(bolt11)
if (
hasattr(invoice, "checking_id")
- and invoice.checking_id[6:] == data["privkey"][:6] # type: ignore
+ and invoice.checking_id[:6] == self.privkey[:6] # type: ignore
):
+ await self.queue.put(invoice)
return PaymentResponse(True, invoice.payment_hash, 0)
else:
return PaymentResponse(
@@ -101,7 +104,6 @@ class FakeWallet(Wallet):
return PaymentStatus(None)
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
- self.queue: asyncio.Queue = asyncio.Queue(0)
while True:
- value = await self.queue.get()
- yield value
+ value: Invoice = await self.queue.get()
+ yield value.payment_hash
diff --git a/lnbits/wallets/lntips.py b/lnbits/wallets/lntips.py
new file mode 100644
index 00000000..54220c85
--- /dev/null
+++ b/lnbits/wallets/lntips.py
@@ -0,0 +1,170 @@
+import asyncio
+import hashlib
+import json
+import time
+from os import getenv
+from typing import AsyncGenerator, Dict, Optional
+
+import httpx
+from loguru import logger
+
+from .base import (
+ InvoiceResponse,
+ PaymentResponse,
+ PaymentStatus,
+ StatusResponse,
+ Wallet,
+)
+
+
+class LnTipsWallet(Wallet):
+ def __init__(self):
+ endpoint = getenv("LNTIPS_API_ENDPOINT")
+ self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
+
+ key = (
+ getenv("LNTIPS_API_KEY")
+ or getenv("LNTIPS_ADMIN_KEY")
+ or getenv("LNTIPS_INVOICE_KEY")
+ )
+ self.auth = {"Authorization": f"Basic {key}"}
+
+ async def status(self) -> StatusResponse:
+ async with httpx.AsyncClient() as client:
+ r = await client.get(
+ f"{self.endpoint}/api/v1/balance", headers=self.auth, timeout=40
+ )
+ try:
+ data = r.json()
+ except:
+ return StatusResponse(
+ f"Failed to connect to {self.endpoint}, got: '{r.text[:200]}...'", 0
+ )
+
+ if data.get("error"):
+ return StatusResponse(data["error"], 0)
+
+ return StatusResponse(None, data["balance"] * 1000)
+
+ async def create_invoice(
+ self,
+ amount: int,
+ memo: Optional[str] = None,
+ description_hash: Optional[bytes] = None,
+ unhashed_description: Optional[bytes] = None,
+ **kwargs,
+ ) -> InvoiceResponse:
+ data: Dict = {"amount": amount}
+ if description_hash:
+ data["description_hash"] = description_hash.hex()
+ elif unhashed_description:
+ data["description_hash"] = hashlib.sha256(unhashed_description).hexdigest()
+ else:
+ data["memo"] = memo or ""
+
+ async with httpx.AsyncClient() as client:
+ r = await client.post(
+ f"{self.endpoint}/api/v1/createinvoice",
+ headers=self.auth,
+ json=data,
+ timeout=40,
+ )
+
+ if r.is_error:
+ try:
+ data = r.json()
+ error_message = data["message"]
+ except:
+ error_message = r.text
+ pass
+
+ return InvoiceResponse(False, None, None, error_message)
+
+ data = r.json()
+ return InvoiceResponse(
+ True, data["payment_hash"], data["payment_request"], None
+ )
+
+ async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
+ async with httpx.AsyncClient() as client:
+ r = await client.post(
+ f"{self.endpoint}/api/v1/payinvoice",
+ headers=self.auth,
+ json={"pay_req": bolt11},
+ timeout=None,
+ )
+ if r.is_error:
+ return PaymentResponse(False, None, 0, None, r.text)
+
+ if "error" in r.json():
+ try:
+ data = r.json()
+ error_message = data["error"]
+ except:
+ error_message = r.text
+ pass
+ return PaymentResponse(False, None, 0, None, error_message)
+
+ data = r.json()["details"]
+ checking_id = data["payment_hash"]
+ fee_msat = -data["fee"]
+ preimage = data["preimage"]
+ return PaymentResponse(True, checking_id, fee_msat, preimage, None)
+
+ async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
+ async with httpx.AsyncClient() as client:
+ r = await client.post(
+ f"{self.endpoint}/api/v1/invoicestatus/{checking_id}",
+ headers=self.auth,
+ )
+
+ if r.is_error or len(r.text) == 0:
+ return PaymentStatus(None)
+
+ data = r.json()
+ return PaymentStatus(data["paid"])
+
+ async def get_payment_status(self, checking_id: str) -> PaymentStatus:
+ async with httpx.AsyncClient() as client:
+ r = await client.post(
+ url=f"{self.endpoint}/api/v1/paymentstatus/{checking_id}",
+ headers=self.auth,
+ )
+
+ if r.is_error:
+ return PaymentStatus(None)
+ data = r.json()
+
+ paid_to_status = {False: None, True: True}
+ return PaymentStatus(paid_to_status[data.get("paid")])
+
+ async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
+ last_connected = None
+ while True:
+ url = f"{self.endpoint}/api/v1/invoicestream"
+ try:
+ async with httpx.AsyncClient(timeout=None, headers=self.auth) as client:
+ last_connected = time.time()
+ async with client.stream("GET", url) as r:
+ async for line in r.aiter_lines():
+ try:
+ prefix = "data: "
+ if not line.startswith(prefix):
+ continue
+ data = line[len(prefix) :] # sse parsing
+ inv = json.loads(data)
+ if not inv.get("payment_hash"):
+ continue
+ except:
+ continue
+ yield inv["payment_hash"]
+ except Exception as e:
+ pass
+
+ # do not sleep if the connection was active for more than 10s
+ # since the backend is expected to drop the connection after 90s
+ if last_connected is None or time.time() - last_connected < 10:
+ logger.error(
+ f"lost connection to {self.endpoint}/api/v1/invoicestream, retrying in 5 seconds"
+ )
+ await asyncio.sleep(5)
diff --git a/lnbits/wallets/void.py b/lnbits/wallets/void.py
index 0de387aa..b74eb245 100644
--- a/lnbits/wallets/void.py
+++ b/lnbits/wallets/void.py
@@ -23,7 +23,7 @@ class VoidWallet(Wallet):
raise Unsupported("")
async def status(self) -> StatusResponse:
- logger.info(
+ logger.warning(
"This backend does nothing, it is here just as a placeholder, you must configure an actual backend before being able to do anything useful with LNbits."
)
return StatusResponse(None, 0)
diff --git a/poetry.lock b/poetry.lock
index 2a57a5c1..5b283d75 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -46,6 +46,17 @@ category = "main"
optional = false
python-versions = "*"
+[[package]]
+name = "async-timeout"
+version = "4.0.2"
+description = "Timeout context manager for asyncio programs"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+typing-extensions = {version = ">=3.6.5", markers = "python_version < \"3.8\""}
+
[[package]]
name = "attrs"
version = "21.2.0"
@@ -111,7 +122,7 @@ jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]]
-name = "cerberus"
+name = "Cerberus"
version = "1.3.4"
description = "Lightweight, extensible schema and data validation tool for Python dictionaries."
category = "main"
@@ -185,7 +196,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "coverage"
-version = "6.4.4"
+version = "6.5.0"
description = "Code coverage measurement for Python"
category = "dev"
optional = false
@@ -402,7 +413,7 @@ plugins = ["setuptools"]
requirements_deprecated_finder = ["pip-api", "pipreqs"]
[[package]]
-name = "jinja2"
+name = "Jinja2"
version = "3.0.1"
description = "A very fast and expressive template engine."
category = "main"
@@ -444,7 +455,7 @@ win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""}
dev = ["Sphinx (>=2.2.1)", "black (>=19.10b0)", "codecov (>=2.0.15)", "colorama (>=0.3.4)", "flake8 (>=3.7.7)", "isort (>=5.1.1)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "tox (>=3.9.0)", "tox-travis (>=0.12)"]
[[package]]
-name = "markupsafe"
+name = "MarkupSafe"
version = "2.0.1"
description = "Safely add untrusted strings to HTML/XML markup."
category = "main"
@@ -578,7 +589,7 @@ testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "protobuf"
-version = "4.21.6"
+version = "4.21.7"
description = ""
category = "main"
optional = false
@@ -686,7 +697,7 @@ optional = false
python-versions = "*"
[[package]]
-name = "pyqrcode"
+name = "PyQRCode"
version = "1.2.1"
description = "A QR code generator written purely in Python with SVG, EPS, PNG and terminal output."
category = "main"
@@ -697,7 +708,7 @@ python-versions = "*"
PNG = ["pypng (>=0.0.13)"]
[[package]]
-name = "pyscss"
+name = "pyScss"
version = "1.4.0"
description = "pyScss, a Scss compiler for Python"
category = "main"
@@ -780,7 +791,7 @@ python-versions = ">=3.5"
cli = ["click (>=5.0)"]
[[package]]
-name = "pyyaml"
+name = "PyYAML"
version = "5.4.1"
description = "YAML parser and emitter for Python"
category = "main"
@@ -788,7 +799,7 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
[[package]]
-name = "represent"
+name = "Represent"
version = "1.6.0.post0"
description = "Create __repr__ automatically or declaratively."
category = "main"
@@ -828,14 +839,14 @@ cffi = ">=1.3.0"
[[package]]
name = "setuptools"
-version = "65.4.0"
+version = "65.4.1"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
category = "main"
optional = false
python-versions = ">=3.7"
[package.extras]
-docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
+docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
@@ -864,7 +875,7 @@ optional = false
python-versions = ">=3.5"
[[package]]
-name = "sqlalchemy"
+name = "SQLAlchemy"
version = "1.3.23"
description = "Database Abstraction Library"
category = "main"
@@ -1040,7 +1051,7 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=4.6)", "pytest-black (
[metadata]
lock-version = "1.1"
python-versions = "^3.10 | ^3.9 | ^3.8 | ^3.7"
-content-hash = "72e4462285d0bc5e2cb83c88c613726beced959b268bd30b984d8baaeff178ea"
+content-hash = "c4a01d5bfc24a8008348b6bd954717354554310afaaecbfc2a14222ad25aca42"
[metadata.files]
aiofiles = [
@@ -1059,6 +1070,10 @@ asn1crypto = [
{file = "asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67"},
{file = "asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c"},
]
+async-timeout = [
+ {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"},
+ {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"},
+]
attrs = [
{file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"},
{file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"},
@@ -1101,7 +1116,7 @@ black = [
{file = "black-22.8.0-py3-none-any.whl", hash = "sha256:d2c21d439b2baf7aa80d6dd4e3659259be64c6f49dfd0f32091063db0e006db4"},
{file = "black-22.8.0.tar.gz", hash = "sha256:792f7eb540ba9a17e8656538701d3eb1afcb134e3b45b71f20b25c77a8db7e6e"},
]
-cerberus = [
+Cerberus = [
{file = "Cerberus-1.3.4.tar.gz", hash = "sha256:d1b21b3954b2498d9a79edf16b3170a3ac1021df88d197dc2ce5928ba519237c"},
]
certifi = [
@@ -1209,56 +1224,56 @@ colorama = [
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
]
coverage = [
- {file = "coverage-6.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7b4da9bafad21ea45a714d3ea6f3e1679099e420c8741c74905b92ee9bfa7cc"},
- {file = "coverage-6.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fde17bc42e0716c94bf19d92e4c9f5a00c5feb401f5bc01101fdf2a8b7cacf60"},
- {file = "coverage-6.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdbb0d89923c80dbd435b9cf8bba0ff55585a3cdb28cbec65f376c041472c60d"},
- {file = "coverage-6.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67f9346aeebea54e845d29b487eb38ec95f2ecf3558a3cffb26ee3f0dcc3e760"},
- {file = "coverage-6.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42c499c14efd858b98c4e03595bf914089b98400d30789511577aa44607a1b74"},
- {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c35cca192ba700979d20ac43024a82b9b32a60da2f983bec6c0f5b84aead635c"},
- {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9cc4f107009bca5a81caef2fca843dbec4215c05e917a59dec0c8db5cff1d2aa"},
- {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f444627b3664b80d078c05fe6a850dd711beeb90d26731f11d492dcbadb6973"},
- {file = "coverage-6.4.4-cp310-cp310-win32.whl", hash = "sha256:66e6df3ac4659a435677d8cd40e8eb1ac7219345d27c41145991ee9bf4b806a0"},
- {file = "coverage-6.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:35ef1f8d8a7a275aa7410d2f2c60fa6443f4a64fae9be671ec0696a68525b875"},
- {file = "coverage-6.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c1328d0c2f194ffda30a45f11058c02410e679456276bfa0bbe0b0ee87225fac"},
- {file = "coverage-6.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61b993f3998ee384935ee423c3d40894e93277f12482f6e777642a0141f55782"},
- {file = "coverage-6.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5dd4b8e9cd0deb60e6fcc7b0647cbc1da6c33b9e786f9c79721fd303994832f"},
- {file = "coverage-6.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7026f5afe0d1a933685d8f2169d7c2d2e624f6255fb584ca99ccca8c0e966fd7"},
- {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9c7b9b498eb0c0d48b4c2abc0e10c2d78912203f972e0e63e3c9dc21f15abdaa"},
- {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ee2b2fb6eb4ace35805f434e0f6409444e1466a47f620d1d5763a22600f0f892"},
- {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ab066f5ab67059d1f1000b5e1aa8bbd75b6ed1fc0014559aea41a9eb66fc2ce0"},
- {file = "coverage-6.4.4-cp311-cp311-win32.whl", hash = "sha256:9d6e1f3185cbfd3d91ac77ea065d85d5215d3dfa45b191d14ddfcd952fa53796"},
- {file = "coverage-6.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:e3d3c4cc38b2882f9a15bafd30aec079582b819bec1b8afdbde8f7797008108a"},
- {file = "coverage-6.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a095aa0a996ea08b10580908e88fbaf81ecf798e923bbe64fb98d1807db3d68a"},
- {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef6f44409ab02e202b31a05dd6666797f9de2aa2b4b3534e9d450e42dea5e817"},
- {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b7101938584d67e6f45f0015b60e24a95bf8dea19836b1709a80342e01b472f"},
- {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a32ec68d721c3d714d9b105c7acf8e0f8a4f4734c811eda75ff3718570b5e3"},
- {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6a864733b22d3081749450466ac80698fe39c91cb6849b2ef8752fd7482011f3"},
- {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:08002f9251f51afdcc5e3adf5d5d66bb490ae893d9e21359b085f0e03390a820"},
- {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a3b2752de32c455f2521a51bd3ffb53c5b3ae92736afde67ce83477f5c1dd928"},
- {file = "coverage-6.4.4-cp37-cp37m-win32.whl", hash = "sha256:f855b39e4f75abd0dfbcf74a82e84ae3fc260d523fcb3532786bcbbcb158322c"},
- {file = "coverage-6.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ee6ae6bbcac0786807295e9687169fba80cb0617852b2fa118a99667e8e6815d"},
- {file = "coverage-6.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:564cd0f5b5470094df06fab676c6d77547abfdcb09b6c29c8a97c41ad03b103c"},
- {file = "coverage-6.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cbbb0e4cd8ddcd5ef47641cfac97d8473ab6b132dd9a46bacb18872828031685"},
- {file = "coverage-6.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6113e4df2fa73b80f77663445be6d567913fb3b82a86ceb64e44ae0e4b695de1"},
- {file = "coverage-6.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d032bfc562a52318ae05047a6eb801ff31ccee172dc0d2504614e911d8fa83e"},
- {file = "coverage-6.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e431e305a1f3126477abe9a184624a85308da8edf8486a863601d58419d26ffa"},
- {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cf2afe83a53f77aec067033199797832617890e15bed42f4a1a93ea24794ae3e"},
- {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:783bc7c4ee524039ca13b6d9b4186a67f8e63d91342c713e88c1865a38d0892a"},
- {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ff934ced84054b9018665ca3967fc48e1ac99e811f6cc99ea65978e1d384454b"},
- {file = "coverage-6.4.4-cp38-cp38-win32.whl", hash = "sha256:e1fabd473566fce2cf18ea41171d92814e4ef1495e04471786cbc943b89a3781"},
- {file = "coverage-6.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:4179502f210ebed3ccfe2f78bf8e2d59e50b297b598b100d6c6e3341053066a2"},
- {file = "coverage-6.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:98c0b9e9b572893cdb0a00e66cf961a238f8d870d4e1dc8e679eb8bdc2eb1b86"},
- {file = "coverage-6.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc600f6ec19b273da1d85817eda339fb46ce9eef3e89f220055d8696e0a06908"},
- {file = "coverage-6.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a98d6bf6d4ca5c07a600c7b4e0c5350cd483c85c736c522b786be90ea5bac4f"},
- {file = "coverage-6.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01778769097dbd705a24e221f42be885c544bb91251747a8a3efdec6eb4788f2"},
- {file = "coverage-6.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfa0b97eb904255e2ab24166071b27408f1f69c8fbda58e9c0972804851e0558"},
- {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fcbe3d9a53e013f8ab88734d7e517eb2cd06b7e689bedf22c0eb68db5e4a0a19"},
- {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:15e38d853ee224e92ccc9a851457fb1e1f12d7a5df5ae44544ce7863691c7a0d"},
- {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6913dddee2deff8ab2512639c5168c3e80b3ebb0f818fed22048ee46f735351a"},
- {file = "coverage-6.4.4-cp39-cp39-win32.whl", hash = "sha256:354df19fefd03b9a13132fa6643527ef7905712109d9c1c1903f2133d3a4e145"},
- {file = "coverage-6.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:1238b08f3576201ebf41f7c20bf59baa0d05da941b123c6656e42cdb668e9827"},
- {file = "coverage-6.4.4-pp36.pp37.pp38-none-any.whl", hash = "sha256:f67cf9f406cf0d2f08a3515ce2db5b82625a7257f88aad87904674def6ddaec1"},
- {file = "coverage-6.4.4.tar.gz", hash = "sha256:e16c45b726acb780e1e6f88b286d3c10b3914ab03438f32117c4aa52d7f30d58"},
+ {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"},
+ {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"},
+ {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"},
+ {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"},
+ {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"},
+ {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"},
+ {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"},
+ {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"},
+ {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"},
+ {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"},
+ {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"},
+ {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"},
+ {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"},
+ {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"},
+ {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"},
+ {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"},
+ {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"},
+ {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"},
+ {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"},
+ {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"},
+ {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"},
+ {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"},
+ {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"},
+ {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"},
+ {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"},
+ {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"},
+ {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"},
+ {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"},
+ {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"},
+ {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"},
+ {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"},
+ {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"},
+ {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"},
+ {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"},
+ {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"},
+ {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"},
+ {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"},
+ {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"},
+ {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"},
+ {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"},
+ {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"},
+ {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"},
+ {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"},
+ {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"},
+ {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"},
+ {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"},
+ {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"},
+ {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"},
+ {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"},
+ {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"},
]
cryptography = [
{file = "cryptography-36.0.2-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:4e2dddd38a5ba733be6a025a1475a9f45e4e41139d1321f412c6b360b19070b6"},
@@ -1413,7 +1428,7 @@ isort = [
{file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
{file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"},
]
-jinja2 = [
+Jinja2 = [
{file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"},
{file = "Jinja2-3.0.1.tar.gz", hash = "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"},
]
@@ -1425,7 +1440,7 @@ loguru = [
{file = "loguru-0.5.3-py3-none-any.whl", hash = "sha256:f8087ac396b5ee5f67c963b495d615ebbceac2796379599820e324419d53667c"},
{file = "loguru-0.5.3.tar.gz", hash = "sha256:b28e72ac7a98be3d28ad28570299a393dfcd32e5e3f6a353dec94675767b6319"},
]
-markupsafe = [
+MarkupSafe = [
{file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"},
{file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"},
{file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"},
@@ -1558,20 +1573,20 @@ pluggy = [
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
]
protobuf = [
- {file = "protobuf-4.21.6-cp310-abi3-win32.whl", hash = "sha256:49f88d56a9180dbb7f6199c920f5bb5c1dd0172f672983bb281298d57c2ac8eb"},
- {file = "protobuf-4.21.6-cp310-abi3-win_amd64.whl", hash = "sha256:7a6cc8842257265bdfd6b74d088b829e44bcac3cca234c5fdd6052730017b9ea"},
- {file = "protobuf-4.21.6-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:ba596b9ffb85c909fcfe1b1a23136224ed678af3faf9912d3fa483d5f9813c4e"},
- {file = "protobuf-4.21.6-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:4143513c766db85b9d7c18dbf8339673c8a290131b2a0fe73855ab20770f72b0"},
- {file = "protobuf-4.21.6-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:b6cea204865595a92a7b240e4b65bcaaca3ad5d2ce25d9db3756eba06041138e"},
- {file = "protobuf-4.21.6-cp37-cp37m-win32.whl", hash = "sha256:9666da97129138585b26afcb63ad4887f602e169cafe754a8258541c553b8b5d"},
- {file = "protobuf-4.21.6-cp37-cp37m-win_amd64.whl", hash = "sha256:308173d3e5a3528787bb8c93abea81d5a950bdce62840d9760effc84127fb39c"},
- {file = "protobuf-4.21.6-cp38-cp38-win32.whl", hash = "sha256:aa29113ec901281f29d9d27b01193407a98aa9658b8a777b0325e6d97149f5ce"},
- {file = "protobuf-4.21.6-cp38-cp38-win_amd64.whl", hash = "sha256:8f9e60f7d44592c66e7b332b6a7b4b6e8d8b889393c79dbc3a91f815118f8eac"},
- {file = "protobuf-4.21.6-cp39-cp39-win32.whl", hash = "sha256:80e6540381080715fddac12690ee42d087d0d17395f8d0078dfd6f1181e7be4c"},
- {file = "protobuf-4.21.6-cp39-cp39-win_amd64.whl", hash = "sha256:77b355c8604fe285536155286b28b0c4cbc57cf81b08d8357bf34829ea982860"},
- {file = "protobuf-4.21.6-py2.py3-none-any.whl", hash = "sha256:07a0bb9cc6114f16a39c866dc28b6e3d96fa4ffb9cc1033057412547e6e75cb9"},
- {file = "protobuf-4.21.6-py3-none-any.whl", hash = "sha256:c7c864148a237f058c739ae7a05a2b403c0dfa4ce7d1f3e5213f352ad52d57c6"},
- {file = "protobuf-4.21.6.tar.gz", hash = "sha256:6b1040a5661cd5f6e610cbca9cfaa2a17d60e2bb545309bc1b278bb05be44bdd"},
+ {file = "protobuf-4.21.7-cp310-abi3-win32.whl", hash = "sha256:c7cb105d69a87416bd9023e64324e1c089593e6dae64d2536f06bcbe49cd97d8"},
+ {file = "protobuf-4.21.7-cp310-abi3-win_amd64.whl", hash = "sha256:3ec85328a35a16463c6f419dbce3c0fc42b3e904d966f17f48bae39597c7a543"},
+ {file = "protobuf-4.21.7-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:db9056b6a11cb5131036d734bcbf91ef3ef9235d6b681b2fc431cbfe5a7f2e56"},
+ {file = "protobuf-4.21.7-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:ca200645d6235ce0df3ccfdff1567acbab35c4db222a97357806e015f85b5744"},
+ {file = "protobuf-4.21.7-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:b019c79e23a80735cc8a71b95f76a49a262f579d6b84fd20a0b82279f40e2cc1"},
+ {file = "protobuf-4.21.7-cp37-cp37m-win32.whl", hash = "sha256:d3f89ccf7182293feba2de2739c8bf34fed1ed7c65a5cf987be00311acac57c1"},
+ {file = "protobuf-4.21.7-cp37-cp37m-win_amd64.whl", hash = "sha256:a74d96cd960b87b4b712797c741bb3ea3a913f5c2dc4b6cbe9c0f8360b75297d"},
+ {file = "protobuf-4.21.7-cp38-cp38-win32.whl", hash = "sha256:8e09d1916386eca1ef1353767b6efcebc0a6859ed7f73cb7fb974feba3184830"},
+ {file = "protobuf-4.21.7-cp38-cp38-win_amd64.whl", hash = "sha256:9e355f2a839d9930d83971b9f562395e13493f0e9211520f8913bd11efa53c02"},
+ {file = "protobuf-4.21.7-cp39-cp39-win32.whl", hash = "sha256:f370c0a71712f8965023dd5b13277444d3cdfecc96b2c778b0e19acbfd60df6e"},
+ {file = "protobuf-4.21.7-cp39-cp39-win_amd64.whl", hash = "sha256:9643684232b6b340b5e63bb69c9b4904cdd39e4303d498d1a92abddc7e895b7f"},
+ {file = "protobuf-4.21.7-py2.py3-none-any.whl", hash = "sha256:8066322588d4b499869bf9f665ebe448e793036b552f68c585a9b28f1e393f66"},
+ {file = "protobuf-4.21.7-py3-none-any.whl", hash = "sha256:58b81358ec6c0b5d50df761460ae2db58405c063fd415e1101209221a0a810e1"},
+ {file = "protobuf-4.21.7.tar.gz", hash = "sha256:71d9dba03ed3432c878a801e2ea51e034b0ea01cf3a4344fb60166cb5f6c8757"},
]
psycopg2-binary = [
{file = "psycopg2-binary-2.9.1.tar.gz", hash = "sha256:b0221ca5a9837e040ebf61f48899926b5783668b7807419e4adae8175a31f773"},
@@ -1691,11 +1706,11 @@ pyparsing = [
pypng = [
{file = "pypng-0.0.21-py3-none-any.whl", hash = "sha256:76f8a1539ec56451da7ab7121f12a361969fe0f2d48d703d198ce2a99d6c5afd"},
]
-pyqrcode = [
+PyQRCode = [
{file = "PyQRCode-1.2.1.tar.gz", hash = "sha256:fdbf7634733e56b72e27f9bce46e4550b75a3a2c420414035cae9d9d26b234d5"},
{file = "PyQRCode-1.2.1.zip", hash = "sha256:1b2812775fa6ff5c527977c4cd2ccb07051ca7d0bc0aecf937a43864abe5eff6"},
]
-pyscss = [
+pyScss = [
{file = "pyScss-1.4.0.tar.gz", hash = "sha256:8f35521ffe36afa8b34c7d6f3195088a7057c185c2b8f15ee459ab19748669ff"},
]
PySocks = [
@@ -1719,7 +1734,7 @@ python-dotenv = [
{file = "python-dotenv-0.19.0.tar.gz", hash = "sha256:f521bc2ac9a8e03c736f62911605c5d83970021e3fa95b37d769e2bbbe9b6172"},
{file = "python_dotenv-0.19.0-py2.py3-none-any.whl", hash = "sha256:aae25dc1ebe97c420f50b81fb0e5c949659af713f31fdb63c749ca68748f34b1"},
]
-pyyaml = [
+PyYAML = [
{file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"},
{file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"},
{file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"},
@@ -1750,7 +1765,7 @@ pyyaml = [
{file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"},
{file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"},
]
-represent = [
+Represent = [
{file = "Represent-1.6.0.post0-py2.py3-none-any.whl", hash = "sha256:99142650756ef1998ce0661568f54a47dac8c638fb27e3816c02536575dbba8c"},
{file = "Represent-1.6.0.post0.tar.gz", hash = "sha256:026c0de2ee8385d1255b9c2426cd4f03fe9177ac94c09979bc601946c8493aa0"},
]
@@ -1784,8 +1799,8 @@ secp256k1 = [
{file = "secp256k1-0.14.0.tar.gz", hash = "sha256:82c06712d69ef945220c8b53c1a0d424c2ff6a1f64aee609030df79ad8383397"},
]
setuptools = [
- {file = "setuptools-65.4.0-py3-none-any.whl", hash = "sha256:c2d2709550f15aab6c9110196ea312f468f41cd546bceb24127a1be6fdcaeeb1"},
- {file = "setuptools-65.4.0.tar.gz", hash = "sha256:a8f6e213b4b0661f590ccf40de95d28a177cd747d098624ad3f69c40287297e9"},
+ {file = "setuptools-65.4.1-py3-none-any.whl", hash = "sha256:1b6bdc6161661409c5f21508763dc63ab20a9ac2f8ba20029aaaa7fdb9118012"},
+ {file = "setuptools-65.4.1.tar.gz", hash = "sha256:3050e338e5871e70c72983072fe34f6032ae1cdeeeb67338199c2f74e083a80e"},
]
shortuuid = [
{file = "shortuuid-1.0.1-py3-none-any.whl", hash = "sha256:492c7402ff91beb1342a5898bd61ea953985bf24a41cd9f247409aa2e03c8f77"},
@@ -1799,7 +1814,7 @@ sniffio = [
{file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"},
{file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"},
]
-sqlalchemy = [
+SQLAlchemy = [
{file = "SQLAlchemy-1.3.23-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:fd3b96f8c705af8e938eaa99cbd8fd1450f632d38cad55e7367c33b263bf98ec"},
{file = "SQLAlchemy-1.3.23-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:29cccc9606750fe10c5d0e8bd847f17a97f3850b8682aef1f56f5d5e1a5a64b1"},
{file = "SQLAlchemy-1.3.23-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:927ce09e49bff3104459e1451ce82983b0a3062437a07d883a4c66f0b344c9b5"},
diff --git a/pyproject.toml b/pyproject.toml
index 864500f7..19dac860 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -15,7 +15,6 @@ asgiref = "3.4.1"
attrs = "21.2.0"
bech32 = "1.2.0"
bitstring = "3.1.9"
-cerberus = "1.3.4"
certifi = "2021.5.30"
charset-normalizer = "2.0.6"
click = "8.0.1"
@@ -62,6 +61,8 @@ cffi = "1.15.0"
websocket-client = "1.3.3"
grpcio = "^1.49.1"
protobuf = "^4.21.6"
+Cerberus = "^1.3.4"
+async-timeout = "^4.0.2"
pyln-client = "0.11.1"
[tool.poetry.dev-dependencies]
diff --git a/requirements.txt b/requirements.txt
index 697ea1d4..eb9a6e5e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -51,3 +51,5 @@ uvloop==0.16.0
watchfiles==0.16.0
websockets==10.3
websocket-client==1.3.3
+async-timeout==4.0.2
+setuptools==65.4.0
\ No newline at end of file