[feat] Watchdog and notifications (#2895)

This commit is contained in:
Vlad Stan 2025-01-23 13:23:09 +02:00 committed by GitHub
parent 56a4b702f3
commit b6bdf50ed7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 1276 additions and 461 deletions

View file

@ -24,14 +24,17 @@ from lnbits.core.crud import (
) )
from lnbits.core.crud.extensions import create_installed_extension from lnbits.core.crud.extensions import create_installed_extension
from lnbits.core.helpers import migrate_extension_database from lnbits.core.helpers import migrate_extension_database
from lnbits.core.models.notifications import NotificationType
from lnbits.core.services.extensions import deactivate_extension, get_valid_extensions from lnbits.core.services.extensions import deactivate_extension, get_valid_extensions
from lnbits.core.tasks import ( # watchdog_task from lnbits.core.services.notifications import enqueue_notification
from lnbits.core.tasks import (
audit_queue, audit_queue,
collect_exchange_rates_data, collect_exchange_rates_data,
killswitch_task,
purge_audit_data, purge_audit_data,
run_by_the_minute_tasks,
wait_for_audit_data, wait_for_audit_data,
wait_for_paid_invoices, wait_for_paid_invoices,
wait_notification_messages,
) )
from lnbits.exceptions import register_exception_handlers from lnbits.exceptions import register_exception_handlers
from lnbits.helpers import version_parse from lnbits.helpers import version_parse
@ -102,9 +105,24 @@ async def startup(app: FastAPI):
# initialize tasks # initialize tasks
register_async_tasks() register_async_tasks()
enqueue_notification(
NotificationType.server_start_stop,
{
"message": "LNbits server started.",
"up_time": settings.lnbits_server_up_time,
},
)
async def shutdown(): async def shutdown():
logger.warning("LNbits shutting down...") logger.warning("LNbits shutting down...")
enqueue_notification(
NotificationType.server_start_stop,
{
"message": "LNbits server shutting down...",
"up_time": settings.lnbits_server_up_time,
},
)
settings.lnbits_running = False settings.lnbits_running = False
# shutdown event # shutdown event
@ -444,6 +462,8 @@ async def check_and_register_extensions(app: FastAPI):
def register_async_tasks(): def register_async_tasks():
create_permanent_task(wait_for_audit_data) create_permanent_task(wait_for_audit_data)
create_permanent_task(wait_notification_messages)
create_permanent_task(check_pending_payments) create_permanent_task(check_pending_payments)
create_permanent_task(invoice_listener) create_permanent_task(invoice_listener)
create_permanent_task(internal_invoice_listener) create_permanent_task(internal_invoice_listener)
@ -454,9 +474,7 @@ def register_async_tasks():
register_invoice_listener(invoice_queue, "core") register_invoice_listener(invoice_queue, "core")
create_permanent_task(lambda: wait_for_paid_invoices(invoice_queue)) create_permanent_task(lambda: wait_for_paid_invoices(invoice_queue))
# TODO: implement watchdog properly create_permanent_task(run_by_the_minute_tasks)
# create_permanent_task(watchdog_task)
create_permanent_task(killswitch_task)
create_permanent_task(purge_audit_data) create_permanent_task(purge_audit_data)
create_permanent_task(collect_exchange_rates_data) create_permanent_task(collect_exchange_rates_data)

View file

@ -4,6 +4,7 @@ from typing import Literal, Optional
from lnbits.core.crud.wallets import get_total_balance, get_wallet from lnbits.core.crud.wallets import get_total_balance, get_wallet
from lnbits.core.db import db from lnbits.core.db import db
from lnbits.core.models import PaymentState from lnbits.core.models import PaymentState
from lnbits.core.models.payments import PaymentsStatusCount
from lnbits.db import DB_TYPE, SQLITE, Connection, Filters, Page from lnbits.db import DB_TYPE, SQLITE, Connection, Filters, Page
from ..models import ( from ..models import (
@ -99,6 +100,7 @@ async def get_payments_paginated(
wallet_id: Optional[str] = None, wallet_id: Optional[str] = None,
complete: bool = False, complete: bool = False,
pending: bool = False, pending: bool = False,
failed: bool = False,
outgoing: bool = False, outgoing: bool = False,
incoming: bool = False, incoming: bool = False,
since: Optional[int] = None, since: Optional[int] = None,
@ -107,7 +109,8 @@ async def get_payments_paginated(
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> Page[Payment]: ) -> Page[Payment]:
""" """
Filters payments to be returned by complete | pending | outgoing | incoming. Filters payments to be returned by:
- complete | pending | failed | outgoing | incoming.
""" """
values: dict = { values: dict = {
@ -137,6 +140,8 @@ async def get_payments_paginated(
) )
elif pending: elif pending:
clause.append(f"status = '{PaymentState.PENDING}'") clause.append(f"status = '{PaymentState.PENDING}'")
elif failed:
clause.append(f"status = '{PaymentState.FAILED}'")
if outgoing and incoming: if outgoing and incoming:
pass pass
@ -200,6 +205,21 @@ async def get_payments(
return page.data return page.data
async def get_payments_status_count() -> PaymentsStatusCount:
empty_page: Filters = Filters(limit=0)
in_payments = await get_payments_paginated(incoming=True, filters=empty_page)
out_payments = await get_payments_paginated(outgoing=True, filters=empty_page)
pending_payments = await get_payments_paginated(pending=True, filters=empty_page)
failed_payments = await get_payments_paginated(failed=True, filters=empty_page)
return PaymentsStatusCount(
incoming=in_payments.total,
outgoing=out_payments.total,
pending=pending_payments.total,
failed=failed_payments.total,
)
async def delete_expired_invoices( async def delete_expired_invoices(
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> None: ) -> None:

View file

@ -135,6 +135,12 @@ async def get_wallets(
) )
async def get_wallets_count():
result = await db.execute("SELECT COUNT(*) as count FROM wallets")
row = result.mappings().first()
return row.get("count", 0)
async def get_wallet_for_key( async def get_wallet_for_key(
key: str, key: str,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
@ -153,6 +159,6 @@ async def get_wallet_for_key(
async def get_total_balance(conn: Optional[Connection] = None): async def get_total_balance(conn: Optional[Connection] = None):
result = await (conn or db).execute("SELECT SUM(balance) FROM balances") result = await (conn or db).execute("SELECT SUM(balance) as balance FROM balances")
row = result.mappings().first() row = result.mappings().first()
return row.get("balance", 0) return row.get("balance", 0)

View file

@ -25,12 +25,12 @@ class Callback(BaseModel):
class BalanceDelta(BaseModel): class BalanceDelta(BaseModel):
lnbits_balance_msats: int lnbits_balance_sats: int
node_balance_msats: int node_balance_sats: int
@property @property
def delta_msats(self): def delta_sats(self) -> int:
return self.node_balance_msats - self.lnbits_balance_msats return int(self.lnbits_balance_sats - self.node_balance_sats)
class SimpleStatus(BaseModel): class SimpleStatus(BaseModel):

View file

@ -0,0 +1,64 @@
from enum import Enum
from pydantic import BaseModel
class NotificationType(Enum):
server_status = "server_status"
settings_update = "settings_update"
balance_update = "balance_update"
watchdog_check = "watchdog_check"
balance_delta = "balance_delta"
server_start_stop = "server_start_stop"
incoming_payment = "incoming_payment"
outgoing_payment = "outgoing_payment"
text_message = "text_message"
class NotificationMessage(BaseModel):
message_type: NotificationType
values: dict
NOTIFICATION_TEMPLATES = {
"text_message": "{message}",
"server_status": """*SERVER STATUS*
*Up time*: `{up_time}`.
*Accounts*: `{accounts_count}`.
*Wallets*: `{wallets_count}`.
*In/Out payments*: `{in_payments_count}`/`{out_payments_count}`.
*Pending payments*: `{pending_payments_count}`.
*Failed payments*: `{failed_payments_count}`.
*LNbits balance*: `{lnbits_balance_sats}` sats.""",
"server_start_stop": """*SERVER*
{message}
*Time*: `{up_time}` seconds.
""",
"settings_update": """*SETTINGS UPDATED*
User: `{username}`.
""",
"balance_update": """*WALLET CREDIT/DEBIT*
Wallet `{wallet_name}` balance updated with `{amount}` sats.
*Current balance*: `{balance}` sats.
*Wallet ID*: `{wallet_id}`
""",
"watchdog_check": """*WATCHDOG BALANCE CHECK*
*Delta*: `{delta_sats}` sats.
*LNbits balance*: `{lnbits_balance_sats}` sats.
*Node balance*: `{node_balance_sats}` sats.
*Switching to Void Wallet*: `{switch_to_void_wallet}`.
""",
"balance_delta": """*BALANCE DELTA CHANGED*
*New delta*: `{delta_sats}` sats.
*Old delta*: `{old_delta_sats}` sats.
*LNbits balance*: `{lnbits_balance_sats}` sats.
*Node balance*: `{node_balance_sats}` sats.
""",
"outgoing_payment": """*PAYMENT SENT*
*Amount*: {fiat_value_fmt}`{amount_sats}`*sats*.
*Wallet*: `{wallet_name}` ({wallet_id}).""",
"incoming_payment": """*PAYMENT RECEIVED*
*Amount*: {fiat_value_fmt}`{amount_sats}`*sats*.
*Wallet*: `{wallet_name}` ({wallet_id}).
""",
}

View file

@ -175,3 +175,10 @@ class CreateInvoice(BaseModel):
if v != "sat" and v not in allowed_currencies(): if v != "sat" and v not in allowed_currencies():
raise ValueError("The provided unit is not supported") raise ValueError("The provided unit is not supported")
return v return v
class PaymentsStatusCount(BaseModel):
incoming: int = 0
outgoing: int = 0
failed: int = 0
pending: int = 0

View file

@ -3,6 +3,7 @@ from .funding_source import (
switch_to_voidwallet, switch_to_voidwallet,
) )
from .lnurl import perform_lnurlauth, redeem_lnurl_withdraw from .lnurl import perform_lnurlauth, redeem_lnurl_withdraw
from .notifications import enqueue_notification
from .payments import ( from .payments import (
calculate_fiat_amounts, calculate_fiat_amounts,
check_transaction_status, check_transaction_status,
@ -37,6 +38,8 @@ __all__ = [
# lnurl # lnurl
"redeem_lnurl_withdraw", "redeem_lnurl_withdraw",
"perform_lnurlauth", "perform_lnurlauth",
# notifications
"enqueue_notification",
# payments # payments
"calculate_fiat_amounts", "calculate_fiat_amounts",
"check_transaction_status", "check_transaction_status",

View file

@ -1,3 +1,7 @@
from loguru import logger
from lnbits.core.models.notifications import NotificationType
from lnbits.core.services.notifications import enqueue_notification
from lnbits.settings import settings from lnbits.settings import settings
from lnbits.wallets import get_funding_source, set_funding_source from lnbits.wallets import get_funding_source, set_funding_source
@ -18,6 +22,62 @@ async def get_balance_delta() -> BalanceDelta:
status = await funding_source.status() status = await funding_source.status()
lnbits_balance = await get_total_balance() lnbits_balance = await get_total_balance()
return BalanceDelta( return BalanceDelta(
lnbits_balance_msats=lnbits_balance, lnbits_balance_sats=lnbits_balance // 1000,
node_balance_msats=status.balance_msat, node_balance_sats=status.balance_msat // 1000,
) )
async def check_server_balance_against_node():
"""
Watchdog will check lnbits balance and nodebalance
and will switch to VoidWallet if the watchdog delta is reached.
"""
if (
not settings.lnbits_watchdog_switch_to_voidwallet
and not settings.lnbits_notification_watchdog
):
return
funding_source = get_funding_source()
if funding_source.__class__.__name__ == "VoidWallet":
return
status = await get_balance_delta()
if status.delta_sats < settings.lnbits_watchdog_delta:
return
use_voidwallet = settings.lnbits_watchdog_switch_to_voidwallet
logger.warning(
f"Balance delta reached: {status.delta_sats} sats."
f" Switch to void wallet: {use_voidwallet}."
)
enqueue_notification(
NotificationType.watchdog_check,
{
"delta_sats": status.delta_sats,
"lnbits_balance_sats": status.lnbits_balance_sats,
"node_balance_sats": status.node_balance_sats,
"switch_to_void_wallet": use_voidwallet,
},
)
if use_voidwallet:
logger.error(f"Switching to VoidWallet. Delta: {status.delta_sats} sats.")
await switch_to_voidwallet()
async def check_balance_delta_changed():
status = await get_balance_delta()
if settings.latest_balance_delta_sats is None:
settings.latest_balance_delta_sats = status.delta_sats
return
if status.delta_sats != settings.latest_balance_delta_sats:
enqueue_notification(
NotificationType.balance_delta,
{
"delta_sats": status.delta_sats,
"old_delta_sats": settings.latest_balance_delta_sats,
"lnbits_balance_sats": status.lnbits_balance_sats,
"node_balance_sats": status.node_balance_sats,
},
)
settings.latest_balance_delta_sats = status.delta_sats

View file

@ -0,0 +1,73 @@
import asyncio
from typing import Tuple
import httpx
from loguru import logger
from pynostr.encrypted_dm import EncryptedDirectMessage
from websocket import WebSocket, create_connection
from lnbits.core.helpers import is_valid_url
from lnbits.utils.nostr import (
validate_identifier,
validate_pub_key,
)
async def send_nostr_dm(
from_private_key_hex: str,
to_pubkey_hex: str,
message: str,
relays: list[str],
) -> dict:
dm = EncryptedDirectMessage()
dm.encrypt(
private_key_hex=from_private_key_hex,
recipient_pubkey=to_pubkey_hex,
cleartext_content=message,
)
dm_event = dm.to_event()
dm_event.sign(private_key_hex=from_private_key_hex)
notification = dm_event.to_message()
ws_connections: list[WebSocket] = []
for relay in relays:
try:
ws = create_connection(relay, timeout=2)
ws.send(notification)
ws_connections.append(ws)
except Exception as e:
logger.warning(f"Error sending notification to relay {relay}: {e}")
await asyncio.sleep(1)
for ws in ws_connections:
try:
ws.close()
except Exception as e:
logger.debug(f"Failed to close websocket connection: {e}")
return dm_event.to_dict()
async def fetch_nip5_details(identifier: str) -> Tuple[str, list[str]]:
identifier, domain = identifier.split("@")
if not identifier or not domain:
raise ValueError("Invalid NIP5 identifier")
if not is_valid_url(f"https://{domain}"):
raise ValueError("Invalid NIP5 domain")
validate_identifier(identifier)
url = f"https://{domain}/.well-known/nostr.json?name={identifier}"
async with httpx.AsyncClient() as client:
resp = await client.get(url)
resp.raise_for_status()
data = resp.json()
if "names" not in data or identifier not in data["names"]:
raise ValueError("NIP5 not name found")
pubkey = data["names"][identifier]
validate_pub_key(pubkey)
relays = data["relays"].get(pubkey, []) if "relays" in data else []
return pubkey, relays

View file

@ -0,0 +1,125 @@
import asyncio
from typing import Optional, Tuple
import httpx
from loguru import logger
from lnbits.core.models.notifications import (
NOTIFICATION_TEMPLATES,
NotificationMessage,
NotificationType,
)
from lnbits.core.services.nostr import fetch_nip5_details, send_nostr_dm
from lnbits.settings import settings
from lnbits.utils.nostr import normalize_private_key
notifications_queue: asyncio.Queue = asyncio.Queue()
def enqueue_notification(message_type: NotificationType, values: dict) -> None:
if not is_message_type_enabled(message_type):
return
try:
notifications_queue.put_nowait(
NotificationMessage(message_type=message_type, values=values)
)
except Exception as e:
logger.error(f"Error enqueuing notification: {e}")
async def process_next_notification():
notification_message: NotificationMessage = await notifications_queue.get()
message_type, text = _notification_message_to_text(notification_message)
await send_notification(text, message_type)
async def send_notification(
message: str,
message_type: Optional[str] = None,
) -> None:
try:
if settings.lnbits_telegram_notifications_enabled:
await send_telegram_notification(message)
logger.debug(f"Sent telegram notification: {message_type}")
except Exception as e:
logger.error(f"Error sending telegram notification {message_type}: {e}")
try:
if settings.lnbits_nostr_notifications_enabled:
await send_nostr_notification(message)
logger.debug(f"Sent nostr notification: {message_type}")
except Exception as e:
logger.error(f"Error sending nostr notification {message_type}: {e}")
async def send_nostr_notification(message: str) -> dict:
for i in settings.lnbits_nostr_notifications_identifiers:
try:
identifier = await fetch_nip5_details(i)
user_pubkey = identifier[0]
relays = identifier[1]
server_private_key = normalize_private_key(
settings.lnbits_nostr_notifications_private_key
)
await send_nostr_dm(
server_private_key,
user_pubkey,
message,
relays,
)
except Exception as e:
logger.warning(f"Error notifying identifier {i}: {e}")
return {"status": "ok"}
async def send_telegram_notification(message: str) -> dict:
return await send_telegram_message(
settings.lnbits_telegram_notifications_access_token,
settings.lnbits_telegram_notifications_chat_id,
message,
)
async def send_telegram_message(token: str, chat_id: str, message: str) -> dict:
url = f"https://api.telegram.org/bot{token}/sendMessage"
payload = {"chat_id": chat_id, "text": message, "parse_mode": "markdown"}
async with httpx.AsyncClient() as client:
response = await client.post(url, data=payload)
response.raise_for_status()
return response.json()
def is_message_type_enabled(message_type: NotificationType) -> bool:
if message_type == NotificationType.balance_update:
return settings.lnbits_notification_credit_debit
if message_type == NotificationType.settings_update:
return settings.lnbits_notification_settings_update
if message_type == NotificationType.watchdog_check:
return settings.lnbits_notification_watchdog
if message_type == NotificationType.balance_delta:
return settings.notification_balance_delta_changed
if message_type == NotificationType.server_start_stop:
return settings.lnbits_notification_server_start_stop
if message_type == NotificationType.server_status:
return settings.lnbits_notification_server_status_hours > 0
if message_type == NotificationType.incoming_payment:
return settings.lnbits_notification_incoming_payment_amount_sats > 0
if message_type == NotificationType.outgoing_payment:
return settings.lnbits_notification_outgoing_payment_amount_sats > 0
return False
def _notification_message_to_text(
notification_message: NotificationMessage,
) -> Tuple[str, str]:
message_type = notification_message.message_type.value
meesage_value = NOTIFICATION_TEMPLATES.get(message_type, message_type)
try:
text = meesage_value.format(**notification_message.values)
except Exception as e:
logger.warning(f"Error formatting notification message: {e}")
text = meesage_value
text = f"""[{settings.lnbits_site_title}]\n{text}"""
return message_type, text

View file

@ -8,6 +8,8 @@ from bolt11 import encode as bolt11_encode
from loguru import logger from loguru import logger
from lnbits.core.db import db from lnbits.core.db import db
from lnbits.core.models.notifications import NotificationType
from lnbits.core.services.notifications import enqueue_notification
from lnbits.db import Connection from lnbits.db import Connection
from lnbits.decorators import check_user_extension_access from lnbits.decorators import check_user_extension_access
from lnbits.exceptions import InvoiceError, PaymentError from lnbits.exceptions import InvoiceError, PaymentError
@ -262,6 +264,18 @@ async def update_wallet_balance(
async def send_payment_notification(wallet: Wallet, payment: Payment): async def send_payment_notification(wallet: Wallet, payment: Payment):
try:
await send_ws_payment_notification(wallet, payment)
except Exception as e:
logger.error("Error sending websocket payment notification", e)
try:
send_chat_payment_notification(wallet, payment)
except Exception as e:
logger.error("Error sending chat payment notification", e)
async def send_ws_payment_notification(wallet: Wallet, payment: Payment):
# TODO: websocket message should be a clean payment model # TODO: websocket message should be a clean payment model
# await websocket_manager.send_data(payment.json(), wallet.inkey) # await websocket_manager.send_data(payment.json(), wallet.inkey)
# TODO: figure out why we send the balance with the payment here. # TODO: figure out why we send the balance with the payment here.
@ -282,6 +296,27 @@ async def send_payment_notification(wallet: Wallet, payment: Payment):
) )
def send_chat_payment_notification(wallet: Wallet, payment: Payment):
amount_sats = abs(payment.sat)
values: dict = {
"wallet_id": wallet.id,
"wallet_name": wallet.name,
"amount_sats": amount_sats,
"fiat_value_fmt": "",
}
if payment.extra.get("wallet_fiat_currency", None):
amount_fiat = payment.extra.get("wallet_fiat_amount", None)
currency = payment.extra.get("wallet_fiat_currency", None)
values["fiat_value_fmt"] = f"`{amount_fiat}`*{currency}* / "
if payment.is_out:
if amount_sats >= settings.lnbits_notification_outgoing_payment_amount_sats:
enqueue_notification(NotificationType.outgoing_payment, values)
else:
if amount_sats >= settings.lnbits_notification_incoming_payment_amount_sats:
enqueue_notification(NotificationType.incoming_payment, values)
async def check_wallet_limits( async def check_wallet_limits(
wallet_id: str, amount_msat: int, conn: Optional[Connection] = None wallet_id: str, amount_msat: int, conn: Optional[Connection] = None
): ):
@ -354,6 +389,7 @@ async def calculate_fiat_amounts(
fiat_amounts["fiat_currency"] = currency fiat_amounts["fiat_currency"] = currency
fiat_amounts["fiat_amount"] = round(amount, ndigits=3) fiat_amounts["fiat_amount"] = round(amount, ndigits=3)
fiat_amounts["fiat_rate"] = amount_sat / amount fiat_amounts["fiat_rate"] = amount_sat / amount
fiat_amounts["btc_rate"] = (amount / amount_sat) * 100_000_000
else: else:
amount_sat = int(amount) amount_sat = int(amount)
@ -365,6 +401,7 @@ async def calculate_fiat_amounts(
fiat_amounts["wallet_fiat_currency"] = wallet_currency fiat_amounts["wallet_fiat_currency"] = wallet_currency
fiat_amounts["wallet_fiat_amount"] = round(fiat_amount, ndigits=3) fiat_amounts["wallet_fiat_amount"] = round(fiat_amount, ndigits=3)
fiat_amounts["wallet_fiat_rate"] = amount_sat / fiat_amount fiat_amounts["wallet_fiat_rate"] = amount_sat / fiat_amount
fiat_amounts["wallet_btc_rate"] = (fiat_amount / amount_sat) * 100_000_000
logger.debug( logger.debug(
f"Calculated fiat amounts {wallet.id=} {amount=} {currency=}: {fiat_amounts=}" f"Calculated fiat amounts {wallet.id=} {amount=} {currency=}: {fiat_amounts=}"

View file

@ -1,4 +1,6 @@
import asyncio import asyncio
import traceback
from typing import Callable, Coroutine
import httpx import httpx
from loguru import logger from loguru import logger
@ -10,70 +12,78 @@ from lnbits.core.crud import (
mark_webhook_sent, mark_webhook_sent,
) )
from lnbits.core.crud.audit import delete_expired_audit_entries from lnbits.core.crud.audit import delete_expired_audit_entries
from lnbits.core.crud.payments import get_payments_status_count
from lnbits.core.crud.users import get_accounts
from lnbits.core.crud.wallets import get_wallets_count
from lnbits.core.models import AuditEntry, Payment from lnbits.core.models import AuditEntry, Payment
from lnbits.core.models.notifications import NotificationType
from lnbits.core.services import ( from lnbits.core.services import (
get_balance_delta,
send_payment_notification, send_payment_notification,
switch_to_voidwallet,
) )
from lnbits.settings import get_funding_source, settings from lnbits.core.services.funding_source import (
from lnbits.tasks import send_push_notification check_balance_delta_changed,
check_server_balance_against_node,
get_balance_delta,
)
from lnbits.core.services.notifications import (
enqueue_notification,
process_next_notification,
)
from lnbits.db import Filters
from lnbits.settings import settings
from lnbits.tasks import create_unique_task, send_push_notification
from lnbits.utils.exchange_rates import btc_rates from lnbits.utils.exchange_rates import btc_rates
audit_queue: asyncio.Queue = asyncio.Queue() audit_queue: asyncio.Queue = asyncio.Queue()
async def killswitch_task(): async def run_by_the_minute_tasks():
""" minute_counter = 0
killswitch will check lnbits-status repository for a signal from
LNbits and will switch to VoidWallet if the killswitch is triggered.
"""
while settings.lnbits_running: while settings.lnbits_running:
funding_source = get_funding_source() status_minutes = settings.lnbits_notification_server_status_hours * 60
if (
settings.lnbits_killswitch
and funding_source.__class__.__name__ != "VoidWallet"
):
with httpx.Client() as client:
try:
r = client.get(settings.lnbits_status_manifest, timeout=4)
r.raise_for_status()
if r.status_code == 200:
ks = r.json().get("killswitch")
if ks and ks == 1:
logger.error(
"Switching to VoidWallet. Killswitch triggered."
)
await switch_to_voidwallet()
except (httpx.RequestError, httpx.HTTPStatusError):
logger.error(
"Cannot fetch lnbits status manifest."
f" {settings.lnbits_status_manifest}"
)
await asyncio.sleep(settings.lnbits_killswitch_interval * 60)
if settings.notification_balance_delta_changed:
async def watchdog_task():
"""
Registers a watchdog which will check lnbits balance and nodebalance
and will switch to VoidWallet if the watchdog delta is reached.
"""
while settings.lnbits_running:
funding_source = get_funding_source()
if (
settings.lnbits_watchdog
and funding_source.__class__.__name__ != "VoidWallet"
):
try: try:
balance = await get_balance_delta() # runs by default every minute, the delta should not change that often
delta = balance.delta_msats await check_balance_delta_changed()
logger.debug(f"Running watchdog task. current delta: {delta}") except Exception as ex:
if delta + settings.lnbits_watchdog_delta <= 0: logger.error(ex)
logger.error(f"Switching to VoidWallet. current delta: {delta}")
await switch_to_voidwallet() if minute_counter % settings.lnbits_watchdog_interval_minutes == 0:
except Exception as e: try:
logger.error("Error in watchdog task", e) await check_server_balance_against_node()
await asyncio.sleep(settings.lnbits_watchdog_interval * 60) except Exception as ex:
logger.error(ex)
if minute_counter % status_minutes == 0:
try:
await _notify_server_status()
except Exception as ex:
logger.error(ex)
minute_counter += 1
await asyncio.sleep(60)
async def _notify_server_status():
accounts = await get_accounts(filters=Filters(limit=0))
wallets_count = await get_wallets_count()
payments = await get_payments_status_count()
status = await get_balance_delta()
values = {
"up_time": settings.lnbits_server_up_time,
"accounts_count": accounts.total,
"wallets_count": wallets_count,
"in_payments_count": payments.incoming,
"out_payments_count": payments.outgoing,
"pending_payments_count": payments.pending,
"failed_payments_count": payments.failed,
"delta_sats": status.delta_sats,
"lnbits_balance_sats": status.lnbits_balance_sats,
"node_balance_sats": status.node_balance_sats,
}
enqueue_notification(NotificationType.server_status, values)
async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue): async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
@ -158,6 +168,16 @@ async def wait_for_audit_data():
await asyncio.sleep(3) await asyncio.sleep(3)
async def wait_notification_messages():
while settings.lnbits_running:
try:
await process_next_notification()
except Exception as ex:
logger.log("error", ex)
await asyncio.sleep(3)
async def purge_audit_data(): async def purge_audit_data():
""" """
Remove audit entries which have passed their retention period. Remove audit entries which have passed their retention period.
@ -194,3 +214,14 @@ async def collect_exchange_rates_data():
else: else:
sleep_time = 60 sleep_time = 60
await asyncio.sleep(sleep_time) await asyncio.sleep(sleep_time)
def _create_unique_task(name: str, func: Callable):
async def _to_coro(func: Callable[[], Coroutine]) -> Coroutine:
return await func()
try:
create_unique_task(name, _to_coro(func))
except Exception as e:
logger.error(f"Error in {name} task", e)
logger.error(traceback.format_exc())

View file

@ -15,16 +15,16 @@
v-text="$t('funding_source', {wallet_class: settings.lnbits_backend_wallet_class})" v-text="$t('funding_source', {wallet_class: settings.lnbits_backend_wallet_class})"
></li> ></li>
<li <li
v-text="$t('node_balance', {balance: (auditData.node_balance_msats / 1000).toLocaleString()})" v-text="$t('node_balance', {balance: (auditData.node_balance_sats || 0).toLocaleString()})"
></li> ></li>
<li <li
v-text="$t('lnbits_balance', {balance: (auditData.lnbits_balance_msats / 1000).toLocaleString()})" v-text="$t('lnbits_balance', {balance: (auditData.lnbits_balance_sats || 0).toLocaleString()})"
></li> ></li>
<li <li
v-text="$t('funding_reserve_percent', { v-text="$t('funding_reserve_percent', {
percent: auditData.lnbits_balance_msats > 0 percent: auditData.lnbits_balance_sats > 0
? (auditData.node_balance_msats / auditData.lnbits_balance_msats * 100).toFixed(2) ? (auditData.node_balance_sats / auditData.lnbits_balance_sats * 100).toFixed(2)
: 100 : 100
})" })"
></li> ></li>
</ul> </ul>
@ -96,6 +96,90 @@
:allowed-funding-sources="settings.lnbits_allowed_funding_sources" :allowed-funding-sources="settings.lnbits_allowed_funding_sources"
/> />
</div> </div>
<q-separator></q-separator>
<h6 class="q-mt-lg q-mb-sm">
<p v-text="$t('watchdog')"></p>
</h6>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label v-text="$t('enable_watchdog')"></q-item-label>
<q-item-label
caption
v-text="$t('enable_watchdog_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_watchdog_switch_to_voidwallet"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
</div>
<div class="col-12 col-md-6">
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notification_watchdog_limit')"
></q-item-label>
<q-item-label
caption
v-text="$t('notification_watchdog_limit_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_notification_watchdog"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
</div>
<div class="col-12 col-md-6">
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label v-text="$t('watchdog_interval')"></q-item-label>
<q-item-label
caption
v-text="$t('watchdog_interval_desc')"
></q-item-label>
</q-item-section>
<q-item-section>
<q-input
filled
v-model="formData.lnbits_watchdog_interval_minutes"
type="number"
/>
</q-item-section>
</q-item>
</div>
<div class="col-12 col-md-6">
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label v-text="$t('watchdog_delta')"></q-item-label>
<q-item-label
caption
v-text="$t('watchdog_delta_desc')"
></q-item-label>
</q-item-section>
<q-item-section>
<q-input
filled
v-model="formData.lnbits_watchdog_delta"
type="number"
/>
</q-item-section>
</q-item>
</div>
</div>
</div> </div>
</q-card-section> </q-card-section>
</q-tab-panel> </q-tab-panel>

View file

@ -1,139 +1,328 @@
<q-tab-panel name="notifications"> <q-tab-panel name="notifications">
<q-card-section class="q-pa-none"> <q-card-section class="q-pa-none">
<div> <h6 class="q-my-none q-mb-sm">
<div class="row q-col-gutter-md"> <span v-text="$t('notifications_configure')"></span>
<div class="col-12 col-md-6"> </h6>
<q-item tag="label" v-ripple> <q-separator class="q-mt-md q-mb-sm"></q-separator>
<q-item-section> <div class="row q-col-gutter-md">
<q-item-label v-text="$t('enable_notifications')"></q-item-label> <div class="col-sm-12 col-md-6">
<q-item-label <strong v-text="$t('notifications_nostr_config')"></strong>
caption <q-item tag="label" v-ripple>
v-text="$t('enable_notifications_desc')" <q-item-section>
></q-item-label> <q-item-label
</q-item-section> v-text="$t('notifications_enable_nostr')"
<q-item-section avatar> ></q-item-label>
<q-toggle <q-item-label
size="md" caption
v-model="formData.lnbits_notifications" v-text="$t('notifications_enable_nostr_desc')"
checked-icon="check" ></q-item-label>
color="green" </q-item-section>
unchecked-icon="clear" <q-item-section avatar>
/> <q-toggle
</q-item-section> size="md"
</q-item> v-model="formData.lnbits_nostr_notifications_enabled"
<br /> checked-icon="check"
<p color="green"
v-if="!formData.lnbits_notifications" unchecked-icon="clear"
v-text="$t('notifications_disabled')" />
></p> </q-item-section>
<div v-if="formData.lnbits_notifications"> </q-item>
{% include "admin/_tab_security_notifications.html" %} <q-item tag="label" v-ripple>
</div> <q-item-section>
<br /> <q-item-label
<div> v-text="$t('notifications_nostr_private_key')"
<p v-text="$t('notification_source')"></p> ></q-item-label>
<q-item-label
caption
v-text="$t('notifications_nostr_private_key_desc')"
></q-item-label>
</q-item-section>
<q-item-section>
<q-input
type="password"
filled
v-model="formData.lnbits_nostr_notifications_private_key"
/>
</q-item-section>
</q-item>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notifications_nostr_identifiers')"
></q-item-label>
<q-item-label
caption
v-text="$t('notifications_nostr_identifiers_desc')"
></q-item-label>
</q-item-section>
<q-item-section>
<q-input <q-input
filled filled
v-model="formData.lnbits_status_manifest" v-model="nostrNotificationIdentifier"
type="text" @keydown.enter="addNostrNotificationIdentifier"
:label="$t('notification_source_label')" >
<q-btn
@click="addNostrNotificationIdentifier()"
dense
flat
icon="add"
></q-btn>
</q-input>
<div>
<q-chip
v-for="identifier in formData.lnbits_nostr_notifications_identifiers"
:key="identifier"
removable
@remove="removeNostrNotificationIdentifier(identifier)"
color="primary"
text-color="white"
><span class="ellipsis" v-text="identifier"></span
></q-chip>
</div>
</q-item-section>
</q-item>
</div>
<div class="col-sm-12 col-md-6">
<strong v-text="$t('notifications_telegram_config')"></strong>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notifications_enable_telegram')"
></q-item-label>
<q-item-label
caption
v-text="$t('notifications_enable_telegram_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_telegram_notifications_enabled"
checked-icon="check"
color="green"
unchecked-icon="clear"
/> />
</div> </q-item-section>
<br /> </q-item>
</div> <q-item tag="label" v-ripple>
<div class="col-12 col-md-6"> <q-item-section>
<p v-text="$t('killswitch')"></p> <q-item-label
<q-item tag="label" v-ripple> v-text="$t('notifications_telegram_access_token')"
<q-item-section> ></q-item-label>
<q-item-label v-text="$t('enable_killswitch')"></q-item-label> <q-item-label
<q-item-label caption
caption v-text="$t('notifications_telegram_access_token_desc')"
v-text="$t('enable_killswitch_desc')" ></q-item-label>
></q-item-label> </q-item-section>
</q-item-section> <q-item-section>
<q-item-section avatar> <q-input
<q-toggle type="password"
size="md" filled
v-model="formData.lnbits_killswitch" v-model="formData.lnbits_telegram_notifications_access_token"
checked-icon="check" />
color="green" </q-item-section>
unchecked-icon="clear" </q-item>
/> <q-item tag="label" v-ripple>
</q-item-section> <q-item-section>
</q-item> <q-item-label v-text="$t('notifications_chat_id')"></q-item-label>
<q-item tag="label" v-ripple> <q-item-label
<q-item-section> caption
<q-item-label v-text="$t('killswitch_interval')"></q-item-label> v-text="$t('notifications_chat_id_desc')"
<q-item-label ></q-item-label>
caption </q-item-section>
v-text="$t('killswitch_interval_desc')" <q-item-section>
></q-item-label> <q-input
</q-item-section> filled
<q-item-section> v-model="formData.lnbits_telegram_notifications_chat_id"
<q-input />
filled </q-item-section>
v-model="formData.lnbits_killswitch_interval" </q-item>
type="number" </div>
/> </div>
</q-item-section> <q-separator> </q-separator>
</q-item> <h6 class="q-mb-sm">
<br /> <span v-text="$t('notifications')"></span>
<p v-text="$t('watchdog')"></p> </h6>
<q-item disabled tag="label" v-ripple> <div class="row q-col-gutter-md">
<q-tooltip><span v-text="$t('coming_soon')"></span></q-tooltip> <div class="col-12">
<q-item-section> <q-item tag="label" v-ripple>
<q-item-label v-text="$t('enable_watchdog')"></q-item-label> <q-item-section>
<q-item-label <q-item-label
caption v-text="$t('notification_settings_update')"
v-text="$t('enable_watchdog_desc')" ></q-item-label>
></q-item-label> <q-item-label
</q-item-section> caption
<q-item-section avatar> v-text="$t('notification_settings_update_desc')"
<q-toggle ></q-item-label>
size="md" </q-item-section>
v-model="formData.lnbits_watchdog" <q-item-section avatar>
checked-icon="check" <q-toggle
color="green" size="md"
unchecked-icon="clear" v-model="formData.lnbits_notification_settings_update"
/> checked-icon="check"
</q-item-section> color="green"
</q-item> unchecked-icon="clear"
<q-item disabled tag="label" v-ripple> />
<q-tooltip><span v-text="$t('coming_soon')"></span></q-tooltip> </q-item-section>
<q-item-section> </q-item>
<q-item-label v-text="$t('watchdog_interval')"></q-item-label> </div>
<q-item-label <div class="col-12">
caption <q-item tag="label" v-ripple>
v-text="$t('watchdog_interval_desc')" <q-item-section>
></q-item-label> <q-item-label
</q-item-section> v-text="$t('notification_credit_debit')"
<q-item-section> ></q-item-label>
<q-input <q-item-label
filled caption
v-model="formData.lnbits_watchdog_interval" v-text="$t('notification_credit_debit_desc')"
type="number" ></q-item-label>
/> </q-item-section>
</q-item-section> <q-item-section avatar>
</q-item> <q-toggle
<q-item disabled tag="label" v-ripple> size="md"
<q-tooltip><span v-text="$t('coming_soon')"></span></q-tooltip> v-model="formData.lnbits_notification_credit_debit"
<q-item-section> checked-icon="check"
<q-item-label v-text="$t('watchdog_delta')"></q-item-label> color="green"
<q-item-label unchecked-icon="clear"
caption />
v-text="$t('watchdog_delta_desc')" </q-item-section>
></q-item-label> </q-item>
</q-item-section> </div>
<q-item-section> <div class="col-12">
<q-input <q-item tag="label" v-ripple>
filled <q-item-section>
v-model="formData.lnbits_watchdog_delta" <q-item-label
type="number" v-text="$t('notification_server_start_stop')"
/> ></q-item-label>
</q-item-section> <q-item-label
</q-item> caption
<br /> v-text="$t('notification_server_start_stop_desc')"
</div> ></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_notification_server_start_stop"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
</div>
<div class="col-12">
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notification_balance_delta_changed')"
></q-item-label>
<q-item-label
caption
v-text="$t('notification_balance_delta_changed_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.notification_balance_delta_changed"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
</div>
<div class="col-12">
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notification_watchdog_limit')"
></q-item-label>
<q-item-label
caption
v-text="$t('notification_watchdog_limit_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_notification_watchdog"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
</div>
<div class="col-12">
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notification_server_status')"
></q-item-label>
<q-item-label
caption
v-text="$t('notification_server_status_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-input
class="flow-right"
type="number"
min="0"
filled
v-model="formData.lnbits_notification_server_status_hours"
/>
</q-item-section>
</q-item>
</div>
<div class="col-12">
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notification_incoming_payment')"
></q-item-label>
<q-item-label
caption
v-text="$t('notification_incoming_payment_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-input
class="flow-right"
type="number"
min="0"
filled
v-model="formData.lnbits_notification_incoming_payment_amount_sats"
/>
</q-item-section>
</q-item>
</div>
<div class="col-12">
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notification_outgoing_payment')"
></q-item-label>
<q-item-label
caption
v-text="$t('notification_outgoing_payment_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-input
class="flow-right"
type="number"
min="0"
filled
v-model="formData.lnbits_notification_outgoing_payment_amount_sats"
/>
</q-item-section>
</q-item>
</div> </div>
</div> </div>
</q-card-section> </q-card-section>

View file

@ -1,77 +0,0 @@
<q-banner v-if="updateAvailable" class="bg-primary text-white">
<q-icon size="28px" name="update"></q-icon>
<span v-text="$t('update_available', {version: statusData.version})"></span>
<template v-slot:action>
<a
class="q-btn"
color="white"
target="_blank"
rel="noopener noreferrer"
href="https://github.com/lnbits/lnbits/releases"
v-text="$t('releases')"
></a>
</template>
</q-banner>
<q-banner v-if="!updateAvailable" class="bg-green text-white">
<q-icon size="28px" name="checkmark"></q-icon>
<span v-text="$t('latest_update', {version: statusData.version})"></span>
</q-banner>
<q-card>
<q-card-section>
<q-table
dense
flat
:rows="statusData.notifications"
:columns="statusDataTable.columns"
:no-data-label="$t('no_notifications')"
>
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width> </q-th>
<q-th
v-for="col in props.cols"
:key="col.name"
:props="props"
v-text="col.label"
></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width class="text-center">
<q-icon
v-if="props.row.type === 'update'"
size="18px"
name="update"
color="green"
></q-icon>
<q-icon
v-if="props.row.type === 'warning'"
size="18px"
name="warning"
color="red"
></q-icon>
</q-td>
<q-td
auto-width
key="date"
:props="props"
v-text="formatDate(props.row.date)"
>
</q-td>
<q-td key="message" :props="props"
><span v-text="props.row.message"></span
><a
v-if="props.row.link"
target="_blank"
rel="noopener noreferrer"
:href="props.row.link"
v-text="$t('more')"
></a
></q-td>
</q-tr>
</template>
</q-table>
</q-card-section>
</q-card>

View file

@ -10,7 +10,9 @@ from fastapi import APIRouter, Depends
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from lnbits.core.models import User from lnbits.core.models import User
from lnbits.core.models.notifications import NotificationType
from lnbits.core.services import ( from lnbits.core.services import (
enqueue_notification,
get_balance_delta, get_balance_delta,
update_cached_settings, update_cached_settings,
) )
@ -60,6 +62,7 @@ async def api_get_settings(
status_code=HTTPStatus.OK, status_code=HTTPStatus.OK,
) )
async def api_update_settings(data: UpdateSettings, user: User = Depends(check_admin)): async def api_update_settings(data: UpdateSettings, user: User = Depends(check_admin)):
enqueue_notification(NotificationType.settings_update, {"username": user.username})
await update_admin_settings(data) await update_admin_settings(data)
admin_settings = await get_admin_settings(user.super_user) admin_settings = await get_admin_settings(user.super_user)
assert admin_settings, "Updated admin settings not found." assert admin_settings, "Updated admin settings not found."
@ -78,12 +81,9 @@ async def api_reset_settings(field_name: str):
return {"default_value": getattr(default_settings, field_name)} return {"default_value": getattr(default_settings, field_name)}
@admin_router.delete( @admin_router.delete("/api/v1/settings", status_code=HTTPStatus.OK)
"/api/v1/settings", async def api_delete_settings(user: User = Depends(check_super_user)) -> None:
status_code=HTTPStatus.OK, enqueue_notification(NotificationType.settings_update, {"username": user.username})
dependencies=[Depends(check_super_user)],
)
async def api_delete_settings() -> None:
await delete_admin_settings() await delete_admin_settings()
server_restart.set() server_restart.set()

View file

@ -49,7 +49,7 @@ api_router = APIRouter(tags=["Core"])
async def health() -> dict: async def health() -> dict:
return { return {
"server_time": int(time()), "server_time": int(time()),
"up_time": int(time() - settings.server_startup_time), "up_time": settings.lnbits_server_up_time,
} }
@ -57,7 +57,8 @@ async def health() -> dict:
async def health_check(user: User = Depends(check_user_exists)) -> dict: async def health_check(user: User = Depends(check_user_exists)) -> dict:
stat: dict[str, Any] = { stat: dict[str, Any] = {
"server_time": int(time()), "server_time": int(time()),
"up_time": int(time() - settings.server_startup_time), "up_time": settings.lnbits_server_up_time,
"up_time_seconds": int(time() - settings.server_startup_time),
} }
stat["version"] = settings.version stat["version"] = settings.version

View file

@ -31,9 +31,11 @@ from lnbits.core.models import (
UserExtra, UserExtra,
Wallet, Wallet,
) )
from lnbits.core.models.notifications import NotificationType
from lnbits.core.models.users import Account from lnbits.core.models.users import Account
from lnbits.core.services import ( from lnbits.core.services import (
create_user_account_no_ckeck, create_user_account_no_ckeck,
enqueue_notification,
update_user_account, update_user_account,
update_user_extensions, update_user_extensions,
update_wallet_balance, update_wallet_balance,
@ -277,4 +279,14 @@ async def api_update_balance(data: UpdateBalance) -> SimpleStatus:
if not wallet: if not wallet:
raise HTTPException(HTTPStatus.NOT_FOUND, "Wallet not found.") raise HTTPException(HTTPStatus.NOT_FOUND, "Wallet not found.")
await update_wallet_balance(wallet=wallet, amount=int(data.amount)) await update_wallet_balance(wallet=wallet, amount=int(data.amount))
enqueue_notification(
NotificationType.balance_update,
{
"amount": int(data.amount),
"wallet_id": wallet.id,
"wallet_name": wallet.name,
"balance": wallet.balance,
},
)
return SimpleStatus(success=True, message="Balance updated.") return SimpleStatus(success=True, message="Balance updated.")

View file

@ -9,7 +9,7 @@ from datetime import datetime, timezone
from enum import Enum from enum import Enum
from hashlib import sha256 from hashlib import sha256
from os import path from os import path
from time import time from time import gmtime, strftime, time
from typing import Any, Optional from typing import Any, Optional
import httpx import httpx
@ -364,20 +364,13 @@ class SecuritySettings(LNbitsSettings):
lnbits_rate_limit_unit: str = Field(default="minute") lnbits_rate_limit_unit: str = Field(default="minute")
lnbits_allowed_ips: list[str] = Field(default=[]) lnbits_allowed_ips: list[str] = Field(default=[])
lnbits_blocked_ips: list[str] = Field(default=[]) lnbits_blocked_ips: list[str] = Field(default=[])
lnbits_notifications: bool = Field(default=False)
lnbits_killswitch: bool = Field(default=False)
lnbits_killswitch_interval: int = Field(default=60)
lnbits_wallet_limit_max_balance: int = Field(default=0) lnbits_wallet_limit_max_balance: int = Field(default=0)
lnbits_wallet_limit_daily_max_withdraw: int = Field(default=0) lnbits_wallet_limit_daily_max_withdraw: int = Field(default=0)
lnbits_wallet_limit_secs_between_trans: int = Field(default=0) lnbits_wallet_limit_secs_between_trans: int = Field(default=0)
lnbits_watchdog: bool = Field(default=False) lnbits_watchdog_switch_to_voidwallet: bool = Field(default=False)
lnbits_watchdog_interval: int = Field(default=60) lnbits_watchdog_interval_minutes: int = Field(default=60)
lnbits_watchdog_delta: int = Field(default=1_000_000) lnbits_watchdog_delta: int = Field(default=1_000_000)
lnbits_status_manifest: str = Field(
default=(
"https://raw.githubusercontent.com/lnbits/lnbits-status/main/manifest.json"
)
)
def is_wallet_max_balance_exceeded(self, amount): def is_wallet_max_balance_exceeded(self, amount):
return ( return (
@ -387,6 +380,24 @@ class SecuritySettings(LNbitsSettings):
) )
class NotificationsSettings(LNbitsSettings):
lnbits_nostr_notifications_enabled: bool = Field(default=False)
lnbits_nostr_notifications_private_key: str = Field(default="")
lnbits_nostr_notifications_identifiers: list[str] = Field(default=[])
lnbits_telegram_notifications_enabled: bool = Field(default=False)
lnbits_telegram_notifications_access_token: str = Field(default="")
lnbits_telegram_notifications_chat_id: str = Field(default="")
lnbits_notification_settings_update: bool = Field(default=True)
lnbits_notification_credit_debit: bool = Field(default=True)
notification_balance_delta_changed: bool = Field(default=True)
lnbits_notification_server_start_stop: bool = Field(default=True)
lnbits_notification_watchdog: bool = Field(default=False)
lnbits_notification_server_status_hours: int = Field(default=24)
lnbits_notification_incoming_payment_amount_sats: int = Field(default=1_000_000)
lnbits_notification_outgoing_payment_amount_sats: int = Field(default=1_000_000)
class FakeWalletFundingSource(LNbitsSettings): class FakeWalletFundingSource(LNbitsSettings):
fake_wallet_secret: str = Field(default="ToTheMoon1") fake_wallet_secret: str = Field(default="ToTheMoon1")
@ -705,6 +716,7 @@ class EditableSettings(
FeeSettings, FeeSettings,
ExchangeProvidersSettings, ExchangeProvidersSettings,
SecuritySettings, SecuritySettings,
NotificationsSettings,
FundingSourcesSettings, FundingSourcesSettings,
LightningSettings, LightningSettings,
WebPushSettings, WebPushSettings,
@ -771,6 +783,11 @@ class EnvSettings(LNbitsSettings):
def has_default_extension_path(self) -> bool: def has_default_extension_path(self) -> bool:
return self.lnbits_extensions_path == "lnbits" return self.lnbits_extensions_path == "lnbits"
@property
def lnbits_server_up_time(self) -> str:
up_time = int(time() - self.server_startup_time)
return strftime("%H:%M:%S", gmtime(up_time))
class SaaSSettings(LNbitsSettings): class SaaSSettings(LNbitsSettings):
lnbits_saas_callback: Optional[str] = Field(default=None) lnbits_saas_callback: Optional[str] = Field(default=None)
@ -822,6 +839,9 @@ class TransientSettings(InstalledExtensionsSettings, ExchangeHistorySettings):
# Long running while loops should use this flag instead of `while True:` # Long running while loops should use this flag instead of `while True:`
lnbits_running: bool = Field(default=True) lnbits_running: bool = Field(default=True)
# Remember the latest balance delta in order to compare with the current one
latest_balance_delta_sats: int = Field(default=None)
@classmethod @classmethod
def readonly_fields(cls): def readonly_fields(cls):
return [f for f in inspect.signature(cls).parameters if not f.startswith("_")] return [f for f in inspect.signature(cls).parameters if not f.startswith("_")]

File diff suppressed because one or more lines are too long

View file

@ -175,12 +175,6 @@ window.localisation.br = {
enable_notifications: 'Ativar notificações', enable_notifications: 'Ativar notificações',
enable_notifications_desc: enable_notifications_desc:
'Se ativado, ele buscará as últimas atualizações de status do LNbits, como incidentes de segurança e atualizações.', 'Se ativado, ele buscará as últimas atualizações de status do LNbits, como incidentes de segurança e atualizações.',
enable_killswitch: 'Ativar Killswitch',
enable_killswitch_desc:
'Se ativado, mudará sua fonte de fundos para VoidWallet automaticamente se o LNbits enviar um sinal de desativação. Você precisará ativar manualmente após uma atualização.',
killswitch_interval: 'Intervalo do Killswitch',
killswitch_interval_desc:
'Com que frequência a tarefa de fundo deve verificar o sinal de desativação do LNbits proveniente da fonte de status (em minutos).',
enable_watchdog: 'Ativar Watchdog', enable_watchdog: 'Ativar Watchdog',
enable_watchdog_desc: enable_watchdog_desc:
'Se ativado, ele mudará automaticamente sua fonte de financiamento para VoidWallet se o seu saldo for inferior ao saldo do LNbits. Você precisará ativar manualmente após uma atualização.', 'Se ativado, ele mudará automaticamente sua fonte de financiamento para VoidWallet se o seu saldo for inferior ao saldo do LNbits. Você precisará ativar manualmente após uma atualização.',
@ -197,7 +191,6 @@ window.localisation.br = {
more: 'mais', more: 'mais',
less: 'menos', less: 'menos',
releases: 'Lançamentos', releases: 'Lançamentos',
killswitch: 'Dispositivo de desativação',
watchdog: 'Cão de guarda', watchdog: 'Cão de guarda',
server_logs: 'Registros do Servidor', server_logs: 'Registros do Servidor',
ip_blocker: 'Bloqueador de IP', ip_blocker: 'Bloqueador de IP',

View file

@ -166,12 +166,7 @@ window.localisation.cn = {
enable_notifications: '启用通知', enable_notifications: '启用通知',
enable_notifications_desc: enable_notifications_desc:
'如果启用它将获取最新的LNbits状态更新如安全事件和更新。', '如果启用它将获取最新的LNbits状态更新如安全事件和更新。',
enable_killswitch: '启用紧急停止开关',
enable_killswitch_desc:
'如果启用当LNbits发送终止信号时系统将自动将您的资金来源更改为VoidWallet。更新后您将需要手动启用。',
killswitch_interval: 'Killswitch 间隔',
killswitch_interval_desc:
'后台任务应该多久检查一次来自状态源的LNbits断路信号以分钟为单位。',
enable_watchdog: '启用看门狗', enable_watchdog: '启用看门狗',
enable_watchdog_desc: enable_watchdog_desc:
'如果启用当您的余额低于LNbits余额时系统将自动将您的资金来源更改为VoidWallet。更新后您将需要手动启用。', '如果启用当您的余额低于LNbits余额时系统将自动将您的资金来源更改为VoidWallet。更新后您将需要手动启用。',
@ -187,7 +182,6 @@ window.localisation.cn = {
more: '更多', more: '更多',
less: '少', less: '少',
releases: '版本', releases: '版本',
killswitch: '杀手锏',
watchdog: '监控程序', watchdog: '监控程序',
server_logs: '服务器日志', server_logs: '服务器日志',
ip_blocker: 'IP 阻止器', ip_blocker: 'IP 阻止器',

View file

@ -174,15 +174,6 @@ window.localisation.cs = {
enable_notifications: 'Povolit notifikace', enable_notifications: 'Povolit notifikace',
enable_notifications_desc: enable_notifications_desc:
'Pokud je povoleno, bude stahovat nejnovější aktualizace stavu LNbits, jako jsou bezpečnostní incidenty a aktualizace.', 'Pokud je povoleno, bude stahovat nejnovější aktualizace stavu LNbits, jako jsou bezpečnostní incidenty a aktualizace.',
enable_killswitch: 'Povolit Killswitch',
enable_killswitch_desc:
'Pokud je povoleno, automaticky změní zdroj financování na VoidWallet pokud LNbits odešle signál killswitch. Po aktualizaci budete muset povolit ručně.',
killswitch_interval: 'Interval Killswitch',
killswitch_interval_desc:
'Jak často by měl úkol na pozadí kontrolovat signál killswitch od LNbits ze zdroje stavu (v minutách).',
enable_watchdog: 'Povolit Watchdog',
enable_watchdog_desc:
'Pokud je povoleno, automaticky změní zdroj financování na VoidWallet pokud je váš zůstatek nižší než zůstatek LNbits. Po aktualizaci budete muset povolit ručně.',
watchdog_interval: 'Interval Watchdog', watchdog_interval: 'Interval Watchdog',
watchdog_interval_desc: watchdog_interval_desc:
'Jak často by měl úkol na pozadí kontrolovat signál killswitch v watchdog delta [node_balance - lnbits_balance] (v minutách).', 'Jak často by měl úkol na pozadí kontrolovat signál killswitch v watchdog delta [node_balance - lnbits_balance] (v minutách).',
@ -196,7 +187,6 @@ window.localisation.cs = {
more: 'více', more: 'více',
less: 'méně', less: 'méně',
releases: 'Vydání', releases: 'Vydání',
killswitch: 'Killswitch',
watchdog: 'Watchdog', watchdog: 'Watchdog',
server_logs: 'Logy serveru', server_logs: 'Logy serveru',
ip_blocker: 'Blokování IP', ip_blocker: 'Blokování IP',

View file

@ -177,12 +177,6 @@ window.localisation.de = {
enable_notifications: 'Aktiviere Benachrichtigungen', enable_notifications: 'Aktiviere Benachrichtigungen',
enable_notifications_desc: enable_notifications_desc:
'Wenn aktiviert, werden die neuesten LNbits-Statusaktualisierungen, wie Sicherheitsvorfälle und Updates, abgerufen.', 'Wenn aktiviert, werden die neuesten LNbits-Statusaktualisierungen, wie Sicherheitsvorfälle und Updates, abgerufen.',
enable_killswitch: 'Aktivieren Sie den Notausschalter',
enable_killswitch_desc:
'Falls aktiviert, wird Ihre Zahlungsquelle automatisch auf VoidWallet umgestellt, wenn LNbits ein Killswitch-Signal sendet. Nach einem Update müssen Sie dies manuell wieder aktivieren.',
killswitch_interval: 'Intervall für den Notausschalter',
killswitch_interval_desc:
'Wie oft die Hintergrundaufgabe nach dem LNbits-Killswitch-Signal aus der Statusquelle suchen soll (in Minuten).',
enable_watchdog: 'Aktiviere Watchdog', enable_watchdog: 'Aktiviere Watchdog',
enable_watchdog_desc: enable_watchdog_desc:
'Wenn aktiviert, wird Ihre Zahlungsquelle automatisch auf VoidWallet umgestellt, wenn Ihr Guthaben niedriger als das LNbits-Guthaben ist. Nach einem Update müssen Sie dies manuell aktivieren.', 'Wenn aktiviert, wird Ihre Zahlungsquelle automatisch auf VoidWallet umgestellt, wenn Ihr Guthaben niedriger als das LNbits-Guthaben ist. Nach einem Update müssen Sie dies manuell aktivieren.',
@ -199,7 +193,7 @@ window.localisation.de = {
more: 'mehr', more: 'mehr',
less: 'weniger', less: 'weniger',
releases: 'Veröffentlichungen', releases: 'Veröffentlichungen',
killswitch: 'Killswitch',
watchdog: 'Wachhund', watchdog: 'Wachhund',
server_logs: 'Serverprotokolle', server_logs: 'Serverprotokolle',
ip_blocker: 'IP-Sperre', ip_blocker: 'IP-Sperre',

View file

@ -168,18 +168,58 @@ window.localisation.en = {
update_available: 'Update {version} available!', update_available: 'Update {version} available!',
latest_update: 'You are on the latest version {version}.', latest_update: 'You are on the latest version {version}.',
notifications: 'Notifications', notifications: 'Notifications',
no_notifications: 'No notifications', notifications_configure: 'Configure Notifications',
notifications_disabled: 'LNbits status notifications are disabled.', notifications_nostr_config: 'Nostr Configuration',
enable_notifications: 'Enable Notifications', notifications_enable_nostr: 'Enable Nostr',
enable_notifications_desc: notifications_enable_nostr_desc: 'Send notfications over Nostr',
'If enabled it will fetch the latest LNbits Status updates, like security incidents and updates.', notifications_nostr_private_key: 'Nostr Private Key',
enable_killswitch: 'Enable Killswitch', notifications_nostr_private_key_desc:
enable_killswitch_desc: 'Private key (hex or nsec) to sign the messages sent to Nostr',
'If enabled it will change your funding source to VoidWallet automatically if LNbits sends out a killswitch signal. You will need to enable manually after an update.', notifications_nostr_identifiers: 'Nostr Identifiers',
killswitch_interval: 'Killswitch Interval', notifications_nostr_identifiers_desc:
killswitch_interval_desc: 'List of identifiers to send notifications to',
'How often the background task should check for the LNbits killswitch signal from the status source (in minutes).',
enable_watchdog: 'Enable Watchdog', notifications_telegram_config: 'Telegram Configuration',
notifications_enable_telegram: 'Enable Telegram',
notifications_enable_telegram_desc: 'Send notfications over Telegram',
notifications_telegram_access_token: 'Access Token',
notifications_telegram_access_token_desc: 'Access token for the bot',
notifications_chat_id: 'Chat ID',
notifications_chat_id_desc: 'Chat ID to send the notifications to',
notification_settings_update: 'Settings updated',
notification_settings_update_desc:
'Notify when server settings have been updated',
notification_server_start_stop: 'Server Start/Stop',
notification_server_start_stop_desc:
'Notify when the server has been started/stopped',
notification_watchdog_limit: 'Watchdog Limit Notification',
notification_watchdog_limit_desc:
'Notify when the watchdog limit has been reached (does not affect the funding source)',
notification_server_status: 'Server Status',
notification_server_status_desc:
'Send regular notifications about the server status (interval value in hours)',
notification_incoming_payment: 'Incoming Payments',
notification_incoming_payment_desc:
'Notify when a wallet has received a payment above the specified amount (sats)',
notification_outgoing_payment: 'Outgoing Payments',
notification_outgoing_payment_desc:
'Notify when a wallet has sent a payment above the specified amount (sats)',
notification_credit_debit: 'Credit / Debit',
notification_credit_debit_desc:
'Notify when a wallet has been credited/debited by the superuser',
notification_balance_delta_changed: 'Balance Delta Changed',
notification_balance_delta_changed_desc:
'Notify when the diference between the node balance and the LNbits balance has changed even by 1 sat. This runs every minute.',
enable_watchdog: 'Enable Watchdog Switch',
enable_watchdog_desc: enable_watchdog_desc:
'If enabled it will change your funding source to VoidWallet automatically if your balance is lower than the LNbits balance. You will need to enable manually after an update.', 'If enabled it will change your funding source to VoidWallet automatically if your balance is lower than the LNbits balance. You will need to enable manually after an update.',
watchdog_interval: 'Watchdog Interval', watchdog_interval: 'Watchdog Interval',
@ -195,7 +235,6 @@ window.localisation.en = {
more: 'more', more: 'more',
less: 'less', less: 'less',
releases: 'Releases', releases: 'Releases',
killswitch: 'Killswitch',
watchdog: 'Watchdog', watchdog: 'Watchdog',
server_logs: 'Server Logs', server_logs: 'Server Logs',
ip_blocker: 'IP Blocker', ip_blocker: 'IP Blocker',

View file

@ -177,13 +177,7 @@ window.localisation.es = {
enable_notifications: 'Activar notificaciones', enable_notifications: 'Activar notificaciones',
enable_notifications_desc: enable_notifications_desc:
'Si está activado, buscará las últimas actualizaciones del estado de LNbits, como incidentes de seguridad y actualizaciones.', 'Si está activado, buscará las últimas actualizaciones del estado de LNbits, como incidentes de seguridad y actualizaciones.',
enable_killswitch: 'Activar Killswitch',
enable_killswitch_desc:
'Si está activado, cambiará automáticamente su fuente de financiamiento a VoidWallet si LNbits envía una señal de parada de emergencia. Necesitará activarlo manualmente después de una actualización.',
killswitch_interval: 'Intervalo de Killswitch',
killswitch_interval_desc:
'Con qué frecuencia la tarea en segundo plano debe verificar la señal de interruptor de emergencia de LNbits desde la fuente de estado (en minutos).',
enable_watchdog: 'Activar Watchdog',
enable_watchdog_desc: enable_watchdog_desc:
'Si está activado, cambiará automáticamente su fuente de financiamiento a VoidWallet si su saldo es inferior al saldo de LNbits. Tendrá que activarlo manualmente después de una actualización.', 'Si está activado, cambiará automáticamente su fuente de financiamiento a VoidWallet si su saldo es inferior al saldo de LNbits. Tendrá que activarlo manualmente después de una actualización.',
watchdog_interval: 'Intervalo de vigilancia', watchdog_interval: 'Intervalo de vigilancia',
@ -199,7 +193,6 @@ window.localisation.es = {
more: 'más', more: 'más',
less: 'menos', less: 'menos',
releases: 'Lanzamientos', releases: 'Lanzamientos',
killswitch: 'Interruptor de apagado',
watchdog: 'Perro guardián', watchdog: 'Perro guardián',
server_logs: 'Registros del Servidor', server_logs: 'Registros del Servidor',
ip_blocker: 'Bloqueador de IP', ip_blocker: 'Bloqueador de IP',

View file

@ -176,12 +176,7 @@ window.localisation.fi = {
enable_notifications: 'Ota tiedotteet käyttöön', enable_notifications: 'Ota tiedotteet käyttöön',
enable_notifications_desc: enable_notifications_desc:
'Tämän ollessa valittuna, noudetaan LNbits-tilatiedotteet. Niitä ovat esimerkiksi turvallisuuteen liittyvät tapahtumatiedotteet ja tiedot tämän ohjelmiston päivityksistä.', 'Tämän ollessa valittuna, noudetaan LNbits-tilatiedotteet. Niitä ovat esimerkiksi turvallisuuteen liittyvät tapahtumatiedotteet ja tiedot tämän ohjelmiston päivityksistä.',
enable_killswitch: 'Ota Killswitch käyttöön',
enable_killswitch_desc:
'Jos LNbits antaa killswitch-komennon, niin rahoituslähteeksi valitaan automaattisesti heti VoidWallet. Päivityksen jälkeen tämä asetus pitää tarkastaa uudelleen.',
killswitch_interval: 'Killswitch-aikaväli',
killswitch_interval_desc:
'Tällä määritetään kuinka usein taustatoiminto tarkistaa killswitch-signaalin tilatiedotteiden lähteestä. Hakujen väli ilmoitetaan minuutteina.',
enable_watchdog: 'Ota Watchdog käyttöön', enable_watchdog: 'Ota Watchdog käyttöön',
enable_watchdog_desc: enable_watchdog_desc:
'Tämän ollessa käytössä, ja solmun varojen laskiessa alle LNbits-varojen määrän, otetaan automaattisesti käyttöön VoidWallet. Päivityksen jälkeen tämä asetus pitää tarkastaa uudelleen.', 'Tämän ollessa käytössä, ja solmun varojen laskiessa alle LNbits-varojen määrän, otetaan automaattisesti käyttöön VoidWallet. Päivityksen jälkeen tämä asetus pitää tarkastaa uudelleen.',
@ -198,7 +193,7 @@ window.localisation.fi = {
more: 'enemmän', more: 'enemmän',
less: 'vähemmän', less: 'vähemmän',
releases: 'Julkaisut', releases: 'Julkaisut',
killswitch: 'Killswitch',
watchdog: 'Watchdog', watchdog: 'Watchdog',
server_logs: 'Palvelimen lokit', server_logs: 'Palvelimen lokit',
ip_blocker: 'IP-suodatin', ip_blocker: 'IP-suodatin',

View file

@ -180,12 +180,7 @@ window.localisation.fr = {
enable_notifications: 'Activer les notifications', enable_notifications: 'Activer les notifications',
enable_notifications_desc: enable_notifications_desc:
'Si activé, il récupérera les dernières mises à jour du statut LNbits, telles que les incidents de sécurité et les mises à jour.', 'Si activé, il récupérera les dernières mises à jour du statut LNbits, telles que les incidents de sécurité et les mises à jour.',
enable_killswitch: 'Activer le Killswitch',
enable_killswitch_desc:
'Si activé, il changera automatiquement votre source de financement en VoidWallet si LNbits envoie un signal de coupure. Vous devrez activer manuellement après une mise à jour.',
killswitch_interval: 'Intervalle du Killswitch',
killswitch_interval_desc:
"À quelle fréquence la tâche de fond doit-elle vérifier le signal d'arrêt d'urgence LNbits provenant de la source de statut (en minutes).",
enable_watchdog: 'Activer le Watchdog', enable_watchdog: 'Activer le Watchdog',
enable_watchdog_desc: enable_watchdog_desc:
'Si elle est activée, elle changera automatiquement votre source de financement en VoidWallet si votre solde est inférieur au solde LNbits. Vous devrez activer manuellement après une mise à jour.', 'Si elle est activée, elle changera automatiquement votre source de financement en VoidWallet si votre solde est inférieur au solde LNbits. Vous devrez activer manuellement après une mise à jour.',
@ -202,7 +197,6 @@ window.localisation.fr = {
more: 'plus', more: 'plus',
less: 'moins', less: 'moins',
releases: 'Versions', releases: 'Versions',
killswitch: "Interrupteur d'arrêt",
watchdog: 'Chien de garde', watchdog: 'Chien de garde',
server_logs: 'Journaux du serveur', server_logs: 'Journaux du serveur',
ip_blocker: "Bloqueur d'IP", ip_blocker: "Bloqueur d'IP",

View file

@ -176,12 +176,6 @@ window.localisation.it = {
enable_notifications: 'Attiva le notifiche', enable_notifications: 'Attiva le notifiche',
enable_notifications_desc: enable_notifications_desc:
'Se attivato, recupererà gli ultimi aggiornamenti sullo stato di LNbits, come incidenti di sicurezza e aggiornamenti.', 'Se attivato, recupererà gli ultimi aggiornamenti sullo stato di LNbits, come incidenti di sicurezza e aggiornamenti.',
enable_killswitch: 'Attiva Killswitch',
enable_killswitch_desc:
'Se attivato, cambierà automaticamente la tua fonte di finanziamento in VoidWallet se LNbits invia un segnale di killswitch. Dovrai attivare manualmente dopo un aggiornamento.',
killswitch_interval: 'Intervallo Killswitch',
killswitch_interval_desc:
'Quanto spesso il compito in background dovrebbe controllare il segnale di killswitch LNbits dalla fonte di stato (in minuti).',
enable_watchdog: 'Attiva Watchdog', enable_watchdog: 'Attiva Watchdog',
enable_watchdog_desc: enable_watchdog_desc:
'Se abilitato, cambierà automaticamente la tua fonte di finanziamento in VoidWallet se il tuo saldo è inferiore al saldo LNbits. Dovrai abilitarlo manualmente dopo un aggiornamento.', 'Se abilitato, cambierà automaticamente la tua fonte di finanziamento in VoidWallet se il tuo saldo è inferiore al saldo LNbits. Dovrai abilitarlo manualmente dopo un aggiornamento.',
@ -198,7 +192,6 @@ window.localisation.it = {
more: 'più', more: 'più',
less: 'meno', less: 'meno',
releases: 'Pubblicazioni', releases: 'Pubblicazioni',
killswitch: 'Interruttore di spegnimento',
watchdog: 'Cane da guardia', watchdog: 'Cane da guardia',
server_logs: 'Registri del server', server_logs: 'Registri del server',
ip_blocker: 'Blocco IP', ip_blocker: 'Blocco IP',

View file

@ -171,12 +171,6 @@ window.localisation.jp = {
enable_notifications: '通知を有効にする', enable_notifications: '通知を有効にする',
enable_notifications_desc: enable_notifications_desc:
'有効にすると、セキュリティインシデントやアップデートのような最新のLNbitsステータス更新を取得します。', '有効にすると、セキュリティインシデントやアップデートのような最新のLNbitsステータス更新を取得します。',
enable_killswitch: 'キルスイッチを有効にする',
enable_killswitch_desc:
'有効にすると、LNbitsからキルスイッチ信号が送信された場合に自動的に資金源をVoidWalletに切り替えます。更新後には手動で有効にする必要があります。',
killswitch_interval: 'キルスイッチ間隔',
killswitch_interval_desc:
'バックグラウンドタスクがステータスソースからLNbitsキルスイッチ信号を確認する頻度分単位。',
enable_watchdog: 'ウォッチドッグを有効にする', enable_watchdog: 'ウォッチドッグを有効にする',
enable_watchdog_desc: enable_watchdog_desc:
'有効にすると、残高がLNbitsの残高より少ない場合に、資金源を自動的にVoidWalletに変更します。アップデート後は手動で有効にする必要があります。', '有効にすると、残高がLNbitsの残高より少ない場合に、資金源を自動的にVoidWalletに変更します。アップデート後は手動で有効にする必要があります。',
@ -193,7 +187,6 @@ window.localisation.jp = {
more: 'より多くの', more: 'より多くの',
less: '少ない', less: '少ない',
releases: 'リリース', releases: 'リリース',
killswitch: 'キルスイッチ',
watchdog: 'ウォッチドッグ', watchdog: 'ウォッチドッグ',
server_logs: 'サーバーログ', server_logs: 'サーバーログ',
ip_blocker: 'IPブロッカー', ip_blocker: 'IPブロッカー',

View file

@ -173,12 +173,6 @@ window.localisation.kr = {
enable_notifications: '알림 활성화', enable_notifications: '알림 활성화',
enable_notifications_desc: enable_notifications_desc:
'활성화 시, 가장 최신의 보안 사고나 소프트웨어 업데이트 등의 LNbits 상황 업데이트를 불러옵니다.', '활성화 시, 가장 최신의 보안 사고나 소프트웨어 업데이트 등의 LNbits 상황 업데이트를 불러옵니다.',
enable_killswitch: '비상 정지 활성화',
enable_killswitch_desc:
'활성화 시, LNbits 메인 서버에서 비상 정지 신호를 보내면 자동으로 자금의 원천을 VoidWallet으로 변경합니다. 업데이트 이후 수동으로 활성화해 주어야 합니다.',
killswitch_interval: '비상 정지 시간 간격',
killswitch_interval_desc:
'LNbits 메인 서버에서 나오는 비상 정지 신호를 백그라운드 작업으로 얼마나 자주 확인할 것인지를 결정합니다. (분 단위)',
enable_watchdog: '와치독 활성화', enable_watchdog: '와치독 활성화',
enable_watchdog_desc: enable_watchdog_desc:
'활성화 시, LNbits 잔금보다 당신의 잔금이 지정한 수준보다 더 낮아질 경우 자동으로 자금의 원천을 VoidWallet으로 변경합니다. 업데이트 이후 수동으로 활성화해 주어야 합니다.', '활성화 시, LNbits 잔금보다 당신의 잔금이 지정한 수준보다 더 낮아질 경우 자동으로 자금의 원천을 VoidWallet으로 변경합니다. 업데이트 이후 수동으로 활성화해 주어야 합니다.',
@ -195,7 +189,6 @@ window.localisation.kr = {
more: '더 알아보기', more: '더 알아보기',
less: '적게', less: '적게',
releases: '배포 버전들', releases: '배포 버전들',
killswitch: '비상 정지',
watchdog: '와치독', watchdog: '와치독',
server_logs: '서버 로그', server_logs: '서버 로그',
ip_blocker: 'IP 기반 차단기', ip_blocker: 'IP 기반 차단기',

View file

@ -177,12 +177,6 @@ window.localisation.nl = {
enable_notifications: 'Schakel meldingen in', enable_notifications: 'Schakel meldingen in',
enable_notifications_desc: enable_notifications_desc:
'Indien ingeschakeld zal het de laatste LNbits Status updates ophalen, zoals veiligheidsincidenten en updates.', 'Indien ingeschakeld zal het de laatste LNbits Status updates ophalen, zoals veiligheidsincidenten en updates.',
enable_killswitch: 'Activeer Killswitch',
enable_killswitch_desc:
'Indien ingeschakeld, zal het uw financieringsbron automatisch wijzigen naar VoidWallet als LNbits een killswitch-signaal verzendt. U zult het na een update handmatig moeten inschakelen.',
killswitch_interval: 'Uitschakelschakelaar-interval',
killswitch_interval_desc:
'Hoe vaak de achtergrondtaak moet controleren op het LNbits killswitch signaal van de statusbron (in minuten).',
enable_watchdog: 'Inschakelen Watchdog', enable_watchdog: 'Inschakelen Watchdog',
enable_watchdog_desc: enable_watchdog_desc:
'Indien ingeschakeld, wordt uw betaalbron automatisch gewijzigd naar VoidWallet als uw saldo lager is dan het saldo van LNbits. U zult dit na een update handmatig moeten inschakelen.', 'Indien ingeschakeld, wordt uw betaalbron automatisch gewijzigd naar VoidWallet als uw saldo lager is dan het saldo van LNbits. U zult dit na een update handmatig moeten inschakelen.',
@ -199,7 +193,6 @@ window.localisation.nl = {
more: 'meer', more: 'meer',
less: 'minder', less: 'minder',
releases: 'Uitgaven', releases: 'Uitgaven',
killswitch: 'Killswitch',
watchdog: 'Waakhond', watchdog: 'Waakhond',
server_logs: 'Serverlogboeken', server_logs: 'Serverlogboeken',
ip_blocker: 'IP-blokkering', ip_blocker: 'IP-blokkering',

View file

@ -175,12 +175,6 @@ window.localisation.pi = {
enable_notifications: 'Enable Notifications', enable_notifications: 'Enable Notifications',
enable_notifications_desc: enable_notifications_desc:
"If ye be allowin' it, it'll be fetchin' the latest LNbits Status updates, like security incidents and updates.", "If ye be allowin' it, it'll be fetchin' the latest LNbits Status updates, like security incidents and updates.",
enable_killswitch: "Enabl' th' Killswitch",
enable_killswitch_desc:
"If enabled it'll be changin' yer fundin' source to VoidWallet automatically if LNbits sends out a killswitch signal, ye will. Ye'll be needin' t' enable manually after an update, arr.",
killswitch_interval: 'Killswitch Interval',
killswitch_interval_desc:
"How oft th' background task should be checkin' fer th' LNbits killswitch signal from th' status source (in minutes).",
enable_watchdog: 'Enable Seadog', enable_watchdog: 'Enable Seadog',
enable_watchdog_desc: enable_watchdog_desc:
"If enabled, it will swap yer treasure source t' VoidWallet on its own if yer balance be lower than th' LNbits balance. Ye'll need t' enable by hand after an update.", "If enabled, it will swap yer treasure source t' VoidWallet on its own if yer balance be lower than th' LNbits balance. Ye'll need t' enable by hand after an update.",
@ -197,7 +191,6 @@ window.localisation.pi = {
more: "Arr, 'tis more.", more: "Arr, 'tis more.",
less: "Arr, 'tis more fewer.", less: "Arr, 'tis more fewer.",
releases: 'Releases', releases: 'Releases',
killswitch: 'Killswitch',
watchdog: 'Seadog', watchdog: 'Seadog',
server_logs: 'Server Logs', server_logs: 'Server Logs',
ip_blocker: 'IP Blockar', ip_blocker: 'IP Blockar',

View file

@ -173,12 +173,6 @@ window.localisation.pl = {
enable_notifications: 'Włącz powiadomienia', enable_notifications: 'Włącz powiadomienia',
enable_notifications_desc: enable_notifications_desc:
'Jeśli ta opcja zostanie włączona, będzie pobierać najnowsze informacje o statusie LNbits, takie jak incydenty bezpieczeństwa i aktualizacje.', 'Jeśli ta opcja zostanie włączona, będzie pobierać najnowsze informacje o statusie LNbits, takie jak incydenty bezpieczeństwa i aktualizacje.',
enable_killswitch: 'Włącz Killswitch',
enable_killswitch_desc:
'Jeśli zostanie włączone, automatycznie zmieni źródło finansowania na VoidWallet, jeśli LNbits wyśle sygnał wyłączający. Po aktualizacji będziesz musiał włączyć to ręcznie.',
killswitch_interval: 'Interwał wyłącznika awaryjnego',
killswitch_interval_desc:
'Jak często zadanie w tle powinno sprawdzać sygnał wyłącznika awaryjnego LNbits ze źródła statusu (w minutach).',
enable_watchdog: 'Włącz Watchdog', enable_watchdog: 'Włącz Watchdog',
enable_watchdog_desc: enable_watchdog_desc:
'Jeśli zostanie włączone, automatycznie zmieni źródło finansowania na VoidWallet, jeśli saldo jest niższe niż saldo LNbits. Po aktualizacji trzeba będzie włączyć ręcznie.', 'Jeśli zostanie włączone, automatycznie zmieni źródło finansowania na VoidWallet, jeśli saldo jest niższe niż saldo LNbits. Po aktualizacji trzeba będzie włączyć ręcznie.',
@ -195,7 +189,6 @@ window.localisation.pl = {
more: 'więcej', more: 'więcej',
less: 'mniej', less: 'mniej',
releases: 'Wydania', releases: 'Wydania',
killswitch: 'Killswitch',
watchdog: 'Pies gończy', watchdog: 'Pies gończy',
server_logs: 'Dzienniki serwera', server_logs: 'Dzienniki serwera',
ip_blocker: 'Blokada IP', ip_blocker: 'Blokada IP',

View file

@ -176,12 +176,6 @@ window.localisation.pt = {
enable_notifications: 'Ativar Notificações', enable_notifications: 'Ativar Notificações',
enable_notifications_desc: enable_notifications_desc:
'Se ativado, ele buscará as últimas atualizações de status do LNbits, como incidentes de segurança e atualizações.', 'Se ativado, ele buscará as últimas atualizações de status do LNbits, como incidentes de segurança e atualizações.',
enable_killswitch: 'Ativar Killswitch',
enable_killswitch_desc:
'Se ativado, ele mudará sua fonte de financiamento para VoidWallet automaticamente se o LNbits enviar um sinal de desativação. Você precisará ativar manualmente após uma atualização.',
killswitch_interval: 'Intervalo do Killswitch',
killswitch_interval_desc:
'Com que frequência a tarefa de fundo deve verificar o sinal de desativação do LNbits proveniente da fonte de status (em minutos).',
enable_watchdog: 'Ativar Watchdog', enable_watchdog: 'Ativar Watchdog',
enable_watchdog_desc: enable_watchdog_desc:
'Se ativado, mudará automaticamente a sua fonte de financiamento para VoidWallet caso o seu saldo seja inferior ao saldo LNbits. Você precisará ativar manualmente após uma atualização.', 'Se ativado, mudará automaticamente a sua fonte de financiamento para VoidWallet caso o seu saldo seja inferior ao saldo LNbits. Você precisará ativar manualmente após uma atualização.',
@ -198,7 +192,6 @@ window.localisation.pt = {
more: 'mais', more: 'mais',
less: 'menos', less: 'menos',
releases: 'Lançamentos', releases: 'Lançamentos',
killswitch: 'Interruptor de desativação',
watchdog: 'Cão de guarda', watchdog: 'Cão de guarda',
server_logs: 'Registros do Servidor', server_logs: 'Registros do Servidor',
ip_blocker: 'Bloqueador de IP', ip_blocker: 'Bloqueador de IP',

View file

@ -172,12 +172,6 @@ window.localisation.sk = {
enable_notifications: 'Povoliť Notifikácie', enable_notifications: 'Povoliť Notifikácie',
enable_notifications_desc: enable_notifications_desc:
'Ak povolené, budú sa načítavať najnovšie aktualizácie stavu LNbits, ako sú bezpečnostné incidenty a aktualizácie.', 'Ak povolené, budú sa načítavať najnovšie aktualizácie stavu LNbits, ako sú bezpečnostné incidenty a aktualizácie.',
enable_killswitch: 'Povoliť Killswitch',
enable_killswitch_desc:
'Ak povolené, vaš zdroj financovania sa automaticky zmení na VoidWallet, ak LNbits vysielajú signál killswitch. Po aktualizácii bude treba povoliť manuálne.',
killswitch_interval: 'Interval Killswitch',
killswitch_interval_desc:
'Ako často by malo pozadie kontrolovať signál killswitch od LNbits zo zdroja stavu (v minútach).',
enable_watchdog: 'Povoliť Watchdog', enable_watchdog: 'Povoliť Watchdog',
enable_watchdog_desc: enable_watchdog_desc:
'Ak povolené, vaš zdroj financovania sa automaticky zmení na VoidWallet, ak je váš zostatok nižší ako zostatok LNbits. Po aktualizácii bude treba povoliť manuálne.', 'Ak povolené, vaš zdroj financovania sa automaticky zmení na VoidWallet, ak je váš zostatok nižší ako zostatok LNbits. Po aktualizácii bude treba povoliť manuálne.',
@ -194,7 +188,6 @@ window.localisation.sk = {
more: 'viac', more: 'viac',
less: 'menej', less: 'menej',
releases: 'Vydania', releases: 'Vydania',
killswitch: 'Killswitch',
watchdog: 'Watchdog', watchdog: 'Watchdog',
server_logs: 'Logy servera', server_logs: 'Logy servera',
ip_blocker: 'Blokovanie IP', ip_blocker: 'Blokovanie IP',

View file

@ -173,12 +173,6 @@ window.localisation.we = {
enable_notifications: 'Galluogi Hysbysiadau', enable_notifications: 'Galluogi Hysbysiadau',
enable_notifications_desc: enable_notifications_desc:
"Os bydd wedi'i alluogi bydd yn nôl y diweddariadau Statws LNbits diweddaraf, fel digwyddiadau diogelwch a diweddariadau.", "Os bydd wedi'i alluogi bydd yn nôl y diweddariadau Statws LNbits diweddaraf, fel digwyddiadau diogelwch a diweddariadau.",
enable_killswitch: 'Galluogi Killswitch',
enable_killswitch_desc:
'Os bydd yn galluogi, bydd yn newid eich ffynhonnell arian i VoidWallet yn awtomatig os bydd LNbits yn anfon arwydd killswitch. Bydd angen i chi alluogi â llaw ar ôl diweddariad.',
killswitch_interval: 'Amlder Cyllell Dorri',
killswitch_interval_desc:
"Pa mor aml y dylai'r dasg gefndir wirio am signal killswitch LNbits o'r ffynhonnell statws (mewn munudau).",
enable_watchdog: 'Galluogi Watchdog', enable_watchdog: 'Galluogi Watchdog',
enable_watchdog_desc: enable_watchdog_desc:
'Os bydd yn cael ei alluogi bydd yn newid eich ffynhonnell ariannu i VoidWallet yn awtomatig os bydd eich balans yn is na balans LNbits. Bydd angen i chi alluogi â llaw ar ôl diweddariad.', 'Os bydd yn cael ei alluogi bydd yn newid eich ffynhonnell ariannu i VoidWallet yn awtomatig os bydd eich balans yn is na balans LNbits. Bydd angen i chi alluogi â llaw ar ôl diweddariad.',
@ -195,7 +189,6 @@ window.localisation.we = {
more: 'mwy', more: 'mwy',
less: 'llai', less: 'llai',
releases: 'Rhyddhau', releases: 'Rhyddhau',
killswitch: 'Killswitch',
watchdog: 'Gwyliwr', watchdog: 'Gwyliwr',
server_logs: 'Logiau Gweinydd', server_logs: 'Logiau Gweinydd',
ip_blocker: 'Rheolydd IP', ip_blocker: 'Rheolydd IP',

View file

@ -41,6 +41,7 @@ window.AdminPageLogic = {
formAddAdmin: '', formAddAdmin: '',
formAddUser: '', formAddUser: '',
formAddExtensionsManifest: '', formAddExtensionsManifest: '',
nostrNotificationIdentifier: '',
formAllowedIPs: '', formAllowedIPs: '',
formBlockedIPs: '', formBlockedIPs: '',
nostrAcceptedUrl: '', nostrAcceptedUrl: '',
@ -238,6 +239,23 @@ window.AdminPageLogic = {
m => m !== manifest m => m !== manifest
) )
}, },
addNostrNotificationIdentifier() {
const identifer = this.nostrNotificationIdentifier.trim()
const identifiers = this.formData.lnbits_nostr_notifications_identifiers
if (identifer && identifer.length && !identifiers.includes(identifer)) {
this.formData.lnbits_nostr_notifications_identifiers = [
...identifiers,
identifer
]
this.nostrNotificationIdentifier = ''
}
},
removeNostrNotificationIdentifier(identifer) {
const identifiers = this.formData.lnbits_nostr_notifications_identifiers
this.formData.lnbits_nostr_notifications_identifiers = identifiers.filter(
m => m !== identifer
)
},
async toggleServerLog() { async toggleServerLog() {
this.serverlogEnabled = !this.serverlogEnabled this.serverlogEnabled = !this.serverlogEnabled
if (this.serverlogEnabled) { if (this.serverlogEnabled) {
@ -377,23 +395,9 @@ window.AdminPageLogic = {
formatDate(date) { formatDate(date) {
return moment(date * 1000).fromNow() return moment(date * 1000).fromNow()
}, },
getNotifications() {
if (this.settings.lnbits_notifications) { getAudit() {
axios LNbits.api
.get(this.settings.lnbits_status_manifest)
.then(response => {
this.statusData = response.data
})
.catch(error => {
this.formData.lnbits_notifications = false
error.response.data = {}
error.response.data.message = 'Could not fetch status manifest.'
LNbits.utils.notifyApiError(error)
})
}
},
async getAudit() {
await LNbits.api
.request('GET', '/admin/api/v1/audit', this.g.user.wallets[0].adminkey) .request('GET', '/admin/api/v1/audit', this.g.user.wallets[0].adminkey)
.then(response => { .then(response => {
this.auditData = response.data this.auditData = response.data
@ -421,7 +425,6 @@ window.AdminPageLogic = {
this.isSuperUser = response.data.is_super_user || false this.isSuperUser = response.data.is_super_user || false
this.settings = response.data this.settings = response.data
this.formData = {...this.settings} this.formData = {...this.settings}
this.getNotifications()
}) })
.catch(LNbits.utils.notifyApiError) .catch(LNbits.utils.notifyApiError)
}, },
@ -441,8 +444,7 @@ window.AdminPageLogic = {
.then(response => { .then(response => {
this.needsRestart = this.needsRestart =
this.settings.lnbits_backend_wallet_class !== this.settings.lnbits_backend_wallet_class !==
this.formData.lnbits_backend_wallet_class || this.formData.lnbits_backend_wallet_class
this.settings.lnbits_killswitch !== this.formData.lnbits_killswitch
this.settings = this.formData this.settings = this.formData
this.formData = _.clone(this.settings) this.formData = _.clone(this.settings)
Quasar.Notify.create({ Quasar.Notify.create({

View file

@ -555,6 +555,7 @@
filled filled
:label="$t('credit_label', {denomination: denomination})" :label="$t('credit_label', {denomination: denomination})"
v-model="scope.value" v-model="scope.value"
type="number"
dense dense
autofocus autofocus
@keyup.enter="updateBalance(scope)" @keyup.enter="updateBalance(scope)"

View file

@ -1,13 +1,22 @@
import base64 import base64
import hashlib import hashlib
import json import json
from typing import Dict, Union import re
from typing import Dict, Tuple, Union
from urllib.parse import urlparse
import secp256k1 import secp256k1
from bech32 import bech32_decode, bech32_encode, convertbits from bech32 import bech32_decode, bech32_encode, convertbits
from Cryptodome import Random from Cryptodome import Random
from Cryptodome.Cipher import AES from Cryptodome.Cipher import AES
from Cryptodome.Util.Padding import pad, unpad from Cryptodome.Util.Padding import pad, unpad
from pynostr.key import PrivateKey
def generate_keypair() -> Tuple[str, str]:
private_key = PrivateKey()
public_key = private_key.public_key
return private_key.hex(), public_key.hex()
def encrypt_content( def encrypt_content(
@ -155,22 +164,30 @@ def json_dumps(data: Union[Dict, list]) -> str:
return json.dumps(data, separators=(",", ":"), ensure_ascii=False) return json.dumps(data, separators=(",", ":"), ensure_ascii=False)
def normalize_public_key(pubkey: str) -> str: def normalize_public_key(key: str) -> str:
if pubkey.startswith("npub1"): return normalize_bech32_key("npub1", key)
_, decoded_data = bech32_decode(pubkey)
assert decoded_data, "Public Key is not valid npub."
def normalize_private_key(key: str) -> str:
return normalize_bech32_key("nsec1", key)
def normalize_bech32_key(hrp: str, key: str) -> str:
if key.startswith(hrp):
_, decoded_data = bech32_decode(key)
assert decoded_data, f"Key is not valid {hrp}."
decoded_data_bits = convertbits(decoded_data, 5, 8, False) decoded_data_bits = convertbits(decoded_data, 5, 8, False)
assert decoded_data_bits, "Public Key is not valid npub." assert decoded_data_bits, f"Key is not valid {hrp}."
return bytes(decoded_data_bits).hex() return bytes(decoded_data_bits).hex()
assert len(pubkey) == 64, "Public key has wrong length." assert len(key) == 64, "Key has wrong length."
try: try:
int(pubkey, 16) int(key, 16)
except Exception as exc: except Exception as exc:
raise AssertionError("Public Key is not valid hex.") from exc raise AssertionError("Key is not valid hex.") from exc
return pubkey return key
def hex_to_npub(hex_pubkey: str) -> str: def hex_to_npub(hex_pubkey: str) -> str:
@ -188,3 +205,46 @@ def hex_to_npub(hex_pubkey: str) -> str:
bits = convertbits(pubkey_bytes, 8, 5, True) bits = convertbits(pubkey_bytes, 8, 5, True)
assert bits assert bits
return bech32_encode("npub", bits) return bech32_encode("npub", bits)
def normalize_identifier(identifier: str):
identifier = identifier.lower().split("@")[0]
validate_identifier(identifier)
return identifier
def validate_pub_key(pubkey: str) -> str:
if pubkey.startswith("npub"):
_, data = bech32_decode(pubkey)
if data:
decoded_data = convertbits(data, 5, 8, False)
if decoded_data:
pubkey = bytes(decoded_data).hex()
try:
_hex = bytes.fromhex(pubkey)
except Exception as exc:
raise ValueError("Pubkey must be in npub or hex format.") from exc
if len(_hex) != 32:
raise ValueError("Pubkey length incorrect.")
return pubkey
def validate_identifier(local_part: str):
regex = re.compile(r"^[a-z0-9_.]+$")
if not re.fullmatch(regex, local_part.lower()):
raise ValueError(
f"Identifier '{local_part}' not allowed! "
"Only a-z, 0-9 and .-_ are allowed characters, case insensitive."
)
def is_ws_url(url):
try:
result = urlparse(url)
if not all([result.scheme, result.netloc]):
return False
return result.scheme in ["ws", "wss"]
except ValueError:
return False

152
poetry.lock generated
View file

@ -1587,6 +1587,30 @@ win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""}
[package.extras] [package.extras]
dev = ["Sphinx (==7.2.5)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.2.2)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.4.1)", "mypy (==v1.5.1)", "pre-commit (==3.4.0)", "pytest (==6.1.2)", "pytest (==7.4.0)", "pytest-cov (==2.12.1)", "pytest-cov (==4.1.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.0.0)", "sphinx-autobuild (==2021.3.14)", "sphinx-rtd-theme (==1.3.0)", "tox (==3.27.1)", "tox (==4.11.0)"] dev = ["Sphinx (==7.2.5)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.2.2)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.4.1)", "mypy (==v1.5.1)", "pre-commit (==3.4.0)", "pytest (==6.1.2)", "pytest (==7.4.0)", "pytest-cov (==2.12.1)", "pytest-cov (==4.1.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.0.0)", "sphinx-autobuild (==2021.3.14)", "sphinx-rtd-theme (==1.3.0)", "tox (==3.27.1)", "tox (==4.11.0)"]
[[package]]
name = "markdown-it-py"
version = "3.0.0"
description = "Python port of markdown-it. Markdown parsing, done right!"
optional = false
python-versions = ">=3.8"
files = [
{file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
{file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
]
[package.dependencies]
mdurl = ">=0.1,<1.0"
[package.extras]
benchmarking = ["psutil", "pytest", "pytest-benchmark"]
code-style = ["pre-commit (>=3.0,<4.0)"]
compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"]
linkify = ["linkify-it-py (>=1,<3)"]
plugins = ["mdit-py-plugins"]
profiling = ["gprof2dot"]
rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
[[package]] [[package]]
name = "markupsafe" name = "markupsafe"
version = "2.1.3" version = "2.1.3"
@ -1676,6 +1700,17 @@ docs = ["alabaster (==0.7.13)", "autodocsumm (==0.2.11)", "sphinx (==7.0.1)", "s
lint = ["flake8 (==6.0.0)", "flake8-bugbear (==23.7.10)", "mypy (==1.4.1)", "pre-commit (>=2.4,<4.0)"] lint = ["flake8 (==6.0.0)", "flake8-bugbear (==23.7.10)", "mypy (==1.4.1)", "pre-commit (>=2.4,<4.0)"]
tests = ["pytest", "pytz", "simplejson"] tests = ["pytest", "pytz", "simplejson"]
[[package]]
name = "mdurl"
version = "0.1.2"
description = "Markdown URL utilities"
optional = false
python-versions = ">=3.7"
files = [
{file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
]
[[package]] [[package]]
name = "mock" name = "mock"
version = "5.1.0" version = "5.1.0"
@ -2090,6 +2125,20 @@ typing-extensions = ">=4.2.0"
dotenv = ["python-dotenv (>=0.10.4)"] dotenv = ["python-dotenv (>=0.10.4)"]
email = ["email-validator (>=1.0.3)"] email = ["email-validator (>=1.0.3)"]
[[package]]
name = "pygments"
version = "2.19.1"
description = "Pygments is a syntax highlighting package written in Python."
optional = false
python-versions = ">=3.8"
files = [
{file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"},
{file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"},
]
[package.extras]
windows-terminal = ["colorama (>=0.4.6)"]
[[package]] [[package]]
name = "pyjwt" name = "pyjwt"
version = "2.9.0" version = "2.9.0"
@ -2151,6 +2200,29 @@ coincurve = ">=20,<21"
cryptography = ">=42,<43" cryptography = ">=42,<43"
PySocks = ">=1,<2" PySocks = ">=1,<2"
[[package]]
name = "pynostr"
version = "0.6.2"
description = "Python Library for nostr."
optional = false
python-versions = ">3.7.0"
files = [
{file = "pynostr-0.6.2-py3-none-any.whl", hash = "sha256:d43fb236c73174093275ee0080b2f8ed17e974b2b516f0d73da4c9a3e908ddc5"},
{file = "pynostr-0.6.2.tar.gz", hash = "sha256:2974ea05b3ff41a1a4060e3b1813eb0ce0e60c0b81fbe668afaa65164c7f82f4"},
]
[package.dependencies]
coincurve = ">=1.8.0"
cryptography = ">=37.0.4"
requests = "*"
rich = "*"
tlv8 = "*"
tornado = "*"
typer = "*"
[package.extras]
websocket-client = ["websocket-client (>=1.3.3)"]
[[package]] [[package]]
name = "pyqrcode" name = "pyqrcode"
version = "1.2.1" version = "1.2.1"
@ -2351,7 +2423,6 @@ files = [
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
@ -2436,6 +2507,25 @@ files = [
[package.dependencies] [package.dependencies]
six = "*" six = "*"
[[package]]
name = "rich"
version = "13.9.4"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
optional = false
python-versions = ">=3.8.0"
files = [
{file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"},
{file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"},
]
[package.dependencies]
markdown-it-py = ">=2.2.0"
pygments = ">=2.13.0,<3.0.0"
typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""}
[package.extras]
jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]] [[package]]
name = "rpds-py" name = "rpds-py"
version = "0.20.0" version = "0.20.0"
@ -2625,6 +2715,17 @@ files = [
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
[[package]]
name = "shellingham"
version = "1.5.4"
description = "Tool to Detect Surrounding Shell"
optional = false
python-versions = ">=3.7"
files = [
{file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"},
{file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"},
]
[[package]] [[package]]
name = "shortuuid" name = "shortuuid"
version = "1.0.13" version = "1.0.13"
@ -2787,6 +2888,16 @@ typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""
[package.extras] [package.extras]
full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"]
[[package]]
name = "tlv8"
version = "0.10.0"
description = "Python module to handle type-length-value (TLV) encoded data 8-bit type, 8-bit length, and N-byte value as described within the Apple HomeKit Accessory Protocol Specification Non-Commercial Version Release R2."
optional = false
python-versions = "*"
files = [
{file = "tlv8-0.10.0.tar.gz", hash = "sha256:7930a590267b809952272ac2a27ee81b99ec5191fa2eba08050e0daee4262684"},
]
[[package]] [[package]]
name = "tomli" name = "tomli"
version = "2.0.1" version = "2.0.1"
@ -2798,6 +2909,26 @@ files = [
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
] ]
[[package]]
name = "tornado"
version = "6.4.2"
description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed."
optional = false
python-versions = ">=3.8"
files = [
{file = "tornado-6.4.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e828cce1123e9e44ae2a50a9de3055497ab1d0aeb440c5ac23064d9e44880da1"},
{file = "tornado-6.4.2-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:072ce12ada169c5b00b7d92a99ba089447ccc993ea2143c9ede887e0937aa803"},
{file = "tornado-6.4.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a017d239bd1bb0919f72af256a970624241f070496635784d9bf0db640d3fec"},
{file = "tornado-6.4.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c36e62ce8f63409301537222faffcef7dfc5284f27eec227389f2ad11b09d946"},
{file = "tornado-6.4.2-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca9eb02196e789c9cb5c3c7c0f04fb447dc2adffd95265b2c7223a8a615ccbf"},
{file = "tornado-6.4.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:304463bd0772442ff4d0f5149c6f1c2135a1fae045adf070821c6cdc76980634"},
{file = "tornado-6.4.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:c82c46813ba483a385ab2a99caeaedf92585a1f90defb5693351fa7e4ea0bf73"},
{file = "tornado-6.4.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:932d195ca9015956fa502c6b56af9eb06106140d844a335590c1ec7f5277d10c"},
{file = "tornado-6.4.2-cp38-abi3-win32.whl", hash = "sha256:2876cef82e6c5978fde1e0d5b1f919d756968d5b4282418f3146b79b58556482"},
{file = "tornado-6.4.2-cp38-abi3-win_amd64.whl", hash = "sha256:908b71bf3ff37d81073356a5fadcc660eb10c1476ee6e2725588626ce7e5ca38"},
{file = "tornado-6.4.2.tar.gz", hash = "sha256:92bad5b4746e9879fd7bf1eb21dce4e3fc5128d71601f80005afa39237ad620b"},
]
[[package]] [[package]]
name = "tqdm" name = "tqdm"
version = "4.66.4" version = "4.66.4"
@ -2818,6 +2949,23 @@ notebook = ["ipywidgets (>=6)"]
slack = ["slack-sdk"] slack = ["slack-sdk"]
telegram = ["requests"] telegram = ["requests"]
[[package]]
name = "typer"
version = "0.15.1"
description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
optional = false
python-versions = ">=3.7"
files = [
{file = "typer-0.15.1-py3-none-any.whl", hash = "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847"},
{file = "typer-0.15.1.tar.gz", hash = "sha256:a0588c0a7fa68a1978a069818657778f86abe6ff5ea6abf472f940a08bfe4f0a"},
]
[package.dependencies]
click = ">=8.0.0"
rich = ">=10.11.0"
shellingham = ">=1.3.0"
typing-extensions = ">=3.7.4.3"
[[package]] [[package]]
name = "types-mock" name = "types-mock"
version = "5.1.0.20240425" version = "5.1.0.20240425"
@ -3234,4 +3382,4 @@ liquid = ["wallycore"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.12 | ^3.11 | ^3.10 | ^3.9" python-versions = "^3.12 | ^3.11 | ^3.10 | ^3.9"
content-hash = "63319dd52462ba4066b53ad3770eb8adff345d92593fa9111f08b126ac3eb68a" content-hash = "a8a4a09601a1c5dcbbcf35aab4e1a2cd3f6ed4f3445bf2c47b055318121eda9d"

View file

@ -61,6 +61,7 @@ wallycore = {version = "1.3.0", optional = true}
breez-sdk = {version = "0.6.6", optional = true} breez-sdk = {version = "0.6.6", optional = true}
jsonpath-ng = "^1.7.0" jsonpath-ng = "^1.7.0"
pynostr = "^0.6.2"
[tool.poetry.extras] [tool.poetry.extras]
breez = ["breez-sdk"] breez = ["breez-sdk"]
liquid = ["wallycore"] liquid = ["wallycore"]
@ -136,6 +137,7 @@ module = [
"bitstring.*", "bitstring.*",
"ecdsa.*", "ecdsa.*",
"pyngrok.*", "pyngrok.*",
"pynostr.*",
"pyln.client.*", "pyln.client.*",
"py_vapid.*", "py_vapid.*",
"pywebpush.*", "pywebpush.*",

View file

@ -21,7 +21,7 @@ from .helpers import (
async def get_node_balance_sats(): async def get_node_balance_sats():
balance = await get_balance_delta() balance = await get_balance_delta()
return balance.node_balance_msats / 1000 return balance.node_balance_sats
@pytest.mark.anyio @pytest.mark.anyio