2021-08-30 19:55:02 +02:00
|
|
|
import asyncio
|
2022-07-16 14:23:03 +02:00
|
|
|
import time
|
2021-07-30 19:26:22 -03:00
|
|
|
import traceback
|
2022-10-04 09:51:47 +02:00
|
|
|
import uuid
|
2020-10-06 00:39:54 -03:00
|
|
|
from http import HTTPStatus
|
2023-01-20 10:44:07 +00:00
|
|
|
from typing import Dict
|
2022-07-07 14:30:16 +02:00
|
|
|
|
2021-09-11 11:02:48 +02:00
|
|
|
from fastapi.exceptions import HTTPException
|
2022-07-16 14:23:03 +02:00
|
|
|
from loguru import logger
|
2021-09-11 11:02:48 +02:00
|
|
|
|
2021-03-24 00:40:32 -03:00
|
|
|
from lnbits.core.crud import (
|
|
|
|
delete_expired_invoices,
|
2021-04-17 18:27:15 -03:00
|
|
|
get_balance_checks,
|
2022-07-16 14:23:03 +02:00
|
|
|
get_payments,
|
|
|
|
get_standalone_payment,
|
2021-03-24 00:40:32 -03:00
|
|
|
)
|
2021-04-17 18:27:15 -03:00
|
|
|
from lnbits.core.services import redeem_lnurl_withdraw
|
2022-10-05 09:46:59 +02:00
|
|
|
from lnbits.settings import get_wallet_class
|
2020-10-06 00:39:54 -03:00
|
|
|
|
2022-09-09 15:02:07 +03:00
|
|
|
from .core import db
|
|
|
|
|
2021-07-30 19:26:22 -03:00
|
|
|
|
|
|
|
async def catch_everything_and_restart(func):
|
|
|
|
try:
|
|
|
|
await func()
|
2021-08-30 19:55:02 +02:00
|
|
|
except asyncio.CancelledError:
|
2021-07-30 19:26:22 -03:00
|
|
|
raise # because we must pass this up
|
|
|
|
except Exception as exc:
|
2022-07-07 14:30:16 +02:00
|
|
|
logger.error("caught exception in background task:", exc)
|
|
|
|
logger.error(traceback.format_exc())
|
|
|
|
logger.error("will restart the task in 5 seconds.")
|
2021-08-30 19:55:02 +02:00
|
|
|
await asyncio.sleep(5)
|
2021-07-30 19:26:22 -03:00
|
|
|
await catch_everything_and_restart(func)
|
2020-10-06 01:50:55 -03:00
|
|
|
|
|
|
|
|
2020-10-06 00:39:54 -03:00
|
|
|
async def send_push_promise(a, b) -> None:
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2022-10-04 09:51:47 +02:00
|
|
|
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)
|
|
|
|
|
2020-10-06 00:39:54 -03:00
|
|
|
|
2022-10-04 09:51:47 +02:00
|
|
|
invoice_listeners: Dict[str, asyncio.Queue] = SseListenersDict("invoice_listeners")
|
2020-10-06 00:39:54 -03:00
|
|
|
|
2022-10-04 09:51:47 +02:00
|
|
|
|
|
|
|
def register_invoice_listener(send_chan: asyncio.Queue, name: str = None):
|
2020-10-06 00:39:54 -03:00
|
|
|
"""
|
2022-10-04 09:51:47 +02:00
|
|
|
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.
|
2020-10-06 00:39:54 -03:00
|
|
|
"""
|
2022-10-04 09:51:47 +02:00
|
|
|
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
|
2020-10-06 00:39:54 -03:00
|
|
|
|
|
|
|
|
|
|
|
async def webhook_handler():
|
2022-10-04 09:51:47 +02:00
|
|
|
"""
|
|
|
|
Returns the webhook_handler for the selected wallet if present. Used by API.
|
|
|
|
"""
|
2022-10-05 09:46:59 +02:00
|
|
|
WALLET = get_wallet_class()
|
2020-10-06 00:39:54 -03:00
|
|
|
handler = getattr(WALLET, "webhook_listener", None)
|
|
|
|
if handler:
|
|
|
|
return await handler()
|
2021-09-11 11:02:48 +02:00
|
|
|
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
2020-10-06 00:39:54 -03:00
|
|
|
|
|
|
|
|
2022-07-19 18:51:35 +02:00
|
|
|
internal_invoice_queue: asyncio.Queue = asyncio.Queue(0)
|
2020-10-22 15:36:37 -03:00
|
|
|
|
|
|
|
|
2021-07-30 19:26:22 -03:00
|
|
|
async def internal_invoice_listener():
|
2022-10-04 09:51:47 +02:00
|
|
|
"""
|
|
|
|
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.
|
|
|
|
"""
|
2021-08-30 19:55:02 +02:00
|
|
|
while True:
|
|
|
|
checking_id = await internal_invoice_queue.get()
|
2022-10-04 09:51:47 +02:00
|
|
|
logger.info("> got internal payment notification", checking_id)
|
2021-08-30 19:55:02 +02:00
|
|
|
asyncio.create_task(invoice_callback_dispatcher(checking_id))
|
2020-10-22 15:36:37 -03:00
|
|
|
|
|
|
|
|
2021-07-30 19:26:22 -03:00
|
|
|
async def invoice_listener():
|
2022-10-04 09:51:47 +02:00
|
|
|
"""
|
|
|
|
invoice_listener will collect all invoices that come directly
|
|
|
|
from the backend wallet.
|
|
|
|
|
|
|
|
Called by the app startup sequence.
|
|
|
|
"""
|
2022-10-05 09:46:59 +02:00
|
|
|
WALLET = get_wallet_class()
|
2021-03-21 17:57:33 -03:00
|
|
|
async for checking_id in WALLET.paid_invoices_stream():
|
2022-07-07 14:30:16 +02:00
|
|
|
logger.info("> got a payment notification", checking_id)
|
2021-08-30 19:55:02 +02:00
|
|
|
asyncio.create_task(invoice_callback_dispatcher(checking_id))
|
2021-03-21 17:57:33 -03:00
|
|
|
|
|
|
|
|
|
|
|
async def check_pending_payments():
|
2022-10-04 09:51:47 +02:00
|
|
|
"""
|
|
|
|
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.
|
|
|
|
"""
|
2021-03-28 00:11:41 -03:00
|
|
|
outgoing = True
|
|
|
|
incoming = True
|
|
|
|
|
2021-03-21 17:59:54 -03:00
|
|
|
while True:
|
2022-09-09 15:02:07 +03:00
|
|
|
async with db.connect() as conn:
|
2022-11-25 11:32:30 +02:00
|
|
|
logger.info(
|
2022-09-09 15:02:07 +03:00
|
|
|
f"Task: checking all pending payments (incoming={incoming}, outgoing={outgoing}) of last 15 days"
|
|
|
|
)
|
|
|
|
start_time: float = time.time()
|
|
|
|
pending_payments = await get_payments(
|
|
|
|
since=(int(time.time()) - 60 * 60 * 24 * 15), # 15 days ago
|
|
|
|
complete=False,
|
|
|
|
pending=True,
|
|
|
|
outgoing=outgoing,
|
|
|
|
incoming=incoming,
|
|
|
|
exclude_uncheckable=True,
|
|
|
|
conn=conn,
|
|
|
|
)
|
|
|
|
for payment in pending_payments:
|
2022-09-12 20:57:23 +03:00
|
|
|
await payment.check_status(conn=conn)
|
2022-09-09 15:02:07 +03:00
|
|
|
|
2022-11-25 11:32:30 +02:00
|
|
|
logger.info(
|
2022-09-09 16:27:37 +03:00
|
|
|
f"Task: pending check finished for {len(pending_payments)} payments (took {time.time() - start_time:0.3f} s)"
|
2022-09-09 15:02:07 +03:00
|
|
|
)
|
|
|
|
# we delete expired invoices once upon the first pending check
|
|
|
|
if incoming:
|
|
|
|
logger.debug("Task: deleting all expired invoices")
|
|
|
|
start_time: float = time.time()
|
|
|
|
await delete_expired_invoices(conn=conn)
|
2022-11-25 11:32:30 +02:00
|
|
|
logger.info(
|
2022-09-09 15:02:07 +03:00
|
|
|
f"Task: expired invoice deletion finished (took {time.time() - start_time:0.3f} s)"
|
|
|
|
)
|
2022-08-30 13:28:58 +02:00
|
|
|
|
2021-03-28 00:11:41 -03:00
|
|
|
# after the first check we will only check outgoing, not incoming
|
|
|
|
# that will be handled by the global invoice listeners, hopefully
|
|
|
|
incoming = False
|
|
|
|
|
2021-08-30 19:55:02 +02:00
|
|
|
await asyncio.sleep(60 * 30) # every 30 minutes
|
2020-10-06 00:39:54 -03:00
|
|
|
|
|
|
|
|
2021-04-17 18:27:15 -03:00
|
|
|
async def perform_balance_checks():
|
|
|
|
while True:
|
|
|
|
for bc in await get_balance_checks():
|
|
|
|
redeem_lnurl_withdraw(bc.wallet, bc.url)
|
|
|
|
|
2021-08-30 19:55:02 +02:00
|
|
|
await asyncio.sleep(60 * 60 * 6) # every 6 hours
|
2021-04-17 18:27:15 -03:00
|
|
|
|
|
|
|
|
2020-10-06 00:39:54 -03:00
|
|
|
async def invoice_callback_dispatcher(checking_id: str):
|
2022-10-04 09:51:47 +02:00
|
|
|
"""
|
|
|
|
Takes incoming payments, sets pending=False, and dispatches them to
|
|
|
|
invoice_listeners from core and extensions.
|
|
|
|
"""
|
2022-06-03 14:33:31 +02:00
|
|
|
payment = await get_standalone_payment(checking_id, incoming=True)
|
2020-10-06 00:39:54 -03:00
|
|
|
if payment and payment.is_in:
|
2022-10-04 09:51:47 +02:00
|
|
|
logger.trace(f"sse sending invoice callback for payment {checking_id}")
|
2020-11-21 18:04:39 -03:00
|
|
|
await payment.set_pending(False)
|
2022-10-04 09:51:47 +02:00
|
|
|
for chan_name, send_chan in invoice_listeners.items():
|
|
|
|
logger.trace(f"sse sending to chan: {chan_name}")
|
2021-08-30 19:55:02 +02:00
|
|
|
await send_chan.put(payment)
|