Compare commits

..
Author SHA1 Message Date
Tiago Vasconcelos 55484b9cb4 feat: update payment extra 2025-07-09 22:08:34 +01:00
dni ⚡andGitHub 4db7d20e60 chore: get rid of mypy notes and typecheck more functions (#3257) 2025-07-09 12:08:48 +03:00
02fec90a29 refactor: theme options, includes fix bg on firefox with dialog (#3247)
Co-authored-by: dni  <office@dnilabs.com>
2025-07-09 11:28:35 +03:00
Tiago VasconcelosandGitHub 4efe1a9fca fix: default theme settings (#3256) 2025-07-08 21:03:11 +02:00
cba8c02d68 feat: Add support for the new CLNRest as a funding source (#2778)
Co-authored-by: Amperstrand <amperstrand@localhost>
Co-authored-by: dni  <office@dnilabs.com>
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2025-07-08 21:01:36 +02:00
ArcandGitHub 5c9d375713 chore: tooltip for ad (#3255) 2025-07-08 15:35:40 +03:00
dni ⚡andGitHub 264d1fa07c chore: update version to 1.2.0 (#3253) 2025-07-08 10:15:13 +02:00
Vlad StanandGitHub fc3e2b2b4d chore: upgrade protobuf (#3252) 2025-07-08 10:52:14 +03:00
dni ⚡andGitHub 64edeebaaf fix: update and fixes for BreezWallet (#3246) 2025-07-07 18:58:11 +03:00
dni ⚡andGitHub c2739188d0 CI: fix using all optional dependecies (#3251) 2025-07-07 18:22:43 +03:00
3f4d778c95 fix: update success status (#3244)
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2025-07-07 18:01:15 +03:00
dni ⚡andGitHub c1788c7219 feat: run_interval in tasks and refactor check_pending_payments (#3245) 2025-07-07 14:52:45 +03:00
07c62285c3 [fix] bandit assert warnings (#3243)
Co-authored-by: dni  <office@dnilabs.com>
2025-07-05 13:16:49 +03:00
Vlad StanandGitHub 841c8ce3a9 [fix] bandit sql warnings (#3242) 2025-07-05 11:12:47 +02:00
Vlad StanandGitHub cc6430221c [fix] Small bandit changes (#3241) 2025-07-04 17:29:16 +03:00
4a34548864 feat: add Breez *Liquid* SDK funding source (#2681)
Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
Co-authored-by: dni  <office@dnilabs.com>
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2025-07-04 14:57:56 +03:00
Vlad StanandGitHub 4a3a3a0425 refactor: extract create_payment_request (#3240) 2025-07-04 12:58:47 +03:00
bf8b33a7b2 fix: phoenixd pending payments (#3075)
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2025-07-04 12:01:50 +03:00
7e08211887 feat: Allow custom memo on pay invoice (#3236)
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2025-07-04 11:59:57 +03:00
Vlad Stan 0bbb0aca2d refactor: use while settings.lnbits_running instead of while True 2025-07-03 12:04:46 +03:00
dni ⚡andGitHub 9b1418b1f4 chore: reduce max complexity lint (#3230) 2025-07-02 14:02:47 +03:00
Tiago VasconcelosandGitHub 5f0a8d78e0 [Fix] Display fee in sats (#3235) 2025-07-02 13:38:35 +03:00
dni ⚡andGitHub 2fe8ae1cb1 chore: update to version v1.2.0-rc3 (#3231) 2025-07-01 13:04:50 +02:00
Vlad StanandGitHub 1a3d23bbee fix: condition for available, but not installed extension (#3232) 2025-07-01 13:33:23 +03:00
Vlad StanandGitHub ae45c1b958 doc: LNBITS_EXTENSIONS_PATH (#3229) 2025-06-30 16:27:44 +03:00
dni ⚡andGitHub 8337351d6a chore: update python packages + formatting + cleanup (#3221) 2025-06-30 15:53:11 +03:00
dni ⚡andGitHub f4dbd369be test: add regtest testcase for no route failure (#3189) 2025-06-30 14:51:34 +02:00
Vlad StanandGitHub 974ee26224 fix: url_for should not return extension upgrades temp paths (#3227) 2025-06-30 15:30:57 +03:00
dni ⚡andGitHub e4fb4453c3 fix: circular import on handle_fiat_payments (#3226) 2025-06-30 14:00:34 +02:00
dni ⚡andGitHub aa7129a351 feat: add internal_invoice_queue_put to get rid of dynamic import (#3223) 2025-06-30 12:55:23 +02:00
Vlad StanandGitHub aba6e4cd87 [feat] Add stripe payments (#3184) 2025-06-30 13:13:13 +03:00
blackcoffeexbtandGitHub f4e7d0c70c Label tweaks (#3224) 2025-06-30 11:14:44 +03:00
Vlad StanandGitHub 4247b9d9a3 feat: add external id for users (#3219) 2025-06-26 15:33:38 +02:00
68 changed files with 7120 additions and 5312 deletions
+4 -9
View File
@@ -4,7 +4,6 @@ import importlib
import os
import shutil
import sys
import time
from contextlib import asynccontextmanager
from pathlib import Path
from typing import Callable, Optional
@@ -27,7 +26,7 @@ from lnbits.core.crud.extensions import create_installed_extension
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.notifications import enqueue_admin_notification
from lnbits.core.services.notifications import enqueue_notification
from lnbits.core.services.payments import check_pending_payments
from lnbits.core.tasks import (
audit_queue,
@@ -71,9 +70,8 @@ from .tasks import internal_invoice_listener, invoice_listener, run_interval
async def startup(app: FastAPI):
logger.info(f"Starting LNbits Version: {settings.version}")
start = time.perf_counter()
settings.lnbits_running = True
# wait till migration is done
await migrate_databases()
@@ -104,7 +102,7 @@ async def startup(app: FastAPI):
# initialize tasks
register_async_tasks()
enqueue_admin_notification(
enqueue_notification(
NotificationType.server_start_stop,
{
"message": "LNbits server started.",
@@ -112,13 +110,10 @@ async def startup(app: FastAPI):
},
)
end = time.perf_counter()
logger.success(f"LNbits started in {end - start:.2f} seconds.")
async def shutdown():
logger.warning("LNbits shutting down...")
enqueue_admin_notification(
enqueue_notification(
NotificationType.server_start_stop,
{
"message": "LNbits server shutting down...",
-2
View File
@@ -11,7 +11,6 @@ from .views.fiat_api import fiat_router
# this compat is needed for usermanager extension
from .views.generic import generic_router
from .views.lnurl_api import lnurl_router
from .views.node_api import node_router, public_node_router, super_node_router
from .views.payment_api import payment_router
from .views.tinyurl_api import tinyurl_router
@@ -43,7 +42,6 @@ def init_core_routers(app: FastAPI):
app.include_router(users_router)
app.include_router(audit_router)
app.include_router(fiat_router)
app.include_router(lnurl_router)
__all__ = ["core_app", "core_app_extra", "db"]
+7 -5
View File
@@ -19,8 +19,13 @@ from ..models import (
)
def update_payment_extra():
pass
async def update_payment_extra(
payment: Payment, conn: Optional[Connection] = None
) -> Payment:
return await (conn or db).execute(
"UPDATE apipayments SET extra = :extra WHERE checking_id = :checking_id",
{"extra": payment.extra, "checking_id": payment.checking_id},
)
async def get_payment(checking_id: str, conn: Optional[Connection] = None) -> Payment:
@@ -380,7 +385,6 @@ async def get_payment_count_stats(
user_id: Optional[str] = None,
conn: Optional[Connection] = None,
) -> list[PaymentCountStat]:
if not filters:
filters = Filters()
extra_stmts = []
@@ -413,7 +417,6 @@ async def get_daily_stats(
user_id: Optional[str] = None,
conn: Optional[Connection] = None,
) -> tuple[list[PaymentDailyStats], list[PaymentDailyStats]]:
if not filters:
filters = Filters()
@@ -463,7 +466,6 @@ async def get_wallets_stats(
user_id: Optional[str] = None,
conn: Optional[Connection] = None,
) -> list[PaymentWalletStats]:
if not filters:
filters = Filters()
+4 -7
View File
@@ -1,5 +1,5 @@
from .audit import AuditEntry, AuditFilters
from .lnurl import CreateLnurlPayment, CreateLnurlWithdraw
from .lnurl import CreateLnurl, CreateLnurlAuth, PayLnurlWData
from .misc import (
BalanceDelta,
Callback,
@@ -9,7 +9,6 @@ from .misc import (
SimpleStatus,
)
from .payments import (
CancelInvoice,
CreateInvoice,
CreatePayment,
DecodePayment,
@@ -24,7 +23,6 @@ from .payments import (
PaymentsStatusCount,
PaymentState,
PaymentWalletStats,
SettleInvoice,
)
from .tinyurl import TinyURL
from .users import (
@@ -59,12 +57,11 @@ __all__ = [
"BalanceDelta",
"BaseWallet",
"Callback",
"CancelInvoice",
"ConversionData",
"CoreAppExtra",
"CreateInvoice",
"CreateLnurlPayment",
"CreateLnurlWithdraw",
"CreateLnurl",
"CreateLnurlAuth",
"CreatePayment",
"CreateUser",
"CreateWallet",
@@ -75,6 +72,7 @@ __all__ = [
"LoginUsernamePassword",
"LoginUsr",
"PayInvoice",
"PayLnurlWData",
"Payment",
"PaymentCountField",
"PaymentCountStat",
@@ -87,7 +85,6 @@ __all__ = [
"PaymentsStatusCount",
"RegisterUser",
"ResetUserPassword",
"SettleInvoice",
"SimpleStatus",
"TinyURL",
"UpdateBalance",
+10 -5
View File
@@ -1,16 +1,21 @@
from typing import Optional
from lnurl import Lnurl, LnurlPayResponse
from pydantic import BaseModel
class CreateLnurlPayment(BaseModel):
res: LnurlPayResponse
class CreateLnurl(BaseModel):
description_hash: str
callback: str
amount: int
comment: Optional[str] = None
description: Optional[str] = None
unit: Optional[str] = None
internal_memo: Optional[str] = None
class CreateLnurlWithdraw(BaseModel):
lnurl_w: Lnurl
class CreateLnurlAuth(BaseModel):
callback: str
class PayLnurlWData(BaseModel):
lnurl_w: str
-3
View File
@@ -2,8 +2,6 @@ from enum import Enum
from pydantic import BaseModel
from lnbits.core.models.users import UserNotifications
class NotificationType(Enum):
server_status = "server_status"
@@ -20,7 +18,6 @@ class NotificationType(Enum):
class NotificationMessage(BaseModel):
message_type: NotificationType
values: dict
user_notifications: UserNotifications | None = None
NOTIFICATION_TEMPLATES = {
+1 -42
View File
@@ -5,7 +5,6 @@ from enum import Enum
from typing import Literal
from fastapi import Query
from lnurl import LnurlWithdrawResponse
from pydantic import BaseModel, Field, validator
from lnbits.db import FilterModel
@@ -253,25 +252,13 @@ class CreateInvoice(BaseModel):
memo: str | None = Query(None, max_length=640)
description_hash: str | None = None
unhashed_description: str | None = None
payment_hash: str | None = Query(
None,
description="The payment hash of the hold invoice.",
min_length=64,
max_length=64,
)
expiry: int | None = None
extra: dict | None = None
webhook: str | None = None
bolt11: str | None = None
lnurl_withdraw: LnurlWithdrawResponse | None = None
lnurl_callback: str | None = None
fiat_provider: str | None = None
@validator("payment_hash")
def check_hex(cls, v):
if v:
_ = bytes.fromhex(v)
return v
@validator("unit")
@classmethod
def unit_is_from_allowed_currencies(cls, v):
@@ -285,31 +272,3 @@ class PaymentsStatusCount(BaseModel):
outgoing: int = 0
failed: int = 0
pending: int = 0
class SettleInvoice(BaseModel):
preimage: str = Field(
...,
description="The preimage of the payment hash to settle the invoice.",
min_length=64,
max_length=64,
)
@validator("preimage")
def check_hex(cls, v):
_ = bytes.fromhex(v)
return v
class CancelInvoice(BaseModel):
payment_hash: str = Field(
...,
description="The payment hash of the invoice to cancel.",
min_length=64,
max_length=64,
)
@validator("payment_hash")
def check_hex(cls, v):
_ = bytes.fromhex(v)
return v
+5 -18
View File
@@ -3,8 +3,8 @@ from __future__ import annotations
from datetime import datetime, timezone
from uuid import UUID
from bcrypt import checkpw, gensalt, hashpw
from fastapi import Query
from passlib.context import CryptContext
from pydantic import BaseModel, Field
from lnbits.core.models.misc import SimpleItem
@@ -20,15 +20,6 @@ from lnbits.settings import settings
from .wallets import Wallet
class UserNotifications(BaseModel):
nostr_identifier: str | None = None
telegram_chat_id: str | None = None
email_address: str | None = None
excluded_wallets: list[str] = []
outgoing_payments_sats: int = 0
incoming_payments_sats: int = 0
class UserExtra(BaseModel):
email_verified: bool | None = False
first_name: str | None = None
@@ -44,8 +35,6 @@ class UserExtra(BaseModel):
# how many wallets are shown in the user interface
visible_wallet_count: int | None = 10
notifications: UserNotifications = UserNotifications()
class EndpointAccess(BaseModel):
path: str
@@ -131,18 +120,16 @@ class Account(BaseModel):
def hash_password(self, password: str) -> str:
"""sets and returns the hashed password"""
salt = gensalt()
hashed_pw = hashpw(password.encode(), salt)
if not hashed_pw:
raise ValueError("Password hashing failed.")
self.password_hash = hashed_pw.decode()
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
self.password_hash = pwd_context.hash(password)
return self.password_hash
def verify_password(self, password: str) -> bool:
"""returns True if the password matches the hash"""
if not self.password_hash:
return False
return checkpw(password.encode(), self.password_hash.encode())
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
return pwd_context.verify(password, self.password_hash)
def validate_fields(self):
if self.username and not is_valid_username(self.username):
+1 -1
View File
@@ -7,11 +7,11 @@ from datetime import datetime, timezone
from enum import Enum
from ecdsa import SECP256k1, SigningKey
from lnurl import encode as lnurl_encode
from pydantic import BaseModel, Field
from lnbits.db import FilterModel
from lnbits.helpers import url_for
from lnbits.lnurl import encode as lnurl_encode
from lnbits.settings import settings
+5 -9
View File
@@ -2,11 +2,10 @@ from .funding_source import (
get_balance_delta,
switch_to_voidwallet,
)
from .lnurl import fetch_lnurl_pay_request, get_pr_from_lnurl
from .notifications import enqueue_admin_notification, send_payment_notification
from .lnurl import perform_lnurlauth, redeem_lnurl_withdraw
from .notifications import enqueue_notification, send_payment_notification
from .payments import (
calculate_fiat_amounts,
cancel_hold_invoice,
check_transaction_status,
check_wallet_limits,
create_fiat_invoice,
@@ -18,7 +17,6 @@ from .payments import (
get_payments_daily_stats,
pay_invoice,
service_fee,
settle_hold_invoice,
update_pending_payment,
update_pending_payments,
update_wallet_balance,
@@ -38,7 +36,6 @@ from .websockets import websocket_manager, websocket_updater
__all__ = [
"calculate_fiat_amounts",
"cancel_hold_invoice",
"check_admin_settings",
"check_transaction_status",
"check_wallet_limits",
@@ -49,17 +46,16 @@ __all__ = [
"create_user_account",
"create_user_account_no_ckeck",
"create_wallet_invoice",
"enqueue_admin_notification",
"enqueue_notification",
"fee_reserve",
"fee_reserve_total",
"fetch_lnurl_pay_request",
"get_balance_delta",
"get_payments_daily_stats",
"get_pr_from_lnurl",
"pay_invoice",
"perform_lnurlauth",
"redeem_lnurl_withdraw",
"send_payment_notification",
"service_fee",
"settle_hold_invoice",
"switch_to_voidwallet",
"update_cached_settings",
"update_pending_payment",
-5
View File
@@ -176,11 +176,6 @@ async def test_connection(provider: str) -> SimpleStatus:
This function should be called when setting up or testing the Stripe integration.
"""
fiat_provider = await get_fiat_provider(provider)
if not fiat_provider:
return SimpleStatus(
success=False,
message=f"Fiat provider '{provider}' not found.",
)
status = await fiat_provider.status()
if status.error_message:
return SimpleStatus(
+3 -3
View File
@@ -1,7 +1,7 @@
from loguru import logger
from lnbits.core.models.notifications import NotificationType
from lnbits.core.services.notifications import enqueue_admin_notification
from lnbits.core.services.notifications import enqueue_notification
from lnbits.settings import settings
from lnbits.wallets import get_funding_source, set_funding_source
@@ -51,7 +51,7 @@ async def check_server_balance_against_node():
f"Balance delta reached: {status.delta_sats} sats."
f" Switch to void wallet: {use_voidwallet}."
)
enqueue_admin_notification(
enqueue_notification(
NotificationType.watchdog_check,
{
"delta_sats": status.delta_sats,
@@ -71,7 +71,7 @@ async def check_balance_delta_changed():
settings.latest_balance_delta_sats = status.delta_sats
return
if status.delta_sats != settings.latest_balance_delta_sats:
enqueue_admin_notification(
enqueue_notification(
NotificationType.balance_delta,
{
"delta_sats": status.delta_sats,
+148 -42
View File
@@ -1,53 +1,159 @@
from lnurl import (
LnurlPayActionResponse,
LnurlPayResponse,
LnurlResponseException,
execute_pay_request,
handle,
import asyncio
import json
from io import BytesIO
from typing import Optional
from urllib.parse import parse_qs, urlparse
import httpx
from fastapi import Depends
from loguru import logger
from lnbits.db import Connection
from lnbits.decorators import (
WalletTypeInfo,
require_admin_key,
)
from lnbits.core.models import CreateLnurlPayment
from lnbits.helpers import check_callback_url, url_for
from lnbits.lnurl import LnurlErrorResponse
from lnbits.lnurl import decode as decode_lnurl
from lnbits.settings import settings
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
from .payments import create_invoice
async def get_pr_from_lnurl(lnurl: str, amount_msat: int) -> str:
res = await handle(lnurl, user_agent=settings.user_agent, timeout=10)
if not isinstance(res, LnurlPayResponse):
raise LnurlResponseException(
"Invalid LNURL response. Expected LnurlPayResponse."
async def redeem_lnurl_withdraw(
wallet_id: str,
lnurl_request: str,
memo: Optional[str] = None,
extra: Optional[dict] = None,
wait_seconds: int = 0,
conn: Optional[Connection] = None,
) -> None:
if not lnurl_request:
return None
res = {}
headers = {"User-Agent": settings.user_agent}
async with httpx.AsyncClient(headers=headers) as client:
lnurl = decode_lnurl(lnurl_request)
check_callback_url(str(lnurl))
r = await client.get(str(lnurl))
res = r.json()
try:
_, payment_request = await create_invoice(
wallet_id=wallet_id,
amount=int(res["maxWithdrawable"] / 1000),
memo=memo or res["defaultDescription"] or "",
extra=extra,
conn=conn,
)
res2 = await execute_pay_request(
res,
msat=str(amount_msat),
user_agent=settings.user_agent,
timeout=10,
)
if not isinstance(res, LnurlPayActionResponse):
raise LnurlResponseException(
"Invalid LNURL pay response. Expected LnurlPayActionResponse."
except Exception:
logger.warning(
f"failed to create invoice on redeem_lnurl_withdraw "
f"from {lnurl}. params: {res}"
)
return res2.pr
return None
if wait_seconds:
await asyncio.sleep(wait_seconds)
params = {"k1": res["k1"], "pr": payment_request}
try:
params["balanceNotify"] = url_for(
f"/withdraw/notify/{urlparse(lnurl_request).netloc}",
external=True,
wal=wallet_id,
)
except Exception as exc:
logger.debug(exc)
headers = {"User-Agent": settings.user_agent}
async with httpx.AsyncClient(headers=headers) as client:
try:
check_callback_url(res["callback"])
await client.get(res["callback"], params=params)
except Exception as exc:
logger.debug(exc)
async def fetch_lnurl_pay_request(data: CreateLnurlPayment) -> LnurlPayActionResponse:
"""
Pay an LNURL payment request.
async def perform_lnurlauth(
callback: str,
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> Optional[LnurlErrorResponse]:
cb = urlparse(callback)
raises `LnurlResponseException` if pay request fails
"""
k1 = bytes.fromhex(parse_qs(cb.query)["k1"][0])
if data.unit and data.unit != "sat":
# shift to float with 2 decimal places
amount = round(data.amount / 1000, 2)
amount_msat = await fiat_amount_as_satoshis(amount, data.unit)
amount_msat *= 1000
else:
amount_msat = data.amount
key = wallet.wallet.lnurlauth_key(cb.netloc)
return await execute_pay_request(
data.res,
msat=str(amount_msat),
user_agent=settings.user_agent,
timeout=10,
)
def int_to_bytes_suitable_der(x: int) -> bytes:
"""for strict DER we need to encode the integer with some quirks"""
b = x.to_bytes((x.bit_length() + 7) // 8, "big")
if len(b) == 0:
# ensure there's at least one byte when the int is zero
return bytes([0])
if b[0] & 0x80 != 0:
# ensure it doesn't start with a 0x80 and so it isn't
# interpreted as a negative number
return bytes([0]) + b
return b
def encode_strict_der(r: int, s: int, order: int):
# if s > order/2 verification will fail sometimes
# so we must fix it here see:
# https://github.com/indutny/elliptic/blob/e71b2d9359c5fe9437fbf46f1f05096de447de57/lib/elliptic/ec/index.js#L146-L147
if s > order // 2:
s = order - s
# now we do the strict DER encoding copied from
# https://github.com/KiriKiri/bip66 (without any checks)
r_temp = int_to_bytes_suitable_der(r)
s_temp = int_to_bytes_suitable_der(s)
r_len = len(r_temp)
s_len = len(s_temp)
sign_len = 6 + r_len + s_len
signature = BytesIO()
signature.write(0x30.to_bytes(1, "big", signed=False))
signature.write((sign_len - 2).to_bytes(1, "big", signed=False))
signature.write(0x02.to_bytes(1, "big", signed=False))
signature.write(r_len.to_bytes(1, "big", signed=False))
signature.write(r_temp)
signature.write(0x02.to_bytes(1, "big", signed=False))
signature.write(s_len.to_bytes(1, "big", signed=False))
signature.write(s_temp)
return signature.getvalue()
sig = key.sign_digest_deterministic(k1, sigencode=encode_strict_der)
headers = {"User-Agent": settings.user_agent}
async with httpx.AsyncClient(headers=headers) as client:
if not key.verifying_key:
raise ValueError("LNURLauth verifying_key does not exist")
check_callback_url(callback)
r = await client.get(
callback,
params={
"k1": k1.hex(),
"key": key.verifying_key.to_string("compressed").hex(),
"sig": sig.hex(),
},
)
try:
resp = json.loads(r.text)
if resp["status"] == "OK":
return None
return LnurlErrorResponse(reason=resp["reason"])
except (KeyError, json.decoder.JSONDecodeError):
return LnurlErrorResponse(
reason=r.text[:200] + "..." if len(r.text) > 200 else r.text
)
+71 -149
View File
@@ -16,14 +16,12 @@ from lnbits.core.crud import (
get_webpush_subscriptions_for_user,
mark_webhook_sent,
)
from lnbits.core.crud.users import get_user
from lnbits.core.models import Payment, Wallet
from lnbits.core.models.notifications import (
NOTIFICATION_TEMPLATES,
NotificationMessage,
NotificationType,
)
from lnbits.core.models.users import UserNotifications
from lnbits.core.services.nostr import fetch_nip5_details, send_nostr_dm
from lnbits.core.services.websockets import websocket_manager
from lnbits.helpers import check_callback_url, is_valid_email_address
@@ -33,8 +31,8 @@ from lnbits.utils.nostr import normalize_private_key
notifications_queue: asyncio.Queue[NotificationMessage] = asyncio.Queue()
def enqueue_admin_notification(message_type: NotificationType, values: dict) -> None:
if not _is_message_type_enabled(message_type):
def enqueue_notification(message_type: NotificationType, values: dict) -> None:
if not is_message_type_enabled(message_type):
return
try:
notifications_queue.put_nowait(
@@ -44,125 +42,62 @@ def enqueue_admin_notification(message_type: NotificationType, values: dict) ->
logger.error(f"Error enqueuing notification: {e}")
def enqueue_user_notification(
message_type: NotificationType, values: dict, user_notifications: UserNotifications
) -> None:
try:
notifications_queue.put_nowait(
NotificationMessage(
message_type=message_type,
values=values,
user_notifications=user_notifications,
)
)
except Exception as e:
logger.error(f"Error enqueuing notification: {e}")
async def process_next_notification() -> None:
notification_message = await notifications_queue.get()
message_type, text = _notification_message_to_text(notification_message)
user_notifications = notification_message.user_notifications
if user_notifications:
await send_user_notification(user_notifications, text, message_type)
else:
await send_admin_notification(text, message_type)
async def send_admin_notification(
message: str,
message_type: Optional[str] = None,
) -> None:
return await send_notification(
settings.lnbits_telegram_notifications_chat_id,
settings.lnbits_nostr_notifications_identifiers,
settings.lnbits_email_notifications_to_emails,
message,
message_type,
)
async def send_user_notification(
user_notifications: UserNotifications,
message: str,
message_type: Optional[str] = None,
) -> None:
email_address = (
[user_notifications.email_address] if user_notifications.email_address else []
)
nostr_identifiers = (
[user_notifications.nostr_identifier]
if user_notifications.nostr_identifier
else []
)
return await send_notification(
user_notifications.telegram_chat_id,
nostr_identifiers,
email_address,
message,
message_type,
)
await send_notification(text, message_type)
async def send_notification(
telegram_chat_id: str | None,
nostr_identifiers: list[str] | None,
email_addresses: list[str] | None,
message: str,
message_type: Optional[str] = None,
) -> None:
try:
if telegram_chat_id and settings.is_telegram_notifications_configured():
await send_telegram_notification(telegram_chat_id, message)
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 nostr_identifiers and settings.is_nostr_notifications_configured():
await send_nostr_notifications(nostr_identifiers, message)
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}")
try:
if email_addresses and settings.lnbits_email_notifications_enabled:
await send_email_notification(email_addresses, message)
if settings.lnbits_email_notifications_enabled:
await send_email_notification(message)
logger.debug(f"Sent email notification: {message_type}")
except Exception as e:
logger.error(f"Error sending email notification {message_type}: {e}")
async def send_nostr_notifications(identifiers: list[str], message: str) -> list[str]:
success_sent: list[str] = []
for identifier in identifiers:
async def send_nostr_notification(message: str) -> dict:
for i in settings.lnbits_nostr_notifications_identifiers:
try:
await send_nostr_notification(identifier, message)
success_sent.append(identifier)
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 {identifier}: {e}")
return success_sent
logger.warning(f"Error notifying identifier {i}: {e}")
return {"status": "ok"}
async def send_nostr_notification(identifier: str, message: str):
nip5 = await fetch_nip5_details(identifier)
user_pubkey = nip5[0]
relays = nip5[1]
server_private_key = normalize_private_key(
settings.lnbits_nostr_notifications_private_key
)
await send_nostr_dm(
server_private_key,
user_pubkey,
message,
relays,
)
async def send_telegram_notification(chat_id: str, message: str) -> dict:
async def send_telegram_notification(message: str) -> dict:
return await send_telegram_message(
settings.lnbits_telegram_notifications_access_token,
chat_id,
settings.lnbits_telegram_notifications_chat_id,
message,
)
@@ -177,7 +112,7 @@ async def send_telegram_message(token: str, chat_id: str, message: str) -> dict:
async def send_email_notification(
to_emails: list[str], message: str, subject: str = "LNbits Notification"
message: str, subject: str = "LNbits Notification"
) -> dict:
if not settings.lnbits_email_notifications_enabled:
return {"status": "error", "message": "Email notifications are disabled"}
@@ -188,7 +123,7 @@ async def send_email_notification(
settings.lnbits_email_notifications_username,
settings.lnbits_email_notifications_password,
settings.lnbits_email_notifications_email,
to_emails,
settings.lnbits_email_notifications_to_emails,
subject,
message,
)
@@ -228,6 +163,41 @@ async def send_email(
return True
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
async def dispatch_webhook(payment: Payment):
"""
Dispatches the webhook to the webhook url.
@@ -261,7 +231,7 @@ async def send_payment_notification(wallet: Wallet, payment: Payment):
except Exception as e:
logger.error(f"Error sending websocket payment notification {e!s}")
try:
await send_chat_payment_notification(wallet, payment)
send_chat_payment_notification(wallet, payment)
except Exception as e:
logger.error(f"Error sending chat payment notification {e!s}")
try:
@@ -298,7 +268,7 @@ async def send_ws_payment_notification(wallet: Wallet, payment: Payment):
)
async def send_chat_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,
@@ -313,23 +283,10 @@ async def send_chat_payment_notification(wallet: Wallet, payment: Payment):
if payment.is_out:
if amount_sats >= settings.lnbits_notification_outgoing_payment_amount_sats:
enqueue_admin_notification(NotificationType.outgoing_payment, values)
elif amount_sats >= settings.lnbits_notification_incoming_payment_amount_sats:
enqueue_admin_notification(NotificationType.incoming_payment, values)
user = await get_user(wallet.user)
user_notifications = user.extra.notifications if user else None
if user_notifications and wallet.id not in user_notifications.excluded_wallets:
out_limit = user_notifications.outgoing_payments_sats
in_limit = user_notifications.incoming_payments_sats
if payment.is_out and (amount_sats >= out_limit):
enqueue_user_notification(
NotificationType.outgoing_payment, values, user_notifications
)
elif amount_sats >= in_limit:
enqueue_user_notification(
NotificationType.incoming_payment, values, user_notifications
)
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 send_payment_push_notification(wallet: Wallet, payment: Payment):
@@ -373,38 +330,3 @@ async def send_push_notification(subscription, title, body, url=""):
f"failed sending push notification: "
f"{e.response.text if e.response else e}"
)
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
+50 -106
View File
@@ -1,13 +1,13 @@
import asyncio
import json
import time
from datetime import datetime, timedelta, timezone
from typing import Optional
import httpx
from bolt11 import Bolt11, MilliSatoshi, Tags
from bolt11 import decode as bolt11_decode
from bolt11 import encode as bolt11_encode
from lnurl import LnurlErrorResponse, LnurlSuccessResponse
from lnurl import execute_withdraw as lnurl_withdraw
from loguru import logger
from lnbits.core.crud.payments import get_daily_stats
@@ -16,16 +16,15 @@ from lnbits.core.models import PaymentDailyStats, PaymentFilters
from lnbits.core.models.payments import CreateInvoice
from lnbits.db import Connection, Filters
from lnbits.decorators import check_user_extension_access
from lnbits.exceptions import InvoiceError, PaymentError, UnsupportedError
from lnbits.exceptions import InvoiceError, PaymentError
from lnbits.fiat import get_fiat_provider
from lnbits.helpers import check_callback_url
from lnbits.settings import settings
from lnbits.tasks import create_task, internal_invoice_queue_put
from lnbits.utils.crypto import fake_privkey, random_secret_and_hash, verify_preimage
from lnbits.utils.crypto import fake_privkey, random_secret_and_hash
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis, satoshis_amount_as_fiat
from lnbits.wallets import fake_wallet, get_funding_source
from lnbits.wallets.base import (
InvoiceResponse,
PaymentPendingStatus,
PaymentResponse,
PaymentStatus,
@@ -91,8 +90,12 @@ async def pay_invoice(
payment = await _pay_invoice(wallet.id, create_payment_model, conn)
service_fee_memo = f"""
Service fee for payment of {abs(payment.sat)} sats.
Wallet: '{wallet.name}' ({wallet.id})."""
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
await _credit_service_fee_wallet(wallet, payment, new_conn)
await _credit_service_fee_wallet(payment, service_fee_memo, new_conn)
return payment
@@ -132,9 +135,6 @@ async def create_fiat_invoice(
internal_payment = await create_wallet_invoice(wallet_id, invoice_data)
fiat_provider = await get_fiat_provider(fiat_provider_name)
if not fiat_provider:
raise InvoiceError("No fiat provider found.", status="failed")
fiat_invoice = await fiat_provider.create_invoice(
amount=invoice_data.amount,
payment_hash=internal_payment.payment_hash,
@@ -168,8 +168,8 @@ async def create_fiat_invoice(
async def create_wallet_invoice(wallet_id: str, data: CreateInvoice) -> Payment:
description_hash = None
unhashed_description = None
description_hash = b""
unhashed_description = b""
memo = data.memo or settings.lnbits_site_title
if data.description_hash or data.unhashed_description:
if data.description_hash:
@@ -200,29 +200,30 @@ async def create_wallet_invoice(wallet_id: str, data: CreateInvoice) -> Payment:
extra=data.extra,
webhook=data.webhook,
internal=data.internal,
payment_hash=data.payment_hash,
)
if data.lnurl_withdraw:
try:
check_callback_url(data.lnurl_withdraw.callback)
res = await lnurl_withdraw(
data.lnurl_withdraw,
payment.bolt11,
user_agent=settings.user_agent,
timeout=10,
)
if isinstance(res, LnurlErrorResponse):
payment.extra["lnurl_response"] = res.reason
payment.status = "failed"
elif isinstance(res, LnurlSuccessResponse):
payment.extra["lnurl_response"] = True
payment.status = "success"
except Exception as exc:
payment.extra["lnurl_response"] = str(exc)
payment.status = "failed"
# updating to payment here would run into a race condition
# with the payment listeners and they will overwrite each other
# lnurl_response is not saved in the database
if data.lnurl_callback:
headers = {"User-Agent": settings.user_agent}
async with httpx.AsyncClient(headers=headers) as client:
try:
check_callback_url(data.lnurl_callback)
r = await client.get(
data.lnurl_callback,
params={"pr": payment.bolt11},
timeout=10,
)
if r.is_error:
payment.extra["lnurl_response"] = r.text
else:
resp = json.loads(r.text)
if resp["status"] != "OK":
payment.extra["lnurl_response"] = resp["reason"]
else:
payment.extra["lnurl_response"] = True
except (httpx.ConnectError, httpx.RequestError) as ex:
logger.error(ex)
payment.extra["lnurl_response"] = False
return payment
@@ -239,7 +240,6 @@ async def create_invoice(
extra: Optional[dict] = None,
webhook: Optional[str] = None,
internal: Optional[bool] = False,
payment_hash: str | None = None,
conn: Optional[Connection] = None,
) -> Payment:
if not amount > 0:
@@ -273,54 +273,39 @@ async def create_invoice(
status="failed",
)
if payment_hash:
try:
invoice_response = await funding_source.create_hold_invoice(
amount=amount_sat,
memo=invoice_memo,
payment_hash=payment_hash,
description_hash=description_hash,
)
extra["hold_invoice"] = True
except UnsupportedError as exc:
raise InvoiceError(
"Hold invoices are not supported by the funding source.",
status="failed",
) from exc
else:
invoice_response = await funding_source.create_invoice(
amount=amount_sat,
memo=invoice_memo,
description_hash=description_hash,
unhashed_description=unhashed_description,
expiry=expiry or settings.lightning_invoice_expiry,
)
payment_response = await funding_source.create_invoice(
amount=amount_sat,
memo=invoice_memo,
description_hash=description_hash,
unhashed_description=unhashed_description,
expiry=expiry or settings.lightning_invoice_expiry,
)
if (
not invoice_response.ok
or not invoice_response.payment_request
or not invoice_response.checking_id
not payment_response.ok
or not payment_response.payment_request
or not payment_response.checking_id
):
raise InvoiceError(
message=invoice_response.error_message or "unexpected backend error.",
message=payment_response.error_message or "unexpected backend error.",
status="pending",
)
invoice = bolt11_decode(invoice_response.payment_request)
invoice = bolt11_decode(payment_response.payment_request)
create_payment_model = CreatePayment(
wallet_id=wallet_id,
bolt11=invoice_response.payment_request,
bolt11=payment_response.payment_request,
payment_hash=invoice.payment_hash,
preimage=invoice_response.preimage,
preimage=payment_response.preimage,
amount_msat=amount_sat * 1000,
expiry=invoice.expiry_date,
memo=memo,
extra=extra,
webhook=webhook,
fee=invoice_response.fee_msat or 0,
fee=payment_response.fee_msat or 0,
)
payment = await create_payment(
checking_id=invoice_response.checking_id,
checking_id=payment_response.checking_id,
data=create_payment_model,
conn=conn,
)
@@ -901,16 +886,12 @@ def _validate_payment_request(
async def _credit_service_fee_wallet(
wallet: Wallet, payment: Payment, conn: Optional[Connection] = None
payment: Payment, memo: str, conn: Optional[Connection] = None
):
service_fee_msat = service_fee(payment.amount, internal=payment.is_internal)
if not settings.lnbits_service_fee_wallet or not service_fee_msat:
return
memo = f"""
Service fee for payment of {abs(payment.sat)} sats.
Wallet: '{wallet.name}' ({wallet.id})."""
create_payment_model = CreatePayment(
wallet_id=settings.lnbits_service_fee_wallet,
bolt11=payment.bolt11,
@@ -968,40 +949,3 @@ async def _check_fiat_invoice_limits(
f"The amount exceeds the '{fiat_provider_name}'"
"faucet wallet balance.",
)
async def settle_hold_invoice(payment: Payment, preimage: str) -> InvoiceResponse:
if verify_preimage(preimage, payment.payment_hash) is False:
raise InvoiceError("Invalid preimage.", status="failed")
funding_source = get_funding_source()
response = await funding_source.settle_hold_invoice(preimage=preimage)
if not response.ok:
raise InvoiceError(
response.error_message or "Unexpected backend error.", status="failed"
)
payment.preimage = preimage
payment.extra["hold_invoice_settled"] = True
await update_payment(payment)
return response
async def cancel_hold_invoice(payment: Payment) -> InvoiceResponse:
funding_source = get_funding_source()
response = await funding_source.cancel_hold_invoice(
payment_hash=payment.payment_hash
)
if not response.ok:
raise InvoiceError(
response.error_message or "Unexpected backend error.", status="failed"
)
payment.status = PaymentState.FAILED
payment.extra["hold_invoice_cancelled"] = True
await update_payment(payment)
return response
+3 -3
View File
@@ -22,7 +22,7 @@ from lnbits.core.services.funding_source import (
get_balance_delta,
)
from lnbits.core.services.notifications import (
enqueue_admin_notification,
enqueue_notification,
process_next_notification,
send_payment_notification,
)
@@ -87,7 +87,7 @@ async def _notify_server_status() -> None:
"lnbits_balance_sats": status.lnbits_balance_sats,
"node_balance_sats": status.node_balance_sats,
}
enqueue_admin_notification(NotificationType.server_status, values)
enqueue_notification(NotificationType.server_status, values)
async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue) -> None:
@@ -123,7 +123,7 @@ async def wait_notification_messages() -> None:
try:
await process_next_notification()
except Exception as ex:
logger.warning("Payment notification error", ex)
logger.log("error", ex)
await asyncio.sleep(3)
+44 -156
View File
@@ -7,71 +7,38 @@
<div class="row q-col-gutter-md q-mb-md">
<div class="col-12">
<q-card>
<div class="q-pa-sm">
<div class="row items-center justify-between q-gutter-xs">
<div class="col">
<q-btn @click="updateAccount" unelevated color="primary">
<span v-text="$t('update_account')"></span>
</q-btn>
</div>
<div>
<div class="q-gutter-y-md">
<q-tabs v-model="tab" align="left">
<q-tab
name="user"
:label="$t('account_settings')"
@update="val => tab = val.name"
></q-tab>
<q-tab
name="theme"
:label="$t('look_and_feel')"
@update="val => tab = val.name"
></q-tab>
<q-tab
name="api_acls"
:label="$t('access_control_list')"
@update="val => tab = val.name"
></q-tab>
</q-tabs>
</div>
</div>
</q-card>
</div>
</div>
<div class="row q-col-gutter-md q-mb-md">
<div class="col-12">
<div class="row q-col-gutter-md">
<div v-if="user" class="col-md-12 col-lg-6 q-gutter-y-md">
<q-card>
<q-splitter>
<template v-slot:before>
<q-tabs v-model="tab" vertical active-color="primary">
<q-tab
name="user"
icon="person"
:label="$q.screen.gt.sm ? $t('account_settings') : ''"
@update="val => tab = val.name"
>
<q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('account_settings')"></span
></q-tooltip>
</q-tab>
<q-tab
name="notifications"
icon="notifications"
:label="$q.screen.gt.sm ? $t('notifications') : ''"
@update="val => tab = val.name"
>
<q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('notifications')"></span
></q-tooltip>
</q-tab>
<q-tab
name="theme"
icon="palette"
:label="$q.screen.gt.sm ? $t('look_and_feel') : ''"
@update="val => tab = val.name"
>
<q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('look_and_feel')"></span
></q-tooltip>
</q-tab>
<q-tab
name="api_acls"
icon="lock"
:label="$q.screen.gt.sm ? $t('access_control_list') : ''"
@update="val => tab = val.name"
>
<q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('access_control_list')"></span
></q-tooltip>
</q-tab>
</q-tabs>
</template>
<template v-slot:after>
<q-scroll-area style="height: 80vh">
<q-tab-panels v-if="user" v-model="tab">
<q-card-section>
<div class="row">
<div class="col">
<q-tab-panels v-model="tab">
<q-tab-panel name="user">
<div v-if="credentialsData.show">
<q-card-section>
@@ -326,6 +293,9 @@
</q-card-section>
<q-separator></q-separator>
<q-card-section>
<q-btn @click="updateAccount" unelevated color="primary">
<span v-text="$t('update_account')"></span>
</q-btn>
<q-btn
@click="showUpdateCredentials()"
:label="$t('change_password')"
@@ -589,101 +559,12 @@
</q-select>
</div>
</div>
</q-tab-panel>
<q-tab-panel name="notifications">
<q-card-section>
<div class="row q-mb-md">
<div class="col-4">
<span
v-text="$t('notifications_nostr_identifier')"
></span>
{%if not nostr_configured%}
<br />
<q-badge v-text="$t('not_connected')"></q-badge>
{%endif%}
</div>
<div class="col-8">
<q-input
filled
dense
v-model="user.extra.notifications.nostr_identifier"
:hint="$t('notifications_nostr_identifier_desc')"
>
</q-input>
</div>
</div>
<div class="row q-mb-md">
<div class="col-4">
<span v-text="$t('notifications_chat_id')"></span>
{%if not telegram_configured%}
<br />
<q-badge v-text="$t('not_connected')"></q-badge>
{%endif%}
</div>
<div class="col-8">
<q-input
filled
dense
v-model="user.extra.notifications.telegram_chat_id"
:hint="$t('notifications_chat_id_desc')"
/>
</div>
</div>
<q-separator class="q-mb-md"></q-separator>
<div class="row q-mb-md">
<div class="col-4">
<span v-text="$t('notification_outgoing_payment')"></span>
</div>
<div class="col-8">
<q-input
filled
dense
type="number"
min="0"
step="1"
v-model="user.extra.notifications.outgoing_payments_sats"
:hint="$t('notification_outgoing_payment_desc')"
/>
</div>
</div>
<div class="row q-mb-md">
<div class="col-4">
<span v-text="$t('notification_incoming_payment')"></span>
</div>
<div class="col-8">
<q-input
filled
dense
type="number"
min="0"
step="1"
v-model="user.extra.notifications.incoming_payments_sats"
:hint="$t('notification_incoming_payment_desc')"
/>
</div>
</div>
<div class="row q-mb-md">
<div class="col-4">
<span v-text="$t('exclude_wallets')"></span>
</div>
<div class="col-8">
<q-select
filled
dense
emit-value
map-options
multiple
v-model="user.extra.notifications.excluded_wallets"
:options="g.user.walletOptions"
:label="$t('exclude_wallets')"
:hint="$t('notifications_excluded_wallets_desc')"
class="q-mt-sm"
>
</q-select>
</div>
</div>
</q-card-section>
<q-separator></q-separator>
<div class="row q-mb-md q-mt-md">
<q-btn @click="updateAccount" unelevated color="primary">
<span v-text="$t('update_account')"></span>
</q-btn>
</div>
</q-tab-panel>
<q-tab-panel name="api_acls">
<div class="row q-mb-md">
@@ -910,9 +791,16 @@
</q-card-section>
</q-tab-panel>
</q-tab-panels>
</q-scroll-area>
</template>
</q-splitter>
</div>
</div>
</q-card-section>
</q-card>
</div>
<div v-else class="col-12 col-md-6 q-gutter-y-md">
<q-card>
<q-card-section>
<h4 class="q-my-none"><span v-text="$t('account')"></span></h4>
</q-card-section>
</q-card>
</div>
</div>
+1 -7
View File
@@ -661,13 +661,7 @@
:readonly="receive.lnurl && receive.lnurl.fixed"
></q-input>
{% endif %}
<q-input
v-if="has_holdinvoice"
filled
dense
v-model="receive.data.payment_hash"
:label="$t('hold_invoice_payment_hash')"
></q-input>
<q-input
filled
dense
+4 -10
View File
@@ -16,7 +16,7 @@ from lnbits.core.models import User
from lnbits.core.models.misc import Image, SimpleStatus
from lnbits.core.models.notifications import NotificationType
from lnbits.core.services import (
enqueue_admin_notification,
enqueue_notification,
get_balance_delta,
update_cached_settings,
)
@@ -65,9 +65,7 @@ async def api_monitor():
)
async def api_test_email():
return await send_email_notification(
settings.lnbits_email_notifications_to_emails,
"This is a LNbits test email.",
"LNbits Test Email",
"This is a LNbits test email.", "LNbits Test Email"
)
@@ -84,9 +82,7 @@ async def api_get_settings(
status_code=HTTPStatus.OK,
)
async def api_update_settings(data: UpdateSettings, user: User = Depends(check_admin)):
enqueue_admin_notification(
NotificationType.settings_update, {"username": user.username}
)
enqueue_notification(NotificationType.settings_update, {"username": user.username})
await update_admin_settings(data)
admin_settings = await get_admin_settings(user.super_user)
if not admin_settings:
@@ -117,9 +113,7 @@ async def api_reset_settings(field_name: str):
@admin_router.delete("/api/v1/settings", status_code=HTTPStatus.OK)
async def api_delete_settings(user: User = Depends(check_super_user)) -> None:
enqueue_admin_notification(
NotificationType.settings_update, {"username": user.username}
)
enqueue_notification(NotificationType.settings_update, {"username": user.username})
await reset_core_settings()
server_restart.set()
+157 -3
View File
@@ -1,20 +1,36 @@
import hashlib
import json
from http import HTTPStatus
from io import BytesIO
from time import time
from typing import Any
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
import httpx
import pyqrcode
from fastapi import APIRouter, Depends
from fastapi import (
APIRouter,
Depends,
)
from fastapi.exceptions import HTTPException
from fastapi.responses import StreamingResponse
from lnbits.core.models import (
BaseWallet,
ConversionData,
CreateLnurlAuth,
CreateWallet,
User,
Wallet,
)
from lnbits.decorators import check_user_exists
from lnbits.decorators import (
WalletTypeInfo,
check_user_exists,
require_admin_key,
require_invoice_key,
)
from lnbits.helpers import check_callback_url
from lnbits.lnurl import decode as lnurl_decode
from lnbits.settings import settings
from lnbits.utils.exchange_rates import (
allowed_currencies,
@@ -25,7 +41,7 @@ from lnbits.utils.exchange_rates import (
from lnbits.wallets import get_funding_source
from lnbits.wallets.base import StatusResponse
from ..services import create_user_account
from ..services import create_user_account, perform_lnurlauth
api_router = APIRouter(tags=["Core"])
@@ -76,6 +92,144 @@ async def api_create_account(data: CreateWallet) -> Wallet:
return user.wallets[0]
@api_router.get("/api/v1/lnurlscan/{code}")
async def api_lnurlscan( # noqa: C901
code: str, wallet: WalletTypeInfo = Depends(require_invoice_key)
):
try:
url = str(lnurl_decode(code))
domain = urlparse(url).netloc
except Exception as exc:
# parse internet identifier (user@domain.com)
name_domain = code.split("@")
if len(name_domain) == 2 and len(name_domain[1].split(".")) >= 2:
name, domain = name_domain
url = (
("http://" if domain.endswith(".onion") else "https://")
+ domain
+ "/.well-known/lnurlp/"
+ name
)
# will proceed with these values
else:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="invalid lnurl"
) from exc
# params is what will be returned to the client
params: dict = {"domain": domain}
if "tag=login" in url:
params.update(kind="auth")
params.update(callback=url) # with k1 already in it
lnurlauth_key = wallet.wallet.lnurlauth_key(domain)
if not lnurlauth_key.verifying_key:
raise ValueError("LNURL auth key not found for this domain.")
params.update(pubkey=lnurlauth_key.verifying_key.to_string("compressed").hex())
else:
headers = {"User-Agent": settings.user_agent}
async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client:
check_callback_url(url)
try:
r = await client.get(url, timeout=5)
r.raise_for_status()
except httpx.HTTPStatusError as exc:
if exc.response.status_code == 404:
raise HTTPException(HTTPStatus.NOT_FOUND, "Not found") from exc
raise HTTPException(
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
detail={
"domain": domain,
"message": "failed to get parameters",
},
) from exc
try:
data = json.loads(r.text)
except json.decoder.JSONDecodeError as exc:
raise HTTPException(
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
detail={
"domain": domain,
"message": f"got invalid response '{r.text[:200]}'",
},
) from exc
try:
tag: str = data.get("tag")
params.update(**data)
if tag == "channelRequest":
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail={
"domain": domain,
"kind": "channel",
"message": "unsupported",
},
)
elif tag == "withdrawRequest":
params.update(kind="withdraw")
params.update(fixed=data["minWithdrawable"] == data["maxWithdrawable"])
# callback with k1 already in it
parsed_callback: ParseResult = urlparse(data["callback"])
qs: dict = parse_qs(parsed_callback.query)
qs["k1"] = data["k1"]
# balanceCheck/balanceNotify
if "balanceCheck" in data:
params.update(balanceCheck=data["balanceCheck"])
# format callback url and send to client
parsed_callback = parsed_callback._replace(
query=urlencode(qs, doseq=True)
)
params.update(callback=urlunparse(parsed_callback))
elif tag == "payRequest":
params.update(kind="pay")
params.update(fixed=data["minSendable"] == data["maxSendable"])
params.update(
description_hash=hashlib.sha256(
data["metadata"].encode()
).hexdigest()
)
metadata = json.loads(data["metadata"])
for [k, v] in metadata:
if k == "text/plain":
params.update(description=v)
if k in ("image/jpeg;base64", "image/png;base64"):
data_uri = f"data:{k},{v}"
params.update(image=data_uri)
if k in ("text/email", "text/identifier"):
params.update(targetUser=v)
params.update(commentAllowed=data.get("commentAllowed", 0))
except KeyError as exc:
raise HTTPException(
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
detail={
"domain": domain,
"message": f"lnurl JSON response invalid: {exc}",
},
) from exc
return params
@api_router.post("/api/v1/lnurlauth")
async def api_perform_lnurlauth(
data: CreateLnurlAuth, wallet: WalletTypeInfo = Depends(require_admin_key)
):
err = await perform_lnurlauth(data.callback, wallet=wallet)
if err:
raise HTTPException(
status_code=HTTPStatus.SERVICE_UNAVAILABLE, detail=err.reason
)
return ""
@api_router.get(
"/api/v1/rate/history",
dependencies=[Depends(check_user_exists)],
+4 -4
View File
@@ -206,15 +206,11 @@ async def account(
request: Request,
user: User = Depends(check_user_exists),
):
nostr_configured = settings.is_nostr_notifications_configured()
telegram_configured = settings.is_telegram_notifications_configured()
return template_renderer().TemplateResponse(
request,
"core/account.html",
{
"user": user.json(),
"nostr_configured": nostr_configured,
"telegram_configured": telegram_configured,
"ajax": _is_ajax_request(request),
},
)
@@ -345,6 +341,7 @@ async def node(request: Request, user: User = Depends(check_admin)):
"node/index.html",
{
"user": user.json(),
"settings": settings.dict(),
"balance": balance,
"wallets": user.wallets[0].json(),
"ajax": _is_ajax_request(request),
@@ -364,6 +361,7 @@ async def node_public(request: Request):
request,
"node/public.html",
{
"settings": settings.dict(),
"balance": balance,
},
)
@@ -382,6 +380,7 @@ async def admin_index(request: Request, user: User = Depends(check_admin)):
"admin/index.html",
{
"user": user.json(),
"settings": settings.dict(),
"balance": balance,
"currencies": list(currencies.keys()),
"ajax": _is_ajax_request(request),
@@ -399,6 +398,7 @@ async def users_index(request: Request, user: User = Depends(check_admin)):
{
"request": request,
"user": user.json(),
"settings": settings.dict(),
"currencies": list(currencies.keys()),
"ajax": _is_ajax_request(request),
},
-143
View File
@@ -1,143 +0,0 @@
from http import HTTPStatus
from typing import Any
from fastapi import (
APIRouter,
Depends,
HTTPException,
)
from lnurl import (
LnurlResponseException,
LnurlSuccessResponse,
)
from lnurl import execute_login as lnurlauth
from lnurl import execute_withdraw as lnurl_withdraw
from lnurl import handle as lnurl_handle
from lnurl.models import (
LnurlAuthResponse,
LnurlErrorResponse,
LnurlPayResponse,
LnurlResponseModel,
LnurlWithdrawResponse,
)
from loguru import logger
from lnbits.core.models import CreateLnurlWithdraw, Payment
from lnbits.core.models.lnurl import CreateLnurlPayment
from lnbits.decorators import (
WalletTypeInfo,
require_admin_key,
require_invoice_key,
)
from lnbits.helpers import check_callback_url
from lnbits.settings import settings
from ..services import fetch_lnurl_pay_request, pay_invoice
lnurl_router = APIRouter(tags=["LNURL"])
@lnurl_router.get(
"/api/v1/lnurlscan/{code}",
dependencies=[Depends(require_invoice_key)],
response_model=LnurlPayResponse
| LnurlWithdrawResponse
| LnurlAuthResponse
| LnurlErrorResponse,
)
async def api_lnurlscan(code: str) -> LnurlResponseModel:
try:
res = await lnurl_handle(code, user_agent=settings.user_agent, timeout=5)
except LnurlResponseException as exc:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail=str(exc)
) from exc
if isinstance(res, (LnurlPayResponse, LnurlWithdrawResponse, LnurlAuthResponse)):
check_callback_url(res.callback)
return res
@lnurl_router.post("/api/v1/lnurlauth")
async def api_perform_lnurlauth(
data: LnurlAuthResponse, key_type: WalletTypeInfo = Depends(require_admin_key)
) -> LnurlResponseModel:
check_callback_url(data.callback)
try:
res = await lnurlauth(
res=data,
seed=key_type.wallet.adminkey,
user_agent=settings.user_agent,
timeout=5,
)
return res
except LnurlResponseException as exc:
logger.warning(exc)
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail=str(exc)
) from exc
@lnurl_router.post("/api/v1/payments/lnurl")
async def api_payments_pay_lnurl(
data: CreateLnurlPayment, wallet: WalletTypeInfo = Depends(require_admin_key)
) -> Payment:
try:
res = await fetch_lnurl_pay_request(data=data)
except LnurlResponseException as exc:
logger.warning(exc)
msg = f"Failed to connect to {data.res.callback}."
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=msg) from exc
extra: dict[str, Any] = {}
if res.success_action:
extra["success_action"] = res.success_action.json()
if data.comment:
extra["comment"] = data.comment
if data.unit and data.unit != "sat":
extra["fiat_currency"] = data.unit
extra["fiat_amount"] = data.amount / 1000
payment = await pay_invoice(
wallet_id=wallet.wallet.id,
payment_request=str(res.pr),
description=data.res.metadata.text,
extra=extra,
)
return payment
@lnurl_router.post(
"/api/v1/payments/{payment_request}/pay-with-nfc", status_code=HTTPStatus.OK
)
async def api_payment_pay_with_nfc(
payment_request: str,
lnurl_data: CreateLnurlWithdraw,
) -> LnurlErrorResponse | LnurlSuccessResponse:
try:
res = await lnurl_handle(
lnurl_data.lnurl_w.callback_url, user_agent=settings.user_agent, timeout=10
)
except (LnurlResponseException, Exception) as exc:
logger.warning(exc)
return LnurlErrorResponse(reason=str(exc))
if not isinstance(res, LnurlWithdrawResponse):
return LnurlErrorResponse(reason="Invalid LNURL-withdraw response.")
try:
check_callback_url(res.callback)
except ValueError as exc:
return LnurlErrorResponse(reason=f"Invalid callback URL: {exc!s}")
try:
res2 = await lnurl_withdraw(
res, payment_request, user_agent=settings.user_agent, timeout=10
)
except (LnurlResponseException, Exception) as exc:
logger.warning(exc)
return LnurlErrorResponse(reason=str(exc))
if not isinstance(res2, (LnurlSuccessResponse, LnurlErrorResponse)):
return LnurlErrorResponse(reason="Invalid LNURL-withdraw response.")
return res2
+5 -10
View File
@@ -7,9 +7,8 @@ from pydantic import BaseModel
from starlette.status import HTTP_503_SERVICE_UNAVAILABLE
from lnbits.decorators import check_admin, check_super_user, parse_filters
from lnbits.nodes import get_node_class
from lnbits.settings import settings
from lnbits.wallets import get_funding_source
from lnbits.wallets.base import Feature
from ...db import Filters, Page
from ...nodes.base import (
@@ -27,13 +26,9 @@ from ...nodes.base import (
from ...utils.cache import cache
def require_node() -> Node:
funding_source = get_funding_source()
if (
not funding_source.features
or Feature.nodemanager not in funding_source.features
or not funding_source.__node_cls__
):
def require_node():
node_class = get_node_class()
if not node_class:
raise HTTPException(
status_code=HTTPStatus.NOT_IMPLEMENTED,
detail="Active backend does not implement Node API",
@@ -43,7 +38,7 @@ def require_node() -> Node:
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
detail="Not enabled",
)
return funding_source.__node_cls__(funding_source)
return node_class
def check_public():
+183 -34
View File
@@ -1,7 +1,11 @@
from hashlib import sha256
import json
import ssl
from http import HTTPStatus
from math import ceil
from typing import Optional
from urllib.parse import urlparse
import httpx
from fastapi import (
APIRouter,
Depends,
@@ -10,7 +14,7 @@ from fastapi import (
Query,
)
from fastapi.responses import JSONResponse
from lnurl import decode as lnurl_decode
from loguru import logger
from lnbits import bolt11
from lnbits.core.crud.payments import (
@@ -18,10 +22,11 @@ from lnbits.core.crud.payments import (
get_wallets_stats,
)
from lnbits.core.models import (
CancelInvoice,
CreateInvoice,
CreateLnurl,
DecodePayment,
KeyType,
PayLnurlWData,
Payment,
PaymentCountField,
PaymentCountStat,
@@ -29,7 +34,6 @@ from lnbits.core.models import (
PaymentFilters,
PaymentHistoryPoint,
PaymentWalletStats,
SettleInvoice,
)
from lnbits.core.models.users import User
from lnbits.db import Filters, Page
@@ -41,10 +45,13 @@ from lnbits.decorators import (
require_invoice_key,
)
from lnbits.helpers import (
check_callback_url,
filter_dict_keys,
generate_filter_params_openapi,
)
from lnbits.wallets.base import InvoiceResponse
from lnbits.lnurl import decode as lnurl_decode
from lnbits.settings import settings
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
from ..crud import (
DateTrunc,
@@ -53,14 +60,13 @@ from ..crud import (
get_payments_paginated,
get_standalone_payment,
get_wallet_for_key,
update_payment_extra,
)
from ..services import (
cancel_hold_invoice,
create_payment_request,
fee_reserve_total,
get_payments_daily_stats,
pay_invoice,
settle_hold_invoice,
update_pending_payment,
update_pending_payments,
)
@@ -258,6 +264,38 @@ async def api_payments_create(
return await create_payment_request(wallet_id, invoice_data)
@payment_router.patch(
"/extra/{payment_hash}", description="Update extra data for a payment"
)
async def api_payments_update_extra(
payment_hash: str,
extra: dict,
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> Payment:
payment = await get_standalone_payment(payment_hash, wallet_id=wallet.wallet.id)
if payment is None:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Payment does not exist."
)
if not isinstance(extra, dict):
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="Extra data must be a dictionary.",
)
if payment.extra is None:
payment.extra = {}
for key, value in extra.items():
if key in payment.extra:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"Key '{key}' already exists in extra data.",
)
payment.extra[key] = value
return await update_payment_extra(payment)
@payment_router.get("/fee-reserve")
async def api_payments_fee_reserve(invoice: str = Query("invoice")) -> JSONResponse:
invoice_obj = bolt11.decode(invoice)
@@ -273,6 +311,92 @@ async def api_payments_fee_reserve(invoice: str = Query("invoice")) -> JSONRespo
)
def _validate_lnurl_response(
params: dict, domain: str, amount_msat: int
) -> bolt11.Invoice:
if params.get("status") == "ERROR":
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"{domain} said: '{params.get('reason', '')}'",
)
if not params.get("pr"):
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"{domain} did not return a payment request.",
)
invoice = bolt11.decode(params["pr"])
if invoice.amount_msat != amount_msat:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=(
f"{domain} returned an invalid invoice. Expected"
f" {amount_msat} msat, got {invoice.amount_msat}."
),
)
return invoice
async def _fetch_lnurl_params(data: CreateLnurl, amount_msat: int, domain: str) -> dict:
headers = {"User-Agent": settings.user_agent}
async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client:
try:
r: httpx.Response = await client.get(
data.callback,
params={"amount": amount_msat, "comment": data.comment},
timeout=40,
)
if r.is_error:
raise httpx.ConnectError("LNURL callback connection error")
r.raise_for_status()
except (httpx.HTTPError, ssl.SSLError) as exc:
logger.warning(exc)
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"Failed to connect to {domain}.",
) from exc
return json.loads(r.text)
@payment_router.post("/lnurl")
async def api_payments_pay_lnurl(
data: CreateLnurl, wallet: WalletTypeInfo = Depends(require_admin_key)
) -> Payment:
domain = urlparse(data.callback).netloc
check_callback_url(data.callback)
if data.unit and data.unit != "sat":
amount_msat = await fiat_amount_as_satoshis(data.amount, data.unit)
amount_msat = ceil(amount_msat // 1000) * 1000
else:
amount_msat = data.amount
params = await _fetch_lnurl_params(data, amount_msat, domain)
_validate_lnurl_response(params, domain, amount_msat)
extra = {}
if params.get("successAction"):
extra["success_action"] = params["successAction"]
if data.comment:
extra["comment"] = data.comment
if data.unit and data.unit != "sat":
extra["fiat_currency"] = data.unit
extra["fiat_amount"] = data.amount / 1000
if data.internal_memo is not None:
if len(data.internal_memo) > 512:
raise ValueError("Internal memo must be 512 characters or less.")
extra["internal_memo"] = data.internal_memo
if data.description is None:
raise ValueError("Description is required")
payment = await pay_invoice(
wallet_id=wallet.wallet.id,
payment_request=params["pr"],
description=data.description,
extra=extra,
)
return payment
# TODO: refactor this route into a public and admin one
@payment_router.get("/{payment_hash}")
async def api_payment(payment_hash, x_api_key: Optional[str] = Header(None)):
@@ -331,32 +455,57 @@ async def api_payments_decode(data: DecodePayment) -> JSONResponse:
)
@payment_router.post("/settle")
async def api_payments_settle(
data: SettleInvoice, key_type: WalletTypeInfo = Depends(require_admin_key)
) -> InvoiceResponse:
payment_hash = sha256(bytes.fromhex(data.preimage)).hexdigest()
payment = await get_standalone_payment(
payment_hash, incoming=True, wallet_id=key_type.wallet.id
)
if not payment:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="Payment does not exist or does not belong to this wallet.",
)
return await settle_hold_invoice(payment, data.preimage)
@payment_router.post("/{payment_request}/pay-with-nfc", status_code=HTTPStatus.OK)
async def api_payment_pay_with_nfc(
payment_request: str,
lnurl_data: PayLnurlWData,
) -> JSONResponse:
lnurl = lnurl_data.lnurl_w.lower()
# Follow LUD-17 -> https://github.com/lnurl/luds/blob/luds/17.md
url = lnurl.replace("lnurlw://", "https://")
@payment_router.post("/cancel")
async def api_payments_cancel(
data: CancelInvoice, key_type: WalletTypeInfo = Depends(require_admin_key)
) -> InvoiceResponse:
payment = await get_standalone_payment(
data.payment_hash, incoming=True, wallet_id=key_type.wallet.id
)
if not payment:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="Payment does not exist or does not belong to this wallet.",
)
return await cancel_hold_invoice(payment)
headers = {"User-Agent": settings.user_agent}
async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client:
try:
check_callback_url(url)
lnurl_req = await client.get(url, timeout=10)
if lnurl_req.is_error:
return JSONResponse(
{"success": False, "detail": "Error loading LNURL request"}
)
lnurl_res = lnurl_req.json()
if lnurl_res.get("status") == "ERROR":
return JSONResponse({"success": False, "detail": lnurl_res["reason"]})
if lnurl_res.get("tag") != "withdrawRequest":
return JSONResponse(
{"success": False, "detail": "Invalid LNURL-withdraw"}
)
callback_url = lnurl_res["callback"]
k1 = lnurl_res["k1"]
callback_req = await client.get(
callback_url,
params={"k1": k1, "pr": payment_request},
timeout=10,
)
if callback_req.is_error:
return JSONResponse(
{"success": False, "detail": "Error loading callback request"}
)
callback_res = callback_req.json()
if callback_res.get("status") == "ERROR":
return JSONResponse(
{"success": False, "detail": callback_res["reason"]}
)
else:
return JSONResponse({"success": True, "detail": callback_res})
except Exception as e:
return JSONResponse({"success": False, "detail": f"Unexpected error: {e}"})
+2 -2
View File
@@ -35,7 +35,7 @@ from lnbits.core.models.notifications import NotificationType
from lnbits.core.models.users import Account
from lnbits.core.services import (
create_user_account_no_ckeck,
enqueue_admin_notification,
enqueue_notification,
update_user_account,
update_user_extensions,
update_wallet_balance,
@@ -321,7 +321,7 @@ async def api_update_balance(data: UpdateBalance) -> SimpleStatus:
if not wallet:
raise HTTPException(HTTPStatus.NOT_FOUND, "Wallet not found.")
await update_wallet_balance(wallet=wallet, amount=int(data.amount))
enqueue_admin_notification(
enqueue_notification(
NotificationType.balance_update,
{
"amount": int(data.amount),
+4 -11
View File
@@ -3,8 +3,6 @@ from __future__ import annotations
import importlib
from enum import Enum
from loguru import logger
from lnbits.fiat.base import FiatProvider
from lnbits.settings import settings
@@ -17,7 +15,7 @@ class FiatProviderType(Enum):
stripe = "StripeWallet"
async def get_fiat_provider(name: str) -> FiatProvider | None:
async def get_fiat_provider(name: str) -> FiatProvider:
if name not in FiatProviderType.__members__:
raise ValueError(f"Fiat provider '{name}' is not supported.")
@@ -29,18 +27,13 @@ async def get_fiat_provider(name: str) -> FiatProvider | None:
del fiat_providers[name]
else:
return fiat_provider
_provider = _init_fiat_provider(FiatProviderType[name])
if not _provider:
return None
fiat_providers[name] = _provider
fiat_providers[name] = _init_fiat_provider(FiatProviderType[name])
return fiat_providers[name]
def _init_fiat_provider(fiat_provider: FiatProviderType) -> FiatProvider | None:
def _init_fiat_provider(fiat_provider: FiatProviderType) -> FiatProvider:
if not settings.is_fiat_provider_enabled(fiat_provider.name):
logger.warning(f"Fiat provider '{fiat_provider.name}' not enabled.")
return None
raise ValueError(f"Fiat provider '{fiat_provider.name}' not enabled.")
provider_constructor = getattr(fiat_module, fiat_provider.value)
return provider_constructor()
+3 -3
View File
@@ -16,6 +16,7 @@ from packaging import version
from pydantic.schema import field_schema
from lnbits.jinja2_templating import Jinja2Templates
from lnbits.nodes import get_node_class
from lnbits.settings import settings
from lnbits.utils.crypto import AESCipher
@@ -82,8 +83,8 @@ def template_renderer(additional_folders: Optional[list] = None) -> Jinja2Templa
"LNBITS_CUSTOM_BADGE_COLOR": settings.lnbits_custom_badge_color,
"LNBITS_EXTENSIONS_DEACTIVATE_ALL": settings.lnbits_extensions_deactivate_all,
"LNBITS_NEW_ACCOUNTS_ALLOWED": settings.new_accounts_allowed,
"LNBITS_NODE_UI": settings.lnbits_node_ui and settings.has_nodemanager,
"LNBITS_NODE_UI_AVAILABLE": settings.has_nodemanager,
"LNBITS_NODE_UI": settings.lnbits_node_ui and get_node_class() is not None,
"LNBITS_NODE_UI_AVAILABLE": get_node_class() is not None,
"LNBITS_QR_LOGO": settings.lnbits_qr_logo,
"LNBITS_SERVICE_FEE": settings.lnbits_service_fee,
"LNBITS_SERVICE_FEE_MAX": settings.lnbits_service_fee_max,
@@ -104,7 +105,6 @@ def template_renderer(additional_folders: Optional[list] = None) -> Jinja2Templa
if settings.lnbits_denomination == "FakeWallet"
else "sats"
),
"has_holdinvoice": settings.has_holdinvoice,
}
t.env.globals["WINDOW_SETTINGS"] = window_settings
+66
View File
@@ -0,0 +1,66 @@
from http import HTTPStatus
from typing import Callable
from fastapi import HTTPException, Request, Response
from fastapi.responses import JSONResponse
from fastapi.routing import APIRoute
from lnurl import LnurlErrorResponse, decode, encode, handle
from loguru import logger
from lnbits.exceptions import InvoiceError, PaymentError
class LnurlErrorResponseHandler(APIRoute):
"""
Custom APIRoute class to handle LNURL errors.
LNURL errors always return with status 200 and
a JSON response with `status="ERROR"` and a `reason` key.
Helps to catch HTTPException and return a valid lnurl error response
Example:
withdraw_lnurl_router = APIRouter(prefix="/api/v1/lnurl")
withdraw_lnurl_router.route_class = LnurlErrorResponseHandler
"""
def get_route_handler(self) -> Callable:
original_route_handler = super().get_route_handler()
async def lnurl_route_handler(request: Request) -> Response:
try:
response = await original_route_handler(request)
return response
except (InvoiceError, PaymentError) as exc:
logger.debug(f"Wallet Error: {exc}")
response = JSONResponse(
status_code=HTTPStatus.OK,
content={"status": "ERROR", "reason": f"{exc.message}"},
)
return response
except HTTPException as exc:
logger.debug(f"HTTPException: {exc}")
response = JSONResponse(
status_code=HTTPStatus.OK,
content={"status": "ERROR", "reason": f"{exc.detail}"},
)
return response
except Exception as exc:
logger.error("Unknown Error:", exc)
response = JSONResponse(
status_code=HTTPStatus.OK,
content={
"status": "ERROR",
"reason": f"UNKNOWN ERROR: {exc!s}",
},
)
return response
return lnurl_route_handler
__all__ = [
"LnurlErrorResponse",
"LnurlErrorResponseHandler",
"decode",
"encode",
"handle",
]
+15
View File
@@ -0,0 +1,15 @@
from typing import Optional
from .base import Node
def get_node_class() -> Optional[Node]:
return NODE
def set_node_class(node: Node):
global NODE
NODE = node
NODE: Optional[Node] = None
+1 -1
View File
@@ -11,12 +11,12 @@ from httpx import HTTPStatusError
from loguru import logger
from lnbits.db import Filters, Page
from lnbits.nodes import Node
from lnbits.nodes.base import (
ChannelBalance,
ChannelPoint,
ChannelState,
ChannelStats,
Node,
NodeChannel,
NodeFees,
NodeInfoResponse,
-15
View File
@@ -432,18 +432,6 @@ class NotificationsSettings(LNbitsSettings):
default=1_000_000, ge=0
)
def is_nostr_notifications_configured(self) -> bool:
return (
self.lnbits_nostr_notifications_enabled
and self.lnbits_nostr_notifications_private_key is not None
)
def is_telegram_notifications_configured(self) -> bool:
return (
self.lnbits_telegram_notifications_enabled
and self.lnbits_telegram_notifications_access_token is not None
)
class FakeWalletFundingSource(LNbitsSettings):
fake_wallet_secret: str = Field(default="ToTheMoon1")
@@ -999,9 +987,6 @@ class TransientSettings(InstalledExtensionsSettings, ExchangeHistorySettings):
server_startup_time: int = Field(default=time())
has_holdinvoice: bool = Field(default=False)
has_nodemanager: bool = Field(default=False)
@property
def lnbits_server_up_time(self) -> str:
up_time = int(time() - self.server_startup_time)
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+3 -3
View File
File diff suppressed because one or more lines are too long
+4 -22
View File
@@ -62,14 +62,6 @@ body[data-theme=monochrome].neon-border .q-date--dark {
box-shadow: none;
}
body[data-theme=salvador].neon-border .q-card,
body[data-theme=salvador].neon-border .q-card.q-card--dark,
body[data-theme=salvador].neon-border .q-date,
body[data-theme=salvador].neon-border .q-date--dark {
border: 2px solid #1976d2;
box-shadow: none;
}
body.hard-border .q-card,
body.hard-border .q-card.q-card--dark,
body.hard-border .q-date,
@@ -117,21 +109,21 @@ body[data-theme=bitcoin] [data-theme=bitcoin] .q-stepper--dark {
body[data-theme=freedom] {
--q-primary: #e22156;
--q-secondary: #b91a45;
--q-dark-page: #462f36;
--q-dark-page: #0a0a0a;
}
body[data-theme=freedom] [data-theme=freedom] .q-card--dark,
body[data-theme=freedom] [data-theme=freedom] .q-stepper--dark {
background: #47393d !important;
background: #1b1b1b !important;
}
body[data-theme=cyber] {
--q-primary: #7cb342;
--q-secondary: #558b2f;
--q-dark-page: #000;
--q-dark-page: #0a0a0a;
}
body[data-theme=cyber] [data-theme=cyber] .q-card--dark,
body[data-theme=cyber] [data-theme=cyber] .q-stepper--dark {
background: #1f2915 !important;
background: #1b1b1b !important;
}
body[data-theme=mint] {
@@ -174,16 +166,6 @@ body[data-theme=monochrome] [data-theme=monochrome] .q-stepper--dark {
background: rgb(39, 39, 39) !important;
}
body[data-theme=salvador] {
--q-primary: #1976d2;
--q-secondary: #26a69a;
--q-dark-page: #253647;
}
body[data-theme=salvador] [data-theme=salvador] .q-card--dark,
body[data-theme=salvador] [data-theme=salvador] .q-stepper--dark {
background: #343d47 !important;
}
body.gradient-bg {
background-image: linear-gradient(to bottom right, var(--q-dark-page), #0a0a0a);
background-attachment: fixed;
+4 -21
View File
@@ -52,7 +52,6 @@ window.localisation.en = {
wallet: 'Wallet: ',
wallet_name: 'Wallet name',
wallets: 'Wallets',
exclude_wallets: 'Exclude Wallets',
add_wallet: 'Add wallet',
add_new_wallet: 'Add a new wallet',
pin_wallet: 'Pin wallet',
@@ -177,17 +176,7 @@ window.localisation.en = {
extension_required_lnbits_version: 'This release requires LNbits version',
min_version: 'Minimum (included)',
max_version: 'Maximum (excluded)',
preimage: 'Preimage',
preimage_hint: 'Preimage to settle the hold invoice',
hold_invoice: 'Hold Invoice',
hold_invoice_description:
'This invoice is on hold and requires a preimage to settle.',
payment_hash: 'Payment Hash',
invoice_cancelled: 'Invoice Cancelled',
invoice_settled: 'Invoice Settled',
hold_invoice_payment_hash: 'Payment hash for hold invoice (optional)',
settle_invoice: 'Settle Invoice',
cancel_invoice: 'Cancel Invoice',
fee: 'Fee',
amount: 'Amount',
amount_limits: 'Amount Limits',
@@ -230,9 +219,6 @@ window.localisation.en = {
notifications_nostr_private_key: 'Nostr Private Key',
notifications_nostr_private_key_desc:
'Private key (hex or nsec) to sign the messages sent to Nostr',
notifications_nostr_identifier: 'Nostr Identifier',
notifications_nostr_identifier_desc:
'Nip5 identifier to send notifications to',
notifications_nostr_identifiers: 'Nostr Identifiers',
notifications_nostr_identifiers_desc:
'List of identifiers to send notifications to',
@@ -242,10 +228,9 @@ window.localisation.en = {
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: 'Telegram Chat ID',
notifications_chat_id_desc: 'Telegram Chat ID to send the notifications to',
notifications_excluded_wallets_desc:
'Do not send notifications for these wallets',
notifications_chat_id: 'Chat ID',
notifications_chat_id_desc: 'Chat ID to send the notifications to',
notifications_email_config: 'Email Configuration',
notifications_enable_email: 'Enable Email',
notifications_enable_email_desc: 'Send notifications over email',
@@ -649,7 +634,5 @@ window.localisation.en = {
'Signing secret for the webhook. Messages will be signed with this secret.',
callback_success_url: 'Callback Success URL',
callback_success_url_hint:
'The user will be redirected to this URL after the payment is successful',
connected: 'Connected',
not_connected: 'Not Connected'
'The user will be redirected to this URL after the payment is successful'
}
+7 -64
View File
@@ -22,7 +22,6 @@ window.localisation.fi = {
active_channels: 'Aktiivisia kanavia',
connect_peer: 'Yhdistä naapuriin',
connect: 'Yhdistä',
reconnect: 'Uudista yhteys',
open_channel: 'Avaa kanava',
open: 'Avaa',
close_channel: 'Sulje kanava',
@@ -52,18 +51,14 @@ window.localisation.fi = {
'Tämä QR-koodi sisältää URL-osoitteen, jolla saa lompakkoosi täydet valtuudet. Voit lukea sen puhelimellasi ja avata sillä lompakkosi. Voit myös lisätä lompakkosi selaimella käytettäväksi PWA-sovellukseksi puhelimen aloitusruudulle. ',
access_wallet_on_mobile: 'Mobiili käyttö',
wallet: 'Lompakko:',
wallet_name: 'Lompakon nimi',
wallets: 'Lompakot',
add_wallet: 'Lisää lompakko',
add_new_wallet: 'Lisää uusi lompakko',
pin_wallet: 'Kiinnitä lompakko',
delete_wallet: 'Poista lompakko',
delete_wallet_desc:
'Lompakko poistetaan pysyvästi. Siirrä lompakosta varat ennalta muualle, sillä tämä toiminto on PERUUTTAMATON!',
rename_wallet: 'Nimeä lompakko uudelleen',
update_name: 'Tallenna',
fiat_tracking: 'Käytettävä valuutta',
fiat_providers: 'Valuutan välittäjät',
currency: 'Valuutta',
update_currency: 'Tallenna',
press_to_claim: 'Lunasta varat painamalla tästä',
@@ -127,7 +122,6 @@ window.localisation.fi = {
no_extensions: 'Laajennuksia ei ole asennettu :(',
created: 'Luotu',
search_extensions: 'Etsi laajennuksia',
search_wallets: 'Etsi lompakkoa',
extension_sources: 'Laajennuslähteet',
ext_sources_hint: 'Lähteet joista laajennuksia voi ladata',
ext_sources_label:
@@ -140,7 +134,6 @@ window.localisation.fi = {
uninstall: 'Poista',
drop_db: 'Poista tiedot',
enable: 'Ota käyttöön',
enabled: 'Käytössä',
pay_to_enable: 'Maksa ottaaksesi käyttöön',
enable_extension_details: 'Ota laajennus käyttöön tälle käyttäjälle',
disable: 'Poista käytöstä',
@@ -172,33 +165,12 @@ window.localisation.fi = {
payment_hash: 'Maksun tiiviste',
fee: 'Kulu',
amount: 'Määrä',
amount_limits: 'Määrien rajat',
amount_sats: 'Määrä (sat)',
faucest_wallet: 'Faucet Wallet',
faucest_wallet_desc_1:
'Each time a payment is confirmed by the {provider} provider funds will be subtracted from this wallet.',
faucest_wallet_desc_2:
'This helps monitor all {provider} payments and their status.',
faucest_wallet_desc_3:
'This wallet must be topped up with the amount of sats that the admin is willing to offer in exchange for the fiat currency.',
faucest_wallet_desc_4:
'If this wallet is configured, but is empty, the {provider} payments will not be processed.',
faucest_wallet_desc_5:
'This wallet can eventually get to a negative balance if parallel fiat payments are made.',
faucest_wallet_id: 'Faucet Wallet ID (optional)',
faucest_wallet_id_hint:
'Wallet ID to use for the faucet. It will be used to send the funds to the user.',
tag: 'Tunniste',
unit: 'Yksikkö',
description: 'Kuvaus',
expiry: 'Vanhenee',
webhook: 'Webhook',
webhook_url: 'Webhook URL',
webhook_url_hint:
'Webhook URL to send the payment details to. It will be called when the payment is completed.',
webhook_events_list: 'The following events must be supported by the webhook:',
webhook_stripe_description:
'One the stripe side you must configure a webhook with a URL that points to your LNbits server.',
payment_proof: 'Maksun varmenne',
update: 'Päivitä',
update_available: 'Saatavilla on päivitys {version}-versioon!',
@@ -290,7 +262,6 @@ window.localisation.fi = {
notification_source_label:
'Lähde-URL (käytä ainoastaan LNbits:iä tai muuta luotettavaa lähdettä)',
more: 'näytä lisää',
more_count: 'näytä {count} lisää',
less: 'supista',
releases: 'Julkaisut',
watchdog: 'Watchdog',
@@ -305,7 +276,7 @@ window.localisation.fi = {
callback_url_rules: 'Callback URL -säännöt',
enter_callback_url_rule: 'Anna URL-sääntö regex-muodossa ja paina enter',
callback_url_rule_hint:
'Callback URL:it (kuten LNURL) tarkistetaan kaikkien näiden sääntöjen mukaisesti. Jos sääntöjä ei ole määritetty, kaikki URL:it ovat sallittuja.',
'Callback URL:it (kuten LNURL) tarkistetaan näiden sääntöjen mukaisesti. Jos sääntöjä ei ole määritetty, kaikki URL:it ovat sallittuja.',
wallet_limiter: 'Lompakon käyttörajoitin',
wallet_config: 'Wallet Config',
wallet_charts: 'Wallet Charts',
@@ -330,9 +301,6 @@ window.localisation.fi = {
login_with_user_id: 'Kirjaudu käyttäjä-ID:llä',
or: 'tai',
create_new_wallet: 'Avaa uusi lompakko',
delete_all_wallets: 'Poista kaikki lompakot',
confirm_delete_all_wallets:
'Oletko todellakin varma, että haluat poistaa käyttäjältä KAIKKI lompakot?',
login_to_account: 'Kirjaudu käyttäjänimellä',
create_account: 'Luo tili',
account_settings: 'Tilin asetukset',
@@ -341,7 +309,7 @@ window.localisation.fi = {
signin_with_nostr: 'Kirjaudu Nostr:lla',
signin_with_google: 'Kirjaudu Google-tunnuksella',
signin_with_github: 'Kirjaudu GitHub-tunnuksella',
signin_with_custom_org: 'Kirjaudu {custom_org}-palvelulla',
signin_with_keycloak: 'Kirjaudu Keycloak-tunnuksella',
username_or_email: 'Käyttäjänimi tai sähköposti',
password: 'Anna uusi salasana',
password_config: 'Salasanan määritys',
@@ -350,10 +318,7 @@ window.localisation.fi = {
change_password: 'Vaihda salasana',
update_credentials: 'Päivitä käyttöoikeustiedot',
update_pubkey: 'Päivitä julkinen avain',
nostr_pubkey_tooltip:
'Syötä tämän käyttäjän julkinen Nostr avain (hex arvona)',
set_password: 'Aseta salasana',
set_password_tooltip: 'Aseta käyttäjätunnukselle salasana',
invalid_password: 'Salasanassa tulee olla vähintään kahdeksan merkkiä',
invalid_password_repeat: 'Salasanat eivät täsmää',
reset_key_generated: 'Salasanan vaihtoavain on luotu.',
@@ -362,8 +327,7 @@ window.localisation.fi = {
register: 'Rekisteröidy',
username: 'Käyttäjänimi',
pubkey: 'Julkinen avain',
user_id: 'Käyttäjä tunnus',
id: 'tunnus',
user_id: 'Käyttäjä ID',
email: 'Sähköposti',
first_name: 'Etunimi',
last_name: 'Sukunimi',
@@ -379,8 +343,6 @@ window.localisation.fi = {
back: 'Takaisin',
logout: 'Poistu',
look_and_feel: 'Kieli ja värit',
endpoint: 'Endpoint',
api: 'API',
api_token: 'API Token',
api_tokens: 'API Tokens',
access_control_list: 'Access Control List',
@@ -392,14 +354,11 @@ window.localisation.fi = {
gradient_background: 'Gradient Background',
language: 'Kieli',
color_scheme: 'Väriteema',
visible_wallet_count: 'Näytettävien lompakkojen määrä',
admin_settings: 'Pääkäyttäjän asetukset',
extension_cost: 'Tämä laajennus edellyttää vähintään {cost} sat maksua.',
extension_paid_sats: 'Olet jo maksanut {paid_sats} satsia.',
release_details_error: 'Ei voi hakea julkaisun tietoja.',
pay_from_wallet: 'Maksa lompakosta',
pay_with: 'Maksa {provider}:lla',
select_payment_provider: 'Valitse maksun välittäjä',
wallet_required: 'Lompakko *',
show_qr: 'Näytä QR',
retry_install: 'Yritä asennusta uudelleen',
@@ -412,9 +371,6 @@ window.localisation.fi = {
'{name} -laajennuksen aktivointi edellyttää vähintään {amount} sat maksua.',
hide_empty_wallets: 'Piilota tyhjät lompakot',
recheck: 'Tarkista uudelleen',
check: 'Tarkista',
check_connection: 'Tarkista yhteys',
check_webhook: 'Tarkista Webhook',
contributors: 'Avustajat',
license: 'Lisenssi',
reset_key: 'Vaihda avain',
@@ -491,9 +447,9 @@ window.localisation.fi = {
fee_reserve_percent: 'Kuluvaraus prosentteina',
fee_reserve_msats: 'Kuluvaraus milli-sat',
reserve_fee_in_percent: 'Kuluvaraus prosentteina',
payment_wait_time: 'Maksun odotusaika (sekuntia)',
payment_wait_time: 'Maksun odotusaika',
payment_wait_time_desc:
'Kuinka pitkään maksua odotetaan saapuvaksi, ennen kuin se merkitään Odotetaan-tilaan. Aseta pidemmäksi käytettäessä HODL-laskuja, Boltz-palvelua, tms',
'Kuinka pitkään maksua odotetaan saapuvaksi ennenkuin se merkitään Odotetaan -tilaan. Aseta pidemmäksi käytettäessä HODL-laskuja, Boltz-palvelua tms',
server_management: 'Palvelimen hallinta',
base_url: 'Palvelimen URL-osoite',
base_url_label: 'Palvelun staattinen pohja-URL',
@@ -518,16 +474,12 @@ window.localisation.fi = {
auth_keycloak_ci_hint:
'Varmista, että valtuutuksen palautus-URL on asetettu muotoon https://{domain}/api/v1/auth/keycloak/token',
auth_keycloak_cs_label: 'Keycloak-asiakassalasana',
auth_keycloak_custom_org_label: 'Valinnainen Keycloak-organisaatio',
auth_keycloak_custom_icon_label: 'Valinnainen Keycloak-kuvake (URL)',
currency_settings: 'Valuutta-asetukset',
allowed_currencies: 'Käytettävät valuutat',
allowed_currencies_hint: 'Valitse käytettävissä olevat fiat-valuutat',
default_account_currency: 'Tilin oletusvaluutta',
default_account_currency_hint: 'Kirjanpidon oletusvaluutta',
min_incoming_payment_amount: 'Pienin vastaanotettava maksun määrä',
min_incoming_payment_amount_desc:
'Pienin maksun määrä jolle voi luoda laskun',
max_incoming_payment_amount: 'Saapuvan maksun enimmäismäärä',
max_incoming_payment_amount_desc: 'Enimmäismäärä jonka voi laskuttaa',
max_outgoing_payment_amount: 'Lähtevän maksun enimmäismäärä',
@@ -590,8 +542,6 @@ window.localisation.fi = {
admin_users_label: 'Käyttäjätunnus',
allowed_users: 'Sallitut käyttäjät',
allowed_users_hint: 'Vain nämä käyttäjät voivat käyttää LNbitsiä',
allowed_users_hint_feature:
'Ainoastaan nämä käyttäjät voivat käyttää ominaisuutta {feature}',
allowed_users_label: 'Käyttäjätunnus',
allow_creation_user: 'Salli uusien käyttäjien luominen',
allow_creation_user_desc: 'Etusivulta on mahdollisuus luoda uusia käyttäjiä',
@@ -627,12 +577,5 @@ window.localisation.fi = {
view_column: 'Näytä lompakot rinnakkain',
filter_payments: 'Suodata maksuja',
filter_date: 'Suodata päiväyksellä',
websocket_example: 'Websocket example',
secret_key: 'Secret Key',
signing_secret: 'Signing Secret',
signing_secret_hint:
'Signing secret for the webhook. Messages will be signed with this secret.',
callback_success_url: 'Callback Success URL',
callback_success_url_hint:
'The user will be redirected to this URL after the payment is successful'
websocket_example: 'Websocket esimerkki'
}
-6
View File
@@ -80,11 +80,6 @@ window.AccountPageLogic = {
token_id_list: [],
allRead: false,
allWrite: false
},
notifications: {
nostr: {
identifier: ''
}
}
}
},
@@ -401,7 +396,6 @@ window.AccountPageLogic = {
}
}
},
async created() {
try {
const {data} = await LNbits.api.getAuthenticatedUser()
+36 -14
View File
@@ -19,19 +19,17 @@ window.LNbits = {
amount,
memo,
unit = 'sat',
lnurlWithdraw = null,
lnurlCallback = null,
fiatProvider = null,
internalMemo = null,
payment_hash = null
internalMemo = null
) {
const data = {
out: false,
amount: amount,
memo: memo,
unit: unit,
lnurl_withdraw: lnurlWithdraw,
fiat_provider: fiatProvider,
payment_hash: payment_hash
lnurl_callback: lnurlCallback,
fiat_provider: fiatProvider
}
if (internalMemo) {
data.extra = {
@@ -52,14 +50,39 @@ window.LNbits = {
}
return this.request('post', '/api/v1/payments', wallet.adminkey, data)
},
cancelInvoice(wallet, paymentHash) {
return this.request('post', '/api/v1/payments/cancel', wallet.adminkey, {
payment_hash: paymentHash
})
payLnurl(
wallet,
callback,
description_hash,
amount,
description = '',
comment = '',
unit = '',
internalMemo = null
) {
const data = {
callback,
description_hash,
amount,
comment,
description,
unit
}
if (internalMemo) {
data.internal_memo = String(internalMemo)
}
return this.request(
'post',
'/api/v1/payments/lnurl',
wallet.adminkey,
data
)
},
settleInvoice(wallet, preimage) {
return this.request('post', `/api/v1/payments/settle`, wallet.adminkey, {
preimage: preimage
authLnurl(wallet, callback) {
return this.request('post', '/api/v1/lnurlauth', wallet.adminkey, {
callback
})
},
createAccount(name) {
@@ -615,7 +638,6 @@ window.windowMixin = {
}
},
applyBackgroundImage() {
if (this.bgimageChoice == 'null') this.bgimageChoice = ''
if (this.bgimageChoice == '') {
document.body.classList.remove('bg-image')
} else {
@@ -118,13 +118,7 @@ window.app.component('payment-list', {
field: row => row.extra.wallet_fiat_amount
}
],
preimage: null,
loading: false
},
hodlInvoice: {
show: false,
payment: null,
preimage: null
}
}
},
@@ -229,35 +223,6 @@ window.app.component('payment-list', {
})
.catch(LNbits.utils.notifyApiError)
},
showHoldInvoiceDialog(payment) {
this.hodlInvoice.show = true
this.hodlInvoice.preimage = ''
this.hodlInvoice.payment = payment
},
cancelHoldInvoice(payment_hash) {
LNbits.api
.cancelInvoice(this.g.wallet, payment_hash)
.then(() => {
this.update = !this.update
Quasar.Notify.create({
type: 'positive',
message: this.$t('invoice_cancelled')
})
})
.catch(LNbits.utils.notifyApiError)
},
settleHoldInvoice(preimage) {
LNbits.api
.settleInvoice(this.g.wallet, preimage)
.then(() => {
this.update = !this.update
Quasar.Notify.create({
type: 'positive',
message: this.$t('invoice_settled')
})
})
.catch(LNbits.utils.notifyApiError)
},
paymentTableRowKey(row) {
return row.payment_hash + row.amount
},
+101 -70
View File
@@ -40,8 +40,7 @@ window.WalletPageLogic = {
data: {
amount: null,
memo: '',
internalMemo: null,
payment_hash: null
internalMemo: null
}
},
invoiceQrCode: '',
@@ -201,7 +200,6 @@ window.WalletPageLogic = {
this.receive.data.amount = null
this.receive.data.memo = null
this.receive.data.internalMemo = null
this.receive.data.payment_hash = null
this.receive.unit = this.isFiatPriority
? this.g.wallet.currency || 'sat'
: 'sat'
@@ -258,10 +256,9 @@ window.WalletPageLogic = {
this.receive.data.amount,
this.receive.data.memo,
this.receive.unit,
this.receive.lnurlWithdraw,
this.receive.lnurl && this.receive.lnurl.callback,
this.receive.fiatProvider,
this.receive.data.internalMemo,
this.receive.data.payment_hash
this.receive.data.internalMemo
)
.then(response => {
this.g.updatePayments = !this.g.updatePayments
@@ -274,26 +271,27 @@ window.WalletPageLogic = {
if (!this.receive.lnurl) {
this.readNfcTag()
}
// TODO: lnurl_callback and lnurl_response
// WITHDRAW
if (response.data.extra.lnurl_response !== null) {
if (response.data.extra.lnurl_response === false) {
response.data.extra.lnurl_response = `Unable to connect`
if (response.data.lnurl_response !== null) {
if (response.data.lnurl_response === false) {
response.data.lnurl_response = `Unable to connect`
}
const domain = this.receive.lnurl.callback.split('/')[2]
if (typeof response.data.extra.lnurl_response === 'string') {
if (typeof response.data.lnurl_response === 'string') {
// failure
Quasar.Notify.create({
timeout: 5000,
type: 'warning',
message: `${domain} lnurl-withdraw call failed.`,
caption: response.data.extra.lnurl_response
message: `${this.receive.lnurl.domain} lnurl-withdraw call failed.`,
caption: response.data.lnurl_response
})
return
} else if (response.data.extra.lnurl_response === true) {
} else if (response.data.lnurl_response === true) {
// success
Quasar.Notify.create({
timeout: 3000,
message: `Invoice sent to ${domain}!`,
timeout: 5000,
message: `Invoice sent to ${this.receive.lnurl.domain}!`,
spinner: true
})
}
@@ -349,25 +347,25 @@ window.WalletPageLogic = {
)
.then(response => {
const data = response.data
if (data.status === 'ERROR') {
Quasar.Notify.create({
timeout: 5000,
type: 'warning',
message: `lnurl scan failed.`,
message: `${data.domain} lnurl call failed.`,
caption: data.reason
})
return
}
if (data.tag === 'payRequest') {
if (data.kind === 'pay') {
this.parse.lnurlpay = Object.freeze(data)
this.parse.data.amount = data.minSendable / 1000
} else if (data.tag === 'login') {
} else if (data.kind === 'auth') {
this.parse.lnurlauth = Object.freeze(data)
} else if (data.tag === 'withdrawRequest') {
} else if (data.kind === 'withdraw') {
this.parse.show = false
this.receive.show = true
this.receive.lnurlWithdraw = Object.freeze(data)
this.receive.status = 'pending'
this.receive.paymentReq = null
this.receive.paymentHash = null
@@ -377,9 +375,8 @@ window.WalletPageLogic = {
data.minWithdrawable / 1000,
data.maxWithdrawable / 1000
]
const domain = data.callback.split('/')[2]
this.receive.lnurl = {
domain: domain,
domain: data.domain,
callback: data.callback,
fixed: data.fixed
}
@@ -515,49 +512,86 @@ window.WalletPageLogic = {
})
},
payLnurl() {
const dismissPaymentMsg = Quasar.Notify.create({
timeout: 0,
message: 'Processing payment...'
})
LNbits.api
.request('post', '/api/v1/payments/lnurl', this.g.wallet.adminkey, {
res: this.parse.lnurlpay,
unit: this.parse.data.unit,
amount: this.parse.data.amount * 1000,
comment: this.parse.data.comment,
internalMemo: this.parse.data.internalMemo
})
.payLnurl(
this.g.wallet,
this.parse.lnurlpay.callback,
this.parse.lnurlpay.description_hash,
this.parse.data.amount * 1000,
this.parse.lnurlpay.description.slice(0, 120),
this.parse.data.comment,
this.parse.data.unit,
this.parse.data.internalMemo
)
.then(response => {
this.parse.show = false
if (response.data.extra.success_action) {
const action = JSON.parse(response.data.extra.success_action)
switch (action.tag) {
case 'url':
Quasar.Notify.create({
message: `<a target="_blank" style="color: inherit" href="${action.url}">${action.url}</a>`,
caption: action.description,
html: true,
type: 'positive',
timeout: 0,
closeBtn: true
})
break
case 'message':
Quasar.Notify.create({
message: action.message,
type: 'positive',
timeout: 0,
closeBtn: true
})
break
case 'aes':
decryptLnurlPayAES(action, response.data.preimage)
Quasar.Notify.create({
message: value,
caption: extra.success_action.description,
html: true,
type: 'positive',
timeout: 0,
closeBtn: true
})
}
}
clearInterval(this.parse.paymentChecker)
setTimeout(() => {
clearInterval(this.parse.paymentChecker)
}, 40000)
this.parse.paymentChecker = setInterval(() => {
LNbits.api
.getPayment(this.g.wallet, response.data.payment_hash)
.then(res => {
if (res.data.paid) {
dismissPaymentMsg()
clearInterval(this.parse.paymentChecker)
// show lnurlpay success action
const extra = response.data.extra
if (extra.success_action) {
switch (extra.success_action.tag) {
case 'url':
Quasar.Notify.create({
message: `<a target="_blank" style="color: inherit" href="${extra.success_action.url}">${extra.success_action.url}</a>`,
caption: extra.success_action.description,
html: true,
type: 'positive',
timeout: 0,
closeBtn: true
})
break
case 'message':
Quasar.Notify.create({
message: extra.success_action.message,
type: 'positive',
timeout: 0,
closeBtn: true
})
break
case 'aes':
LNbits.api
.getPayment(this.g.wallet, response.data.payment_hash)
.then(({data: payment}) =>
decryptLnurlPayAES(
extra.success_action,
payment.preimage
)
)
.then(value => {
Quasar.Notify.create({
message: value,
caption: extra.success_action.description,
html: true,
type: 'positive',
timeout: 0,
closeBtn: true
})
})
break
}
}
}
})
}, 2000)
})
.catch(err => {
dismissPaymentMsg()
LNbits.utils.notifyApiError(err)
})
},
authLnurl() {
@@ -565,13 +599,9 @@ window.WalletPageLogic = {
timeout: 10,
message: 'Performing authentication...'
})
LNbits.api
.request(
'post',
'/api/v1/lnurlauth',
wallet.adminkey,
this.parse.lnurlauth
)
.authLnurl(this.g.wallet, this.parse.lnurlauth.callback)
.then(_ => {
dismissAuthMsg()
Quasar.Notify.create({
@@ -582,9 +612,10 @@ window.WalletPageLogic = {
this.parse.show = false
})
.catch(err => {
dismissAuthMsg()
if (err.response.data.reason) {
Quasar.Notify.create({
message: `Authentication failed. ${this.parse.lnurlauth.callback} says:`,
message: `Authentication failed. ${this.parse.lnurlauth.domain} says:`,
caption: err.response.data.reason,
type: 'warning',
timeout: 5000
+4 -12
View File
@@ -18,16 +18,16 @@ $themes: (
'freedom': (
primary: #e22156,
secondary: #b91a45,
dark: #462f36,
info: #47393d,
dark: #0a0a0a,
info: #1b1b1b,
marginal-bg: #2d293b,
marginal-text: #fff
),
'cyber': (
primary: #7cb342,
secondary: #558b2f,
dark: #000,
info: #1f2915,
dark: #0a0a0a,
info: #1b1b1b,
marginal-bg: #2d293b,
marginal-text: #fff
),
@@ -62,13 +62,5 @@ $themes: (
info: rgb(39, 39, 39),
marginal-bg: #000,
marginal-text: rgb(255, 255, 255)
),
'salvador': (
primary: #1976d2,
secondary: #26a69a,
dark: #253647,
info: #343d47,
marginal-bg: #0d1620,
marginal-text: rgb(255, 255, 255)
)
);
File diff suppressed because one or more lines are too long
+1 -64
View File
@@ -880,19 +880,6 @@
:props="props"
style="white-space: normal; word-break: break-all"
>
<q-icon
v-if="
props.row.isIn &&
props.row.isPending &&
props.row.extra.hold_invoice
"
name="pause_presentation"
color="grey"
class="cursor-pointer q-mr-sm"
@click="showHoldInvoiceDialog(props.row)"
>
<q-tooltip><span v-text="$t('hold_invoice')"></span></q-tooltip>
</q-icon>
<q-badge
v-if="props.row.tag"
color="yellow"
@@ -954,7 +941,7 @@
</q-td>
<q-dialog v-model="props.expand" :props="props" position="top">
<q-card class="q-pa-sm q-pt-xl lnbits__dialog-card">
<q-card-section>
<q-card-section class="">
<q-list bordered separator>
<q-expansion-item
expand-separator
@@ -1017,7 +1004,6 @@
></lnbits-payment-details>
</q-expansion-item>
</q-list>
<div
v-if="props.row.isIn && props.row.isPending && props.row.bolt11"
class="text-center q-my-lg"
@@ -1074,55 +1060,6 @@
</q-card-section>
</q-card>
</q-dialog>
<q-dialog v-model="hodlInvoice.show" position="top">
<q-card class="q-pa-sm q-pt-xl lnbits__dialog-card">
<q-card-section>
<q-item-label class="text-h6">
<span v-text="$t('hold_invoice')"></span>
</q-item-label>
<q-item-label class="text-subtitle2">
<span v-text="$t('hold_invoice_description')"></span>
</q-item-label>
</q-card-section>
<q-card-section>
<q-input
filled
:label="$t('preimage')"
:hint="$t('preimage_hint')"
v-model="hodlInvoice.preimage"
dense
autofocus
@keyup.enter="settleHoldInvoice(hodlInvoice.preimage)"
>
</q-input>
</q-card-section>
<q-card-section class="row q-gutter-x-sm">
<q-btn
@click="settleHoldInvoice(hodlInvoice.preimage)"
outline
v-close-popup
color="grey"
:label="$t('settle_invoice')"
>
</q-btn>
<q-btn
v-close-popup
outline
color="grey"
class="q-ml-sm"
@click="cancelHoldInvoice(hodlInvoice.payment.payment_hash)"
:label="$t('cancel_invoice')"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('close')"
></q-btn>
</q-card-section>
</q-card>
</q-dialog>
</q-tr>
</template>
</q-table>
+1 -1
View File
@@ -13,7 +13,7 @@ from lnbits.settings import settings
def log_server_info():
logger.info("LNbits Info")
logger.info("Starting LNbits")
logger.info(f"Version: {settings.version}")
logger.info(f"Baseurl: {settings.lnbits_baseurl}")
logger.info(f"Host: {settings.host}")
+5 -4
View File
@@ -2,8 +2,9 @@ from __future__ import annotations
import importlib
from lnbits.nodes import set_node_class
from lnbits.settings import settings
from lnbits.wallets.base import Feature, Wallet
from lnbits.wallets.base import Wallet
from .alby import AlbyWallet
from .blink import BlinkWallet
@@ -34,13 +35,13 @@ from .void import VoidWallet
from .zbd import ZBDWallet
def set_funding_source(class_name: str | None = None) -> None:
def set_funding_source(class_name: str | None = None):
backend_wallet_class = class_name or settings.lnbits_backend_wallet_class
funding_source_constructor = getattr(wallets_module, backend_wallet_class)
global funding_source
funding_source = funding_source_constructor()
settings.has_nodemanager = funding_source.has_feature(Feature.nodemanager)
settings.has_holdinvoice = funding_source.has_feature(Feature.holdinvoice)
if funding_source.__node_cls__:
set_node_class(funding_source.__node_cls__(funding_source))
def get_funding_source() -> Wallet:
-35
View File
@@ -3,24 +3,16 @@ from __future__ import annotations
import asyncio
from abc import ABC, abstractmethod
from collections.abc import AsyncGenerator, Coroutine
from enum import Enum
from typing import TYPE_CHECKING, NamedTuple
from loguru import logger
from lnbits.exceptions import InvoiceError
from lnbits.settings import settings
if TYPE_CHECKING:
from lnbits.nodes.base import Node
class Feature(Enum):
nodemanager = "nodemanager"
holdinvoice = "holdinvoice"
# bolt12 = "bolt12"
class StatusResponse(NamedTuple):
error_message: str | None
balance_msat: int
@@ -108,10 +100,6 @@ class PaymentPendingStatus(PaymentStatus):
class Wallet(ABC):
__node_cls__: type[Node] | None = None
features: list[Feature] | None = None
def has_feature(self, feature: Feature) -> bool:
return self.features is not None and feature in self.features
def __init__(self) -> None:
self.pending_invoices: list[str] = []
@@ -153,29 +141,6 @@ class Wallet(ABC):
) -> Coroutine[None, None, PaymentStatus]:
pass
async def create_hold_invoice(
self,
amount: int,
payment_hash: str,
memo: str | None = None,
description_hash: bytes | None = None,
unhashed_description: bytes | None = None,
**kwargs,
) -> InvoiceResponse:
raise InvoiceError(
message="Hold invoices are not supported by this wallet.", status="failed"
)
async def settle_hold_invoice(self, preimage: str) -> InvoiceResponse:
raise InvoiceError(
message="Hold invoices are not supported by this wallet.", status="failed"
)
async def cancel_hold_invoice(self, payment_hash: str) -> InvoiceResponse:
raise InvoiceError(
message="Hold invoices are not supported by this wallet.", status="failed"
)
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
while settings.lnbits_running:
for invoice in self.pending_invoices:
+2 -1
View File
@@ -7,7 +7,8 @@ from typing import Optional
import httpx
from loguru import logger
from pydantic import BaseModel
from websockets import Subprotocol, connect
from websockets.legacy.client import connect
from websockets.typing import Subprotocol
from lnbits import bolt11
from lnbits.helpers import normalize_endpoint
-5
View File
@@ -14,7 +14,6 @@ from lnbits.settings import settings
from lnbits.utils.crypto import random_secret_and_hash
from .base import (
Feature,
InvoiceResponse,
PaymentFailedStatus,
PaymentPendingStatus,
@@ -32,16 +31,12 @@ async def run_sync(func) -> Any:
class CoreLightningWallet(Wallet):
"""Core Lightning RPC implementation."""
__node_cls__ = CoreLightningNode
features = [Feature.nodemanager]
async def cleanup(self):
pass
def __init__(self):
rpc = settings.corelightning_rpc or settings.clightning_rpc
if not rpc:
raise ValueError(
+2 -4
View File
@@ -9,7 +9,7 @@ from typing import Any, Optional
import httpx
from loguru import logger
from websockets import connect
from websockets.legacy.client import connect
from lnbits.helpers import normalize_endpoint
from lnbits.settings import settings
@@ -245,9 +245,7 @@ class EclairWallet(Wallet):
try:
async with connect(
self.ws_url,
additional_headers=[
("Authorization", self.headers["Authorization"])
],
extra_headers=[("Authorization", self.headers["Authorization"])],
) as ws:
while settings.lnbits_running:
message = await ws.recv()
+1 -1
View File
@@ -5,7 +5,7 @@ from typing import Optional
import httpx
from loguru import logger
from websockets import connect
from websockets.legacy.client import connect
from lnbits.helpers import normalize_endpoint
from lnbits.settings import settings
@@ -1,65 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: invoices.proto
# Protobuf Python Version: 5.28.1
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
_runtime_version.Domain.PUBLIC,
5,
28,
1,
'',
'invoices.proto'
)
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
import lnbits.wallets.lnd_grpc_files.lightning_pb2 as lightning__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0einvoices.proto\x12\x0binvoicesrpc\x1a\x0flightning.proto\"(\n\x10\x43\x61ncelInvoiceMsg\x12\x14\n\x0cpayment_hash\x18\x01 \x01(\x0c\"\x13\n\x11\x43\x61ncelInvoiceResp\"\xe4\x01\n\x15\x41\x64\x64HoldInvoiceRequest\x12\x0c\n\x04memo\x18\x01 \x01(\t\x12\x0c\n\x04hash\x18\x02 \x01(\x0c\x12\r\n\x05value\x18\x03 \x01(\x03\x12\x12\n\nvalue_msat\x18\n \x01(\x03\x12\x18\n\x10\x64\x65scription_hash\x18\x04 \x01(\x0c\x12\x0e\n\x06\x65xpiry\x18\x05 \x01(\x03\x12\x15\n\rfallback_addr\x18\x06 \x01(\t\x12\x13\n\x0b\x63ltv_expiry\x18\x07 \x01(\x04\x12%\n\x0broute_hints\x18\x08 \x03(\x0b\x32\x10.lnrpc.RouteHint\x12\x0f\n\x07private\x18\t \x01(\x08\"V\n\x12\x41\x64\x64HoldInvoiceResp\x12\x17\n\x0fpayment_request\x18\x01 \x01(\t\x12\x11\n\tadd_index\x18\x02 \x01(\x04\x12\x14\n\x0cpayment_addr\x18\x03 \x01(\x0c\"$\n\x10SettleInvoiceMsg\x12\x10\n\x08preimage\x18\x01 \x01(\x0c\"\x13\n\x11SettleInvoiceResp\"5\n\x1dSubscribeSingleInvoiceRequest\x12\x0e\n\x06r_hash\x18\x02 \x01(\x0cJ\x04\x08\x01\x10\x02\"\x99\x01\n\x10LookupInvoiceMsg\x12\x16\n\x0cpayment_hash\x18\x01 \x01(\x0cH\x00\x12\x16\n\x0cpayment_addr\x18\x02 \x01(\x0cH\x00\x12\x10\n\x06set_id\x18\x03 \x01(\x0cH\x00\x12\x34\n\x0flookup_modifier\x18\x04 \x01(\x0e\x32\x1b.invoicesrpc.LookupModifierB\r\n\x0binvoice_ref\".\n\nCircuitKey\x12\x0f\n\x07\x63han_id\x18\x01 \x01(\x04\x12\x0f\n\x07htlc_id\x18\x02 \x01(\x04\"\xdd\x02\n\x11HtlcModifyRequest\x12\x1f\n\x07invoice\x18\x01 \x01(\x0b\x32\x0e.lnrpc.Invoice\x12\x36\n\x15\x65xit_htlc_circuit_key\x18\x02 \x01(\x0b\x32\x17.invoicesrpc.CircuitKey\x12\x15\n\rexit_htlc_amt\x18\x03 \x01(\x04\x12\x18\n\x10\x65xit_htlc_expiry\x18\x04 \x01(\r\x12\x16\n\x0e\x63urrent_height\x18\x05 \x01(\r\x12\x64\n\x1d\x65xit_htlc_wire_custom_records\x18\x06 \x03(\x0b\x32=.invoicesrpc.HtlcModifyRequest.ExitHtlcWireCustomRecordsEntry\x1a@\n\x1e\x45xitHtlcWireCustomRecordsEntry\x12\x0b\n\x03key\x18\x01 \x01(\x04\x12\r\n\x05value\x18\x02 \x01(\x0c:\x02\x38\x01\"z\n\x12HtlcModifyResponse\x12,\n\x0b\x63ircuit_key\x18\x01 \x01(\x0b\x32\x17.invoicesrpc.CircuitKey\x12\x15\n\x08\x61mt_paid\x18\x02 \x01(\x04H\x00\x88\x01\x01\x12\x12\n\ncancel_set\x18\x03 \x01(\x08\x42\x0b\n\t_amt_paid*D\n\x0eLookupModifier\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10\x00\x12\x11\n\rHTLC_SET_ONLY\x10\x01\x12\x12\n\x0eHTLC_SET_BLANK\x10\x02\x32\xf0\x03\n\x08Invoices\x12V\n\x16SubscribeSingleInvoice\x12*.invoicesrpc.SubscribeSingleInvoiceRequest\x1a\x0e.lnrpc.Invoice0\x01\x12N\n\rCancelInvoice\x12\x1d.invoicesrpc.CancelInvoiceMsg\x1a\x1e.invoicesrpc.CancelInvoiceResp\x12U\n\x0e\x41\x64\x64HoldInvoice\x12\".invoicesrpc.AddHoldInvoiceRequest\x1a\x1f.invoicesrpc.AddHoldInvoiceResp\x12N\n\rSettleInvoice\x12\x1d.invoicesrpc.SettleInvoiceMsg\x1a\x1e.invoicesrpc.SettleInvoiceResp\x12@\n\x0fLookupInvoiceV2\x12\x1d.invoicesrpc.LookupInvoiceMsg\x1a\x0e.lnrpc.Invoice\x12S\n\x0cHtlcModifier\x12\x1f.invoicesrpc.HtlcModifyResponse\x1a\x1e.invoicesrpc.HtlcModifyRequest(\x01\x30\x01\x42\x33Z1github.com/lightningnetwork/lnd/lnrpc/invoicesrpcb\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'invoices_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
_globals['DESCRIPTOR']._loaded_options = None
_globals['DESCRIPTOR']._serialized_options = b'Z1github.com/lightningnetwork/lnd/lnrpc/invoicesrpc'
_globals['_HTLCMODIFYREQUEST_EXITHTLCWIRECUSTOMRECORDSENTRY']._loaded_options = None
_globals['_HTLCMODIFYREQUEST_EXITHTLCWIRECUSTOMRECORDSENTRY']._serialized_options = b'8\001'
_globals['_LOOKUPMODIFIER']._serialized_start=1224
_globals['_LOOKUPMODIFIER']._serialized_end=1292
_globals['_CANCELINVOICEMSG']._serialized_start=48
_globals['_CANCELINVOICEMSG']._serialized_end=88
_globals['_CANCELINVOICERESP']._serialized_start=90
_globals['_CANCELINVOICERESP']._serialized_end=109
_globals['_ADDHOLDINVOICEREQUEST']._serialized_start=112
_globals['_ADDHOLDINVOICEREQUEST']._serialized_end=340
_globals['_ADDHOLDINVOICERESP']._serialized_start=342
_globals['_ADDHOLDINVOICERESP']._serialized_end=428
_globals['_SETTLEINVOICEMSG']._serialized_start=430
_globals['_SETTLEINVOICEMSG']._serialized_end=466
_globals['_SETTLEINVOICERESP']._serialized_start=468
_globals['_SETTLEINVOICERESP']._serialized_end=487
_globals['_SUBSCRIBESINGLEINVOICEREQUEST']._serialized_start=489
_globals['_SUBSCRIBESINGLEINVOICEREQUEST']._serialized_end=542
_globals['_LOOKUPINVOICEMSG']._serialized_start=545
_globals['_LOOKUPINVOICEMSG']._serialized_end=698
_globals['_CIRCUITKEY']._serialized_start=700
_globals['_CIRCUITKEY']._serialized_end=746
_globals['_HTLCMODIFYREQUEST']._serialized_start=749
_globals['_HTLCMODIFYREQUEST']._serialized_end=1098
_globals['_HTLCMODIFYREQUEST_EXITHTLCWIRECUSTOMRECORDSENTRY']._serialized_start=1034
_globals['_HTLCMODIFYREQUEST_EXITHTLCWIRECUSTOMRECORDSENTRY']._serialized_end=1098
_globals['_HTLCMODIFYRESPONSE']._serialized_start=1100
_globals['_HTLCMODIFYRESPONSE']._serialized_end=1222
_globals['_INVOICES']._serialized_start=1295
_globals['_INVOICES']._serialized_end=1791
# @@protoc_insertion_point(module_scope)
@@ -1,392 +0,0 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc
import warnings
import lnbits.wallets.lnd_grpc_files.invoices_pb2 as invoices__pb2
import lnbits.wallets.lnd_grpc_files.lightning_pb2 as lightning__pb2
GRPC_GENERATED_VERSION = '1.68.1'
GRPC_VERSION = grpc.__version__
_version_not_supported = False
try:
from grpc._utilities import first_version_is_lower
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
except ImportError:
_version_not_supported = True
if _version_not_supported:
raise RuntimeError(
f'The grpc package installed is at version {GRPC_VERSION},'
+ f' but the generated code in invoices_pb2_grpc.py depends on'
+ f' grpcio>={GRPC_GENERATED_VERSION}.'
+ f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
+ f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
)
class InvoicesStub(object):
"""
Comments in this file will be directly parsed into the API
Documentation as descriptions of the associated method, message, or field.
These descriptions should go right above the definition of the object, and
can be in either block or // comment format.
An RPC method can be matched to an lncli command by placing a line in the
beginning of the description in exactly the following format:
lncli: `methodname`
Failure to specify the exact name of the command will cause documentation
generation to fail.
More information on how exactly the gRPC documentation is generated from
this proto file can be found here:
https://github.com/lightninglabs/lightning-api
Invoices is a service that can be used to create, accept, settle and cancel
invoices.
"""
def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.SubscribeSingleInvoice = channel.unary_stream(
'/invoicesrpc.Invoices/SubscribeSingleInvoice',
request_serializer=invoices__pb2.SubscribeSingleInvoiceRequest.SerializeToString,
response_deserializer=lightning__pb2.Invoice.FromString,
_registered_method=True)
self.CancelInvoice = channel.unary_unary(
'/invoicesrpc.Invoices/CancelInvoice',
request_serializer=invoices__pb2.CancelInvoiceMsg.SerializeToString,
response_deserializer=invoices__pb2.CancelInvoiceResp.FromString,
_registered_method=True)
self.AddHoldInvoice = channel.unary_unary(
'/invoicesrpc.Invoices/AddHoldInvoice',
request_serializer=invoices__pb2.AddHoldInvoiceRequest.SerializeToString,
response_deserializer=invoices__pb2.AddHoldInvoiceResp.FromString,
_registered_method=True)
self.SettleInvoice = channel.unary_unary(
'/invoicesrpc.Invoices/SettleInvoice',
request_serializer=invoices__pb2.SettleInvoiceMsg.SerializeToString,
response_deserializer=invoices__pb2.SettleInvoiceResp.FromString,
_registered_method=True)
self.LookupInvoiceV2 = channel.unary_unary(
'/invoicesrpc.Invoices/LookupInvoiceV2',
request_serializer=invoices__pb2.LookupInvoiceMsg.SerializeToString,
response_deserializer=lightning__pb2.Invoice.FromString,
_registered_method=True)
self.HtlcModifier = channel.stream_stream(
'/invoicesrpc.Invoices/HtlcModifier',
request_serializer=invoices__pb2.HtlcModifyResponse.SerializeToString,
response_deserializer=invoices__pb2.HtlcModifyRequest.FromString,
_registered_method=True)
class InvoicesServicer(object):
"""
Comments in this file will be directly parsed into the API
Documentation as descriptions of the associated method, message, or field.
These descriptions should go right above the definition of the object, and
can be in either block or // comment format.
An RPC method can be matched to an lncli command by placing a line in the
beginning of the description in exactly the following format:
lncli: `methodname`
Failure to specify the exact name of the command will cause documentation
generation to fail.
More information on how exactly the gRPC documentation is generated from
this proto file can be found here:
https://github.com/lightninglabs/lightning-api
Invoices is a service that can be used to create, accept, settle and cancel
invoices.
"""
def SubscribeSingleInvoice(self, request, context):
"""
SubscribeSingleInvoice returns a uni-directional stream (server -> client)
to notify the client of state transitions of the specified invoice.
Initially the current invoice state is always sent out.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def CancelInvoice(self, request, context):
"""lncli: `cancelinvoice`
CancelInvoice cancels a currently open invoice. If the invoice is already
canceled, this call will succeed. If the invoice is already settled, it will
fail.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def AddHoldInvoice(self, request, context):
"""lncli: `addholdinvoice`
AddHoldInvoice creates a hold invoice. It ties the invoice to the hash
supplied in the request.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def SettleInvoice(self, request, context):
"""lncli: `settleinvoice`
SettleInvoice settles an accepted invoice. If the invoice is already
settled, this call will succeed.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def LookupInvoiceV2(self, request, context):
"""
LookupInvoiceV2 attempts to look up at invoice. An invoice can be referenced
using either its payment hash, payment address, or set ID.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def HtlcModifier(self, request_iterator, context):
"""
HtlcModifier is a bidirectional streaming RPC that allows a client to
intercept and modify the HTLCs that attempt to settle the given invoice. The
server will send HTLCs of invoices to the client and the client can modify
some aspects of the HTLC in order to pass the invoice acceptance tests.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def add_InvoicesServicer_to_server(servicer, server):
rpc_method_handlers = {
'SubscribeSingleInvoice': grpc.unary_stream_rpc_method_handler(
servicer.SubscribeSingleInvoice,
request_deserializer=invoices__pb2.SubscribeSingleInvoiceRequest.FromString,
response_serializer=lightning__pb2.Invoice.SerializeToString,
),
'CancelInvoice': grpc.unary_unary_rpc_method_handler(
servicer.CancelInvoice,
request_deserializer=invoices__pb2.CancelInvoiceMsg.FromString,
response_serializer=invoices__pb2.CancelInvoiceResp.SerializeToString,
),
'AddHoldInvoice': grpc.unary_unary_rpc_method_handler(
servicer.AddHoldInvoice,
request_deserializer=invoices__pb2.AddHoldInvoiceRequest.FromString,
response_serializer=invoices__pb2.AddHoldInvoiceResp.SerializeToString,
),
'SettleInvoice': grpc.unary_unary_rpc_method_handler(
servicer.SettleInvoice,
request_deserializer=invoices__pb2.SettleInvoiceMsg.FromString,
response_serializer=invoices__pb2.SettleInvoiceResp.SerializeToString,
),
'LookupInvoiceV2': grpc.unary_unary_rpc_method_handler(
servicer.LookupInvoiceV2,
request_deserializer=invoices__pb2.LookupInvoiceMsg.FromString,
response_serializer=lightning__pb2.Invoice.SerializeToString,
),
'HtlcModifier': grpc.stream_stream_rpc_method_handler(
servicer.HtlcModifier,
request_deserializer=invoices__pb2.HtlcModifyResponse.FromString,
response_serializer=invoices__pb2.HtlcModifyRequest.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
'invoicesrpc.Invoices', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))
server.add_registered_method_handlers('invoicesrpc.Invoices', rpc_method_handlers)
# This class is part of an EXPERIMENTAL API.
class Invoices(object):
"""
Comments in this file will be directly parsed into the API
Documentation as descriptions of the associated method, message, or field.
These descriptions should go right above the definition of the object, and
can be in either block or // comment format.
An RPC method can be matched to an lncli command by placing a line in the
beginning of the description in exactly the following format:
lncli: `methodname`
Failure to specify the exact name of the command will cause documentation
generation to fail.
More information on how exactly the gRPC documentation is generated from
this proto file can be found here:
https://github.com/lightninglabs/lightning-api
Invoices is a service that can be used to create, accept, settle and cancel
invoices.
"""
@staticmethod
def SubscribeSingleInvoice(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_stream(
request,
target,
'/invoicesrpc.Invoices/SubscribeSingleInvoice',
invoices__pb2.SubscribeSingleInvoiceRequest.SerializeToString,
lightning__pb2.Invoice.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def CancelInvoice(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/invoicesrpc.Invoices/CancelInvoice',
invoices__pb2.CancelInvoiceMsg.SerializeToString,
invoices__pb2.CancelInvoiceResp.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def AddHoldInvoice(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/invoicesrpc.Invoices/AddHoldInvoice',
invoices__pb2.AddHoldInvoiceRequest.SerializeToString,
invoices__pb2.AddHoldInvoiceResp.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def SettleInvoice(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/invoicesrpc.Invoices/SettleInvoice',
invoices__pb2.SettleInvoiceMsg.SerializeToString,
invoices__pb2.SettleInvoiceResp.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def LookupInvoiceV2(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/invoicesrpc.Invoices/LookupInvoiceV2',
invoices__pb2.LookupInvoiceMsg.SerializeToString,
lightning__pb2.Invoice.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def HtlcModifier(request_iterator,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.stream_stream(
request_iterator,
target,
'/invoicesrpc.Invoices/HtlcModifier',
invoices__pb2.HtlcModifyResponse.SerializeToString,
invoices__pb2.HtlcModifyRequest.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+8 -76
View File
@@ -8,18 +8,15 @@ from typing import Optional
import grpc
from loguru import logger
import lnbits.wallets.lnd_grpc_files.invoices_pb2 as invoices
import lnbits.wallets.lnd_grpc_files.invoices_pb2_grpc as invoicesrpc
import lnbits.wallets.lnd_grpc_files.lightning_pb2 as ln
import lnbits.wallets.lnd_grpc_files.lightning_pb2_grpc as lnrpc
import lnbits.wallets.lnd_grpc_files.router_pb2 as router
import lnbits.wallets.lnd_grpc_files.router_pb2_grpc as routerrpc
from lnbits.helpers import normalize_endpoint
from lnbits.settings import settings
from lnbits.utils.crypto import random_secret_and_hash
from lnbits.wallets.lnd_grpc_files.router_pb2_grpc import RouterStub
from .base import (
Feature,
InvoiceResponse,
PaymentFailedStatus,
PaymentPendingStatus,
@@ -65,8 +62,6 @@ environ["GRPC_SSL_CIPHER_SUITES"] = "HIGH+ECDSA"
class LndWallet(Wallet):
features = [Feature.holdinvoice]
def __init__(self):
if not settings.lnd_grpc_endpoint:
raise ValueError("cannot initialize LndWallet: missing lnd_grpc_endpoint")
@@ -103,8 +98,7 @@ class LndWallet(Wallet):
f"{self.endpoint}:{self.port}", composite_creds
)
self.rpc = lnrpc.LightningStub(channel)
self.routerpc = RouterStub(channel)
self.invoicesrpc = invoicesrpc.InvoicesStub(channel)
self.routerpc = routerrpc.RouterStub(channel)
def metadata_callback(self, _, callback):
callback([("macaroon", self.macaroon)], None)
@@ -114,7 +108,7 @@ class LndWallet(Wallet):
async def status(self) -> StatusResponse:
try:
resp = await self.rpc.ChannelBalance(ln.ChannelBalanceRequest()) # type: ignore
resp = await self.rpc.ChannelBalance(ln.ChannelBalanceRequest())
except Exception as exc:
return StatusResponse(f"Unable to connect, got: '{exc}'", 0)
@@ -150,7 +144,7 @@ class LndWallet(Wallet):
data["r_hash"] = bytes.fromhex(payment_hash)
data["r_preimage"] = bytes.fromhex(preimage)
try:
req = ln.Invoice(**data) # type: ignore
req = ln.Invoice(**data)
resp = await self.rpc.AddInvoice(req)
# response model
# {
@@ -174,7 +168,7 @@ class LndWallet(Wallet):
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
# fee_limit_fixed = ln.FeeLimit(fixed=fee_limit_msat // 1000)
req = router.SendPaymentRequest( # type: ignore
req = router.SendPaymentRequest(
payment_request=bolt11,
fee_limit_msat=fee_limit_msat,
timeout_seconds=30,
@@ -233,7 +227,7 @@ class LndWallet(Wallet):
# that use different checking_id formats
raise ValueError
resp = await self.rpc.LookupInvoice(ln.PaymentHash(r_hash=r_hash)) # type: ignore
resp = await self.rpc.LookupInvoice(ln.PaymentHash(r_hash=r_hash))
if resp.settled:
return PaymentSuccessStatus(preimage=resp.r_preimage.hex())
@@ -277,7 +271,7 @@ class LndWallet(Wallet):
try:
resp = self.routerpc.TrackPaymentV2(
router.TrackPaymentRequest(payment_hash=r_hash) # type: ignore
router.TrackPaymentRequest(payment_hash=r_hash)
)
async for payment in resp:
if len(payment.htlcs) and statuses[payment.status]:
@@ -294,7 +288,7 @@ class LndWallet(Wallet):
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
while settings.lnbits_running:
try:
request = ln.InvoiceSubscription() # type: ignore
request = ln.InvoiceSubscription()
async for i in self.rpc.SubscribeInvoices(request):
if not i.settled:
continue
@@ -307,65 +301,3 @@ class LndWallet(Wallet):
"retrying in 5 seconds"
)
await asyncio.sleep(5)
async def create_hold_invoice(
self,
amount: int,
payment_hash: str,
memo: Optional[str] = None,
description_hash: Optional[bytes] = None,
unhashed_description: Optional[bytes] = None,
**kwargs,
) -> InvoiceResponse:
data: dict = {
"description_hash": b"",
"value": amount,
"hash": hex_to_bytes(payment_hash),
"private": True,
"memo": memo or "",
}
if kwargs.get("expiry"):
data["expiry"] = kwargs["expiry"]
if description_hash:
data["description_hash"] = description_hash
elif unhashed_description:
data["description_hash"] = sha256(unhashed_description).digest()
try:
req = invoices.AddHoldInvoiceRequest(**data) # type: ignore
res = await self.invoicesrpc.AddHoldInvoice(req)
logger.debug(f"AddHoldInvoice response: {res}")
except Exception as exc:
logger.warning(exc)
error_message = str(exc)
return InvoiceResponse(ok=False, error_message=error_message)
return InvoiceResponse(
ok=True, checking_id=payment_hash, payment_request=str(res.payment_request)
)
async def settle_hold_invoice(self, preimage: str) -> InvoiceResponse:
try:
req = invoices.SettleInvoiceMsg(preimage=hex_to_bytes(preimage)) # type: ignore
await self.invoicesrpc.SettleInvoice(req)
except grpc.aio.AioRpcError as exc:
return InvoiceResponse(
ok=False, error_message=exc.details() or "unknown grpc exception"
)
except Exception as exc:
logger.warning(exc)
return InvoiceResponse(ok=False, error_message=str(exc))
return InvoiceResponse(ok=True, preimage=preimage)
async def cancel_hold_invoice(self, payment_hash: str) -> InvoiceResponse:
try:
req = invoices.CancelInvoiceMsg(payment_hash=hex_to_bytes(payment_hash)) # type: ignore
res = await self.invoicesrpc.CancelInvoice(req)
logger.debug(f"CancelInvoice response: {res}")
except Exception as exc:
logger.warning(exc)
# If we cannot cancel the invoice, we return an error message
# and True for ok that should be ignored by the service
return InvoiceResponse(
ok=False, checking_id=payment_hash, error_message=str(exc)
)
# If we reach here, the invoice was successfully canceled and payment failed
return InvoiceResponse(True, checking_id=payment_hash)
+1 -77
View File
@@ -14,7 +14,6 @@ from lnbits.settings import settings
from lnbits.utils.crypto import random_secret_and_hash
from .base import (
Feature,
InvoiceResponse,
PaymentFailedStatus,
PaymentPendingStatus,
@@ -28,10 +27,9 @@ from .macaroon import load_macaroon
class LndRestWallet(Wallet):
"""https://api.lightning.community/#lnd-rest-api-reference"""
"""https://api.lightning.community/rest/index.html#lnd-rest-api-reference"""
__node_cls__ = LndRestNode
features = [Feature.nodemanager, Feature.holdinvoice]
def __init__(self):
if not settings.lnd_rest_endpoint:
@@ -322,77 +320,3 @@ class LndRestWallet(Wallet):
" seconds"
)
await asyncio.sleep(5)
async def create_hold_invoice(
self,
amount: int,
payment_hash: str,
memo: Optional[str] = None,
description_hash: Optional[bytes] = None,
unhashed_description: Optional[bytes] = None,
**_,
) -> InvoiceResponse:
data: dict = {
"value": amount,
"private": True,
"hash": base64.b64encode(bytes.fromhex(payment_hash)).decode("ascii"),
}
if description_hash:
data["description_hash"] = base64.b64encode(description_hash).decode(
"ascii"
)
elif unhashed_description:
data["description_hash"] = base64.b64encode(
hashlib.sha256(unhashed_description).digest()
).decode("ascii")
else:
data["memo"] = memo or ""
try:
r = await self.client.post(url="/v2/invoices/hodl", json=data)
r.raise_for_status()
data = r.json()
except httpx.HTTPStatusError as exc:
logger.warning(exc)
return InvoiceResponse(ok=False, error_message=exc.response.text)
except Exception as exc:
logger.error(exc)
return InvoiceResponse(ok=False, error_message=str(exc))
payment_request = data["payment_request"]
payment_hash = base64.b64encode(bytes.fromhex(payment_hash)).decode("ascii")
return InvoiceResponse(
ok=True, checking_id=payment_hash, payment_request=payment_request
)
async def settle_hold_invoice(self, preimage: str) -> InvoiceResponse:
data: dict = {
"preimage": base64.b64encode(bytes.fromhex(preimage)).decode("ascii")
}
try:
r = await self.client.post(url="/v2/invoices/settle", json=data)
r.raise_for_status()
return InvoiceResponse(ok=True, preimage=preimage)
except httpx.HTTPStatusError as exc:
logger.warning(exc)
return InvoiceResponse(ok=False, error_message=exc.response.text)
except Exception as exc:
logger.error(exc)
return InvoiceResponse(ok=False, error_message=str(exc))
async def cancel_hold_invoice(self, payment_hash: str) -> InvoiceResponse:
rhash = bytes.fromhex(payment_hash)
try:
r = await self.client.post(
url="/v2/invoices/cancel",
json={"payment_hash": base64.b64encode(rhash).decode("ascii")},
)
r.raise_for_status()
logger.debug(f"Cancel hold invoice response: {r.text}")
return InvoiceResponse(ok=True, checking_id=payment_hash)
except httpx.HTTPStatusError as exc:
return InvoiceResponse(ok=False, error_message=exc.response.text)
except Exception as exc:
logger.error(exc)
return InvoiceResponse(ok=False, error_message=str(exc))
+1 -1
View File
@@ -10,7 +10,7 @@ from urllib.parse import parse_qs, unquote, urlparse
import secp256k1
from bolt11 import decode as bolt11_decode
from loguru import logger
from websockets import connect as ws_connect
from websockets.legacy.client import connect as ws_connect
from lnbits.settings import settings
from lnbits.utils.nostr import (
+2 -4
View File
@@ -9,7 +9,7 @@ from typing import Any, Optional
import httpx
from httpx import RequestError, TimeoutException
from loguru import logger
from websockets import connect
from websockets.legacy.client import connect
from lnbits.helpers import normalize_endpoint
from lnbits.settings import settings
@@ -300,9 +300,7 @@ class PhoenixdWallet(Wallet):
try:
async with connect(
self.ws_url,
additional_headers=[
("Authorization", self.headers["Authorization"])
],
extra_headers=[("Authorization", self.headers["Authorization"])],
) as ws:
logger.info("connected to phoenixd invoices stream")
while settings.lnbits_running:
+18 -18
View File
@@ -15,7 +15,7 @@
"showdown": "^2.1.0",
"underscore": "^1.13.7",
"vue": "3.5.8",
"vue-i18n": "^10.0.8",
"vue-i18n": "^10.0.6",
"vue-qrcode-reader": "^5.5.10",
"vue-router": "4.4.5",
"vuex": "4.1.0"
@@ -72,13 +72,13 @@
}
},
"node_modules/@intlify/core-base": {
"version": "10.0.8",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-10.0.8.tgz",
"integrity": "sha512-FoHslNWSoHjdUBLy35bpm9PV/0LVI/DSv9L6Km6J2ad8r/mm0VaGg06C40FqlE8u2ADcGUM60lyoU7Myo4WNZQ==",
"version": "10.0.6",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-10.0.6.tgz",
"integrity": "sha512-/NINGvy7t8qSCyyuqMIPmHS6CBQjqPIPVOps0Rb7xWrwwkwHJKtahiFnW1HC4iQVhzoYwEW6Js0923zTScLDiA==",
"license": "MIT",
"dependencies": {
"@intlify/message-compiler": "10.0.8",
"@intlify/shared": "10.0.8"
"@intlify/message-compiler": "10.0.6",
"@intlify/shared": "10.0.6"
},
"engines": {
"node": ">= 16"
@@ -88,12 +88,12 @@
}
},
"node_modules/@intlify/message-compiler": {
"version": "10.0.8",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-10.0.8.tgz",
"integrity": "sha512-DV+sYXIkHVd5yVb2mL7br/NEUwzUoLBsMkV3H0InefWgmYa34NLZUvMCGi5oWX+Hqr2Y2qUxnVrnOWF4aBlgWg==",
"version": "10.0.6",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-10.0.6.tgz",
"integrity": "sha512-QcUYprK+e4X2lU6eJDxLuf/mUtCuVPj2RFBoFRlJJxK3wskBejzlRvh1Q0lQCi9tDOnD4iUK1ftcGylE3X3idA==",
"license": "MIT",
"dependencies": {
"@intlify/shared": "10.0.8",
"@intlify/shared": "10.0.6",
"source-map-js": "^1.0.2"
},
"engines": {
@@ -104,9 +104,9 @@
}
},
"node_modules/@intlify/shared": {
"version": "10.0.8",
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-10.0.8.tgz",
"integrity": "sha512-BcmHpb5bQyeVNrptC3UhzpBZB/YHHDoEREOUERrmF2BRxsyOEuRrq+Z96C/D4+2KJb8kuHiouzAei7BXlG0YYw==",
"version": "10.0.6",
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-10.0.6.tgz",
"integrity": "sha512-2xqwm05YPpo7TM//+v0bzS0FWiTzsjpSMnWdt7ZXs5/ZfQIedSuBXIrskd8HZ7c/cZzo1G9ALHTksnv/74vk/Q==",
"license": "MIT",
"engines": {
"node": ">= 16"
@@ -1305,13 +1305,13 @@
}
},
"node_modules/vue-i18n": {
"version": "10.0.8",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-10.0.8.tgz",
"integrity": "sha512-mIjy4utxMz9lMMo6G9vYePv7gUFt4ztOMhY9/4czDJxZ26xPeJ49MAGa9wBAE3XuXbYCrtVPmPxNjej7JJJkZQ==",
"version": "10.0.6",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-10.0.6.tgz",
"integrity": "sha512-pQPspK5H4srzlu+47+HEY2tmiY3GyYIvSPgSBdQaYVWv7t1zj1t9p1FvHlxBXyJ17t9stG/Vxj+pykrvPWBLeQ==",
"license": "MIT",
"dependencies": {
"@intlify/core-base": "10.0.8",
"@intlify/shared": "10.0.8",
"@intlify/core-base": "10.0.6",
"@intlify/shared": "10.0.6",
"@vue/devtools-api": "^6.5.0"
},
"engines": {
+1 -1
View File
@@ -29,7 +29,7 @@
"nostr-tools": "^2.7.2",
"underscore": "^1.13.7",
"vue": "3.5.8",
"vue-i18n": "^10.0.8",
"vue-i18n": "^10.0.6",
"vue-qrcode-reader": "^5.5.10",
"vue-router": "4.4.5",
"vuex": "4.1.0"
Generated
+83 -156
View File
@@ -378,71 +378,6 @@ files = [
[package.extras]
tests = ["PyHamcrest (>=2.0.2)", "mypy", "pytest (>=4.6)", "pytest-benchmark", "pytest-cov", "pytest-flake8"]
[[package]]
name = "bcrypt"
version = "4.3.0"
description = "Modern password hashing for your software and your servers"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "bcrypt-4.3.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281"},
{file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb"},
{file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180"},
{file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f"},
{file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09"},
{file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d"},
{file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd"},
{file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af"},
{file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231"},
{file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c"},
{file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f"},
{file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d"},
{file = "bcrypt-4.3.0-cp313-cp313t-win32.whl", hash = "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4"},
{file = "bcrypt-4.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669"},
{file = "bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d"},
{file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b"},
{file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e"},
{file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59"},
{file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753"},
{file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761"},
{file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb"},
{file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d"},
{file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f"},
{file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732"},
{file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef"},
{file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304"},
{file = "bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51"},
{file = "bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62"},
{file = "bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3"},
{file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24"},
{file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef"},
{file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b"},
{file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676"},
{file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1"},
{file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe"},
{file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0"},
{file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f"},
{file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23"},
{file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe"},
{file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505"},
{file = "bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a"},
{file = "bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b"},
{file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c950d682f0952bafcceaf709761da0a32a942272fad381081b51096ffa46cea1"},
{file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:107d53b5c67e0bbc3f03ebf5b030e0403d24dda980f8e244795335ba7b4a027d"},
{file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b693dbb82b3c27a1604a3dff5bfc5418a7e6a781bb795288141e5f80cf3a3492"},
{file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:b6354d3760fcd31994a14c89659dee887f1351a06e5dac3c1142307172a79f90"},
{file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a839320bf27d474e52ef8cb16449bb2ce0ba03ca9f44daba6d93fa1d8828e48a"},
{file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:bdc6a24e754a555d7316fa4774e64c6c3997d27ed2d1964d55920c7c227bc4ce"},
{file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:55a935b8e9a1d2def0626c4269db3fcd26728cbff1e84f0341465c31c4ee56d8"},
{file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:57967b7a28d855313a963aaea51bf6df89f833db4320da458e5b3c5ab6d4c938"},
{file = "bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18"},
]
[package.extras]
tests = ["pytest (>=3.2.1,!=3.3.0)"]
typecheck = ["mypy"]
[[package]]
name = "bech32"
version = "1.2.0"
@@ -455,21 +390,6 @@ files = [
{file = "bech32-1.2.0.tar.gz", hash = "sha256:7d6db8214603bd7871fcfa6c0826ef68b85b0abd90fa21c285a9c5e21d2bd899"},
]
[[package]]
name = "bip32"
version = "4.0"
description = "Minimalistic implementation of the BIP32 key derivation scheme"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "bip32-4.0-py3-none-any.whl", hash = "sha256:9728b38336129c00e1f870bbb3e328c9632d51c1bddeef4011fd3115cb3aeff9"},
{file = "bip32-4.0.tar.gz", hash = "sha256:8035588f252f569bb414bc60df151ae431fc1c6789a19488a32890532ef3a2fc"},
]
[package.dependencies]
coincurve = ">=15.0,<21"
[[package]]
name = "bitarray"
version = "3.4.3"
@@ -1371,25 +1291,24 @@ test = ["pytest (>=6)"]
[[package]]
name = "fastapi"
version = "0.116.1"
version = "0.115.13"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "fastapi-0.116.1-py3-none-any.whl", hash = "sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565"},
{file = "fastapi-0.116.1.tar.gz", hash = "sha256:ed52cbf946abfd70c5a0dccb24673f0670deeb517a88b3544d03c2a6bf283143"},
{file = "fastapi-0.115.13-py3-none-any.whl", hash = "sha256:0a0cab59afa7bab22f5eb347f8c9864b681558c278395e94035a741fc10cd865"},
{file = "fastapi-0.115.13.tar.gz", hash = "sha256:55d1d25c2e1e0a0a50aceb1c8705cd932def273c102bff0b1c1da88b3c6eb307"},
]
[package.dependencies]
pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0"
starlette = ">=0.40.0,<0.48.0"
starlette = ">=0.40.0,<0.47.0"
typing-extensions = ">=4.8.0"
[package.extras]
all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
[[package]]
name = "fastapi-sso"
@@ -2133,23 +2052,21 @@ valkey = ["valkey (>=6)"]
[[package]]
name = "lnurl"
version = "0.6.8"
version = "0.5.3"
description = "LNURL implementation for Python."
optional = false
python-versions = ">=3.10"
python-versions = "<4.0,>=3.9"
groups = ["main"]
files = [
{file = "lnurl-0.6.8-py3-none-any.whl", hash = "sha256:4fff53efcdd401cf4169676bd1ab85e9e241a762a9a5407ee11e6a6e120e8279"},
{file = "lnurl-0.6.8.tar.gz", hash = "sha256:de64a47179980a4b52cd6b89ad377cda14502f1998f53724490683f6f5c4ed90"},
{file = "lnurl-0.5.3-py3-none-any.whl", hash = "sha256:feaf6c60b0b7f104894ef3accbd30d23d52e038c2797c58432baea7f4a8aa952"},
{file = "lnurl-0.5.3.tar.gz", hash = "sha256:60154bcfdbb98fb143eeca970a16d73a582f28e057a826b5f222259411c497fe"},
]
[package.dependencies]
bech32 = "*"
bip32 = ">=4.0,<5.0"
bolt11 = "*"
ecdsa = "*"
httpx = "*"
pycryptodomex = ">=3.21.0,<4.0.0"
bech32 = ">=1.2.0,<2.0.0"
bolt11 = ">=2.0.5,<3.0.0"
ecdsa = ">=0.19.0,<0.20.0"
httpx = ">=0.27.0,<0.28.0"
pydantic = ">=1,<2"
[[package]]
@@ -2622,6 +2539,24 @@ files = [
{file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"},
]
[[package]]
name = "passlib"
version = "1.7.4"
description = "comprehensive password hashing framework supporting over 30 schemes"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"},
{file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"},
]
[package.extras]
argon2 = ["argon2-cffi (>=18.2.0)"]
bcrypt = ["bcrypt (>=3.1.0)"]
build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"]
totp = ["cryptography"]
[[package]]
name = "pathable"
version = "0.4.4"
@@ -2998,62 +2933,55 @@ files = [
[[package]]
name = "pydantic"
version = "1.10.22"
version = "1.10.18"
description = "Data validation and settings management using python type hints"
optional = false
python-versions = ">=3.7"
groups = ["main", "dev"]
files = [
{file = "pydantic-1.10.22-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:57889565ccc1e5b7b73343329bbe6198ebc472e3ee874af2fa1865cfe7048228"},
{file = "pydantic-1.10.22-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:90729e22426de79bc6a3526b4c45ec4400caf0d4f10d7181ba7f12c01bb3897d"},
{file = "pydantic-1.10.22-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8684d347f351554ec94fdcb507983d3116dc4577fb8799fed63c65869a2d10"},
{file = "pydantic-1.10.22-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c8dad498ceff2d9ef1d2e2bc6608f5b59b8e1ba2031759b22dfb8c16608e1802"},
{file = "pydantic-1.10.22-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fac529cc654d4575cf8de191cce354b12ba705f528a0a5c654de6d01f76cd818"},
{file = "pydantic-1.10.22-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4148232aded8dd1dd13cf910a01b32a763c34bd79a0ab4d1ee66164fcb0b7b9d"},
{file = "pydantic-1.10.22-cp310-cp310-win_amd64.whl", hash = "sha256:ece68105d9e436db45d8650dc375c760cc85a6793ae019c08769052902dca7db"},
{file = "pydantic-1.10.22-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8e530a8da353f791ad89e701c35787418605d35085f4bdda51b416946070e938"},
{file = "pydantic-1.10.22-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:654322b85642e9439d7de4c83cb4084ddd513df7ff8706005dada43b34544946"},
{file = "pydantic-1.10.22-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8bece75bd1b9fc1c32b57a32831517943b1159ba18b4ba32c0d431d76a120ae"},
{file = "pydantic-1.10.22-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eccb58767f13c6963dcf96d02cb8723ebb98b16692030803ac075d2439c07b0f"},
{file = "pydantic-1.10.22-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7778e6200ff8ed5f7052c1516617423d22517ad36cc7a3aedd51428168e3e5e8"},
{file = "pydantic-1.10.22-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bffe02767d27c39af9ca7dc7cd479c00dda6346bb62ffc89e306f665108317a2"},
{file = "pydantic-1.10.22-cp311-cp311-win_amd64.whl", hash = "sha256:23bc19c55427091b8e589bc08f635ab90005f2dc99518f1233386f46462c550a"},
{file = "pydantic-1.10.22-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:92d0f97828a075a71d9efc65cf75db5f149b4d79a38c89648a63d2932894d8c9"},
{file = "pydantic-1.10.22-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6af5a2811b6b95b58b829aeac5996d465a5f0c7ed84bd871d603cf8646edf6ff"},
{file = "pydantic-1.10.22-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6cf06d8d40993e79af0ab2102ef5da77b9ddba51248e4cb27f9f3f591fbb096e"},
{file = "pydantic-1.10.22-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:184b7865b171a6057ad97f4a17fbac81cec29bd103e996e7add3d16b0d95f609"},
{file = "pydantic-1.10.22-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:923ad861677ab09d89be35d36111156063a7ebb44322cdb7b49266e1adaba4bb"},
{file = "pydantic-1.10.22-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:82d9a3da1686443fb854c8d2ab9a473251f8f4cdd11b125522efb4d7c646e7bc"},
{file = "pydantic-1.10.22-cp312-cp312-win_amd64.whl", hash = "sha256:1612604929af4c602694a7f3338b18039d402eb5ddfbf0db44f1ebfaf07f93e7"},
{file = "pydantic-1.10.22-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b259dc89c9abcd24bf42f31951fb46c62e904ccf4316393f317abeeecda39978"},
{file = "pydantic-1.10.22-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9238aa0964d80c0908d2f385e981add58faead4412ca80ef0fa352094c24e46d"},
{file = "pydantic-1.10.22-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f8029f05b04080e3f1a550575a1bca747c0ea4be48e2d551473d47fd768fc1b"},
{file = "pydantic-1.10.22-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5c06918894f119e0431a36c9393bc7cceeb34d1feeb66670ef9b9ca48c073937"},
{file = "pydantic-1.10.22-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e205311649622ee8fc1ec9089bd2076823797f5cd2c1e3182dc0e12aab835b35"},
{file = "pydantic-1.10.22-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:815f0a73d5688d6dd0796a7edb9eca7071bfef961a7b33f91e618822ae7345b7"},
{file = "pydantic-1.10.22-cp313-cp313-win_amd64.whl", hash = "sha256:9dfce71d42a5cde10e78a469e3d986f656afc245ab1b97c7106036f088dd91f8"},
{file = "pydantic-1.10.22-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3ecaf8177b06aac5d1f442db1288e3b46d9f05f34fd17fdca3ad34105328b61a"},
{file = "pydantic-1.10.22-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb36c2de9ea74bd7f66b5481dea8032d399affd1cbfbb9bb7ce539437f1fce62"},
{file = "pydantic-1.10.22-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6b8d14a256be3b8fff9286d76c532f1a7573fbba5f189305b22471c6679854d"},
{file = "pydantic-1.10.22-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:1c33269e815db4324e71577174c29c7aa30d1bba51340ce6be976f6f3053a4c6"},
{file = "pydantic-1.10.22-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:8661b3ab2735b2a9ccca2634738534a795f4a10bae3ab28ec0a10c96baa20182"},
{file = "pydantic-1.10.22-cp37-cp37m-win_amd64.whl", hash = "sha256:22bdd5fe70d4549995981c55b970f59de5c502d5656b2abdfcd0a25be6f3763e"},
{file = "pydantic-1.10.22-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e3f33d1358aa4bc2795208cc29ff3118aeaad0ea36f0946788cf7cadeccc166b"},
{file = "pydantic-1.10.22-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:813f079f9cd136cac621f3f9128a4406eb8abd2ad9fdf916a0731d91c6590017"},
{file = "pydantic-1.10.22-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab618ab8dca6eac7f0755db25f6aba3c22c40e3463f85a1c08dc93092d917704"},
{file = "pydantic-1.10.22-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d128e1aaa38db88caca920d5822c98fc06516a09a58b6d3d60fa5ea9099b32cc"},
{file = "pydantic-1.10.22-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:cc97bbc25def7025e55fc9016080773167cda2aad7294e06a37dda04c7d69ece"},
{file = "pydantic-1.10.22-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dda5d7157d543b1fa565038cae6e952549d0f90071c839b3740fb77c820fab8"},
{file = "pydantic-1.10.22-cp38-cp38-win_amd64.whl", hash = "sha256:a093fe44fe518cb445d23119511a71f756f8503139d02fcdd1173f7b76c95ffe"},
{file = "pydantic-1.10.22-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ec54c89b2568b258bb30d7348ac4d82bec1b58b377fb56a00441e2ac66b24587"},
{file = "pydantic-1.10.22-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d8f1d1a1532e4f3bcab4e34e8d2197a7def4b67072acd26cfa60e92d75803a48"},
{file = "pydantic-1.10.22-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ad83ca35508c27eae1005b6b61f369f78aae6d27ead2135ec156a2599910121"},
{file = "pydantic-1.10.22-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53cdb44b78c420f570ff16b071ea8cd5a477635c6b0efc343c8a91e3029bbf1a"},
{file = "pydantic-1.10.22-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:16d0a5ae9d98264186ce31acdd7686ec05fd331fab9d68ed777d5cb2d1514e5e"},
{file = "pydantic-1.10.22-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8aee040e25843f036192b1a1af62117504a209a043aa8db12e190bb86ad7e611"},
{file = "pydantic-1.10.22-cp39-cp39-win_amd64.whl", hash = "sha256:7f691eec68dbbfca497d3c11b92a3e5987393174cbedf03ec7a4184c35c2def6"},
{file = "pydantic-1.10.22-py3-none-any.whl", hash = "sha256:343037d608bcbd34df937ac259708bfc83664dadf88afe8516c4f282d7d471a9"},
{file = "pydantic-1.10.22.tar.gz", hash = "sha256:ee1006cebd43a8e7158fb7190bb8f4e2da9649719bff65d0c287282ec38dec6d"},
{file = "pydantic-1.10.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e405ffcc1254d76bb0e760db101ee8916b620893e6edfbfee563b3c6f7a67c02"},
{file = "pydantic-1.10.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e306e280ebebc65040034bff1a0a81fd86b2f4f05daac0131f29541cafd80b80"},
{file = "pydantic-1.10.18-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11d9d9b87b50338b1b7de4ebf34fd29fdb0d219dc07ade29effc74d3d2609c62"},
{file = "pydantic-1.10.18-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b661ce52c7b5e5f600c0c3c5839e71918346af2ef20062705ae76b5c16914cab"},
{file = "pydantic-1.10.18-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c20f682defc9ef81cd7eaa485879ab29a86a0ba58acf669a78ed868e72bb89e0"},
{file = "pydantic-1.10.18-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c5ae6b7c8483b1e0bf59e5f1843e4fd8fd405e11df7de217ee65b98eb5462861"},
{file = "pydantic-1.10.18-cp310-cp310-win_amd64.whl", hash = "sha256:74fe19dda960b193b0eb82c1f4d2c8e5e26918d9cda858cbf3f41dd28549cb70"},
{file = "pydantic-1.10.18-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72fa46abace0a7743cc697dbb830a41ee84c9db8456e8d77a46d79b537efd7ec"},
{file = "pydantic-1.10.18-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef0fe7ad7cbdb5f372463d42e6ed4ca9c443a52ce544472d8842a0576d830da5"},
{file = "pydantic-1.10.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a00e63104346145389b8e8f500bc6a241e729feaf0559b88b8aa513dd2065481"},
{file = "pydantic-1.10.18-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae6fa2008e1443c46b7b3a5eb03800121868d5ab6bc7cda20b5df3e133cde8b3"},
{file = "pydantic-1.10.18-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9f463abafdc92635da4b38807f5b9972276be7c8c5121989768549fceb8d2588"},
{file = "pydantic-1.10.18-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3445426da503c7e40baccefb2b2989a0c5ce6b163679dd75f55493b460f05a8f"},
{file = "pydantic-1.10.18-cp311-cp311-win_amd64.whl", hash = "sha256:467a14ee2183bc9c902579bb2f04c3d3dac00eff52e252850509a562255b2a33"},
{file = "pydantic-1.10.18-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:efbc8a7f9cb5fe26122acba1852d8dcd1e125e723727c59dcd244da7bdaa54f2"},
{file = "pydantic-1.10.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:24a4a159d0f7a8e26bf6463b0d3d60871d6a52eac5bb6a07a7df85c806f4c048"},
{file = "pydantic-1.10.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b74be007703547dc52e3c37344d130a7bfacca7df112a9e5ceeb840a9ce195c7"},
{file = "pydantic-1.10.18-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcb20d4cb355195c75000a49bb4a31d75e4295200df620f454bbc6bdf60ca890"},
{file = "pydantic-1.10.18-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:46f379b8cb8a3585e3f61bf9ae7d606c70d133943f339d38b76e041ec234953f"},
{file = "pydantic-1.10.18-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cbfbca662ed3729204090c4d09ee4beeecc1a7ecba5a159a94b5a4eb24e3759a"},
{file = "pydantic-1.10.18-cp312-cp312-win_amd64.whl", hash = "sha256:c6d0a9f9eccaf7f438671a64acf654ef0d045466e63f9f68a579e2383b63f357"},
{file = "pydantic-1.10.18-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3d5492dbf953d7d849751917e3b2433fb26010d977aa7a0765c37425a4026ff1"},
{file = "pydantic-1.10.18-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe734914977eed33033b70bfc097e1baaffb589517863955430bf2e0846ac30f"},
{file = "pydantic-1.10.18-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15fdbe568beaca9aacfccd5ceadfb5f1a235087a127e8af5e48df9d8a45ae85c"},
{file = "pydantic-1.10.18-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c3e742f62198c9eb9201781fbebe64533a3bbf6a76a91b8d438d62b813079dbc"},
{file = "pydantic-1.10.18-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:19a3bd00b9dafc2cd7250d94d5b578edf7a0bd7daf102617153ff9a8fa37871c"},
{file = "pydantic-1.10.18-cp37-cp37m-win_amd64.whl", hash = "sha256:2ce3fcf75b2bae99aa31bd4968de0474ebe8c8258a0110903478bd83dfee4e3b"},
{file = "pydantic-1.10.18-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:335a32d72c51a313b33fa3a9b0fe283503272ef6467910338e123f90925f0f03"},
{file = "pydantic-1.10.18-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:34a3613c7edb8c6fa578e58e9abe3c0f5e7430e0fc34a65a415a1683b9c32d9a"},
{file = "pydantic-1.10.18-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9ee4e6ca1d9616797fa2e9c0bfb8815912c7d67aca96f77428e316741082a1b"},
{file = "pydantic-1.10.18-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:23e8ec1ce4e57b4f441fc91e3c12adba023fedd06868445a5b5f1d48f0ab3682"},
{file = "pydantic-1.10.18-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:44ae8a3e35a54d2e8fa88ed65e1b08967a9ef8c320819a969bfa09ce5528fafe"},
{file = "pydantic-1.10.18-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5389eb3b48a72da28c6e061a247ab224381435256eb541e175798483368fdd3"},
{file = "pydantic-1.10.18-cp38-cp38-win_amd64.whl", hash = "sha256:069b9c9fc645474d5ea3653788b544a9e0ccd3dca3ad8c900c4c6eac844b4620"},
{file = "pydantic-1.10.18-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:80b982d42515632eb51f60fa1d217dfe0729f008e81a82d1544cc392e0a50ddf"},
{file = "pydantic-1.10.18-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aad8771ec8dbf9139b01b56f66386537c6fe4e76c8f7a47c10261b69ad25c2c9"},
{file = "pydantic-1.10.18-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941a2eb0a1509bd7f31e355912eb33b698eb0051730b2eaf9e70e2e1589cae1d"},
{file = "pydantic-1.10.18-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65f7361a09b07915a98efd17fdec23103307a54db2000bb92095457ca758d485"},
{file = "pydantic-1.10.18-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6951f3f47cb5ca4da536ab161ac0163cab31417d20c54c6de5ddcab8bc813c3f"},
{file = "pydantic-1.10.18-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7a4c5eec138a9b52c67f664c7d51d4c7234c5ad65dd8aacd919fb47445a62c86"},
{file = "pydantic-1.10.18-cp39-cp39-win_amd64.whl", hash = "sha256:49e26c51ca854286bffc22b69787a8d4063a62bf7d83dc21d44d2ff426108518"},
{file = "pydantic-1.10.18-py3-none-any.whl", hash = "sha256:06a189b81ffc52746ec9c8c007f16e5167c8b0a696e1a726369327e3db7b2a82"},
{file = "pydantic-1.10.18.tar.gz", hash = "sha256:baebdff1907d1d96a139c25136a9bb7d17e118f133a76a2ef3b845e831e3403a"},
]
[package.dependencies]
@@ -3888,22 +3816,21 @@ uvicorn = ["uvicorn (>=0.34.0)"]
[[package]]
name = "starlette"
version = "0.47.1"
version = "0.40.0"
description = "The little ASGI library that shines."
optional = false
python-versions = ">=3.9"
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "starlette-0.47.1-py3-none-any.whl", hash = "sha256:5e11c9f5c7c3f24959edbf2dffdc01bba860228acf657129467d8a7468591527"},
{file = "starlette-0.47.1.tar.gz", hash = "sha256:aef012dd2b6be325ffa16698f9dc533614fb1cebd593a906b90dc1025529a79b"},
{file = "starlette-0.40.0-py3-none-any.whl", hash = "sha256:c494a22fae73805376ea6bf88439783ecfba9aac88a43911b48c653437e784c4"},
{file = "starlette-0.40.0.tar.gz", hash = "sha256:1a3139688fb298ce5e2d661d37046a66ad996ce94be4d4983be019a23a04ea35"},
]
[package.dependencies]
anyio = ">=3.6.2,<5"
typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""}
anyio = ">=3.4.0,<5"
[package.extras]
full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"]
full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"]
[[package]]
name = "tlv8"
@@ -4573,4 +4500,4 @@ migration = ["psycopg2-binary"]
[metadata]
lock-version = "2.1"
python-versions = "~3.12 | ~3.11 | ~3.10"
content-hash = "c1714a4df0e8f7d8702fffe9c57b21fc39effb194631741b4718466c18b73d8f"
content-hash = "265fb5ec81a3c703ca7db07dfe206194fbb8616cb6e196d0917f33e6df58753c"
+5 -6
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "lnbits"
version = "1.3.0-rc2"
version = "1.2.0"
description = "LNbits, free and open-source Lightning wallet and accounts system."
authors = ["Alan Bits <alan@lnbits.com>"]
readme = "README.md"
@@ -16,12 +16,11 @@ python = "~3.12 | ~3.11 | ~3.10"
bech32 = "1.2.0"
click = "8.2.1"
ecdsa = "0.19.1"
fastapi = "0.116.1"
starlette = "0.47.1"
fastapi = "0.115.13"
httpx = "0.27.0"
jinja2 = "3.1.6"
lnurl = "0.6.8"
pydantic = "1.10.22"
lnurl = "0.5.3"
pydantic = "1.10.18"
pyqrcode = "1.2.1"
shortuuid = "1.0.13"
sse-starlette = "2.3.6"
@@ -43,6 +42,7 @@ pycryptodomex = "3.23.0"
packaging = "25.0"
bolt11 = "2.1.1"
pyjwt = "2.10.1"
passlib = "1.7.4"
itsdangerous = "2.2.0"
fastapi-sso = "0.18.0"
# needed for boltz, lnurldevice, watchonly extensions
@@ -66,7 +66,6 @@ pynostr = "^0.6.2"
python-multipart = "^0.0.20"
filetype = "^1.2.0"
nostr-sdk = "^0.42.1"
bcrypt = "^4.3.0"
[tool.poetry.extras]
breez = ["breez-sdk", "breez-sdk-liquid"]
+30 -67
View File
@@ -1,6 +1,5 @@
import hashlib
from http import HTTPStatus
from json import JSONDecodeError
from unittest.mock import AsyncMock, Mock
import pytest
@@ -588,14 +587,13 @@ async def test_fiat_tracking(client, adminkey_headers_from, settings: Settings):
"tag": "withdrawRequest",
"callback": "https://example.com/callback",
"k1": "randomk1value",
"minWithdrawable": 1000,
"maxWithdrawable": 1_500_000,
},
{
"status": "OK",
},
{
"status": "OK",
"success": True,
"detail": {"status": "OK"},
},
),
# Error loading LNURL request
@@ -603,35 +601,33 @@ async def test_fiat_tracking(client, adminkey_headers_from, settings: Settings):
"error_loading_lnurl",
None,
{
"status": "ERROR",
"reason": "Error loading callback request",
"success": False,
"detail": "Error loading LNURL request",
},
),
# LNURL response with error status
(
{
"status": "ERROR",
"reason": "LNURL request failed",
},
None,
{
"status": "ERROR",
"reason": "Invalid LNURL-withdraw response.",
"success": False,
"detail": "LNURL request failed",
},
),
# Invalid LNURL-withdraw pay request
# Invalid LNURL-withdraw
(
{
"tag": "payRequest",
"callback": "https://example.com/callback",
"k1": "randomk1value",
"minSendable": 1000,
"maxSendable": 1_500_000,
"metadata": '[["text/plain", "Payment to yo"]]',
},
None,
{
"status": "ERROR",
"reason": "Invalid LNURL-withdraw response.",
"success": False,
"detail": "Invalid LNURL-withdraw",
},
),
# Error loading callback request
@@ -640,13 +636,11 @@ async def test_fiat_tracking(client, adminkey_headers_from, settings: Settings):
"tag": "withdrawRequest",
"callback": "https://example.com/callback",
"k1": "randomk1value",
"minWithdrawable": 1000,
"maxWithdrawable": 1_500_000,
},
"error_loading_callback",
{
"status": "ERROR",
"reason": "Error loading callback request",
"success": False,
"detail": "Error loading callback request",
},
),
# Callback response with error status
@@ -655,16 +649,14 @@ async def test_fiat_tracking(client, adminkey_headers_from, settings: Settings):
"tag": "withdrawRequest",
"callback": "https://example.com/callback",
"k1": "randomk1value",
"minWithdrawable": 1000,
"maxWithdrawable": 1_500_000,
},
{
"status": "ERROR",
"reason": "Callback failed",
},
{
"status": "ERROR",
"reason": "Callback failed",
"success": False,
"detail": "Callback failed",
},
),
# Unexpected exception during LNURL response JSON parsing
@@ -672,8 +664,8 @@ async def test_fiat_tracking(client, adminkey_headers_from, settings: Settings):
"exception_in_lnurl_response_json",
None,
{
"status": "ERROR",
"reason": "Invalid JSON response from https://example.com/lnurl",
"success": False,
"detail": "Unexpected error: Simulated exception",
},
),
],
@@ -685,35 +677,25 @@ async def test_api_payment_pay_with_nfc(
callback_response_data,
expected_response,
):
payment_request = (
"lnbc15u1p3xnhl2pp5jptserfk3zk4qy42tlucycrfwxhydvlemu9pqr93tuzlv9cc7g3sdq"
"svfhkcap3xyhx7un8cqzpgxqzjcsp5f8c52y2stc300gl6s4xswtjpc37hrnnr3c9wvtgjfu"
"vqmpm35evq9qyyssqy4lgd8tj637qcjp05rdpxxykjenthxftej7a2zzmwrmrl70fyj9hvj0"
"rewhzj7jfyuwkwcg9g2jpwtk3wkjtwnkdks84hsnu8xps5vsq4gj5hs"
)
payment_request = "lnbc1..."
lnurl = "lnurlw://example.com/lnurl"
lnurl_data = {"lnurl_w": lnurl}
# Create a mock for httpx.AsyncClient
mock_async_client = AsyncMock()
mock_async_client.__aenter__.return_value = mock_async_client
# Mock the get method
async def mock_get(url, *_, **__):
async def mock_get(url, *args, **kwargs):
if url == "https://example.com/lnurl":
if lnurl_response_data == "error_loading_lnurl":
response = Mock()
response.is_error = True
response.status_code = 500
response.raise_for_status.side_effect = Exception(
"Error loading callback request"
)
return response
elif lnurl_response_data == "exception_in_lnurl_response_json":
response = Mock()
response.is_error = False
response.json.side_effect = JSONDecodeError(
doc="Simulated exception", pos=0, msg="JSONDecodeError"
)
response.json.side_effect = Exception("Simulated exception")
return response
elif isinstance(lnurl_response_data, dict):
response = Mock()
@@ -724,19 +706,11 @@ async def test_api_payment_pay_with_nfc(
# Handle unexpected data
response = Mock()
response.is_error = True
response.status_code = 500
response.raise_for_status.side_effect = Exception(
"Error loading callback request"
)
return response
elif url == "https://example.com/callback":
if callback_response_data == "error_loading_callback":
response = Mock()
response.is_error = True
response.status_code = 500
response.raise_for_status.side_effect = Exception(
"Error loading callback request"
)
return response
elif isinstance(callback_response_data, dict):
response = Mock()
@@ -747,16 +721,10 @@ async def test_api_payment_pay_with_nfc(
# Handle cases where callback is not called
response = Mock()
response.is_error = True
response.raise_for_status.side_effect = Exception(
"Error loading callback request"
)
return response
else:
response = Mock()
response.is_error = True
response.raise_for_status.side_effect = Exception(
"Error loading callback request"
)
return response
mock_async_client.get.side_effect = mock_get
@@ -766,7 +734,7 @@ async def test_api_payment_pay_with_nfc(
response = await client.post(
f"/api/v1/payments/{payment_request}/pay-with-nfc",
json={"lnurl_w": lnurl},
json=lnurl_data,
)
assert response.status_code == HTTPStatus.OK
@@ -775,32 +743,27 @@ async def test_api_payment_pay_with_nfc(
@pytest.mark.anyio
async def test_api_payments_pay_lnurl(client, adminkey_headers_from):
lnurl_data = {
"res": {
"callback": "https://xxxxxxx.lnbits.com",
"minSendable": 1000,
"maxSendable": 1_500_000,
"metadata": '[["text/plain", "Payment to yo"]]',
},
valid_lnurl_data = {
"description_hash": "randomhash",
"callback": "https://xxxxxxx.lnbits.com",
"amount": 1000,
"unit": "sat",
"comment": "test comment",
"description": "test description",
}
invalid_lnurl_data = {**valid_lnurl_data, "callback": "invalid_url"}
# Test with valid callback URL
response = await client.post(
"/api/v1/payments/lnurl", json=lnurl_data, headers=adminkey_headers_from
"/api/v1/payments/lnurl", json=valid_lnurl_data, headers=adminkey_headers_from
)
assert response.status_code == 400
assert (
response.json()["detail"] == "Failed to connect to https://xxxxxxx.lnbits.com."
)
assert response.json()["detail"] == "Failed to connect to xxxxxxx.lnbits.com."
# Test with invalid callback URL
lnurl_data["res"]["callback"] = "invalid-url.lnbits.com"
response = await client.post(
"/api/v1/payments/lnurl", json=lnurl_data, headers=adminkey_headers_from
"/api/v1/payments/lnurl", json=invalid_lnurl_data, headers=adminkey_headers_from
)
assert response.status_code == 400
assert "value_error.url.scheme" in response.json()["detail"]
assert "Callback not allowed." in response.json()["detail"]
-118
View File
@@ -1,118 +0,0 @@
import asyncio
import pytest
from lnbits.core.models import Payment
from lnbits.core.services.payments import (
cancel_hold_invoice,
create_invoice,
get_standalone_payment,
settle_hold_invoice,
)
from lnbits.exceptions import InvoiceError
from lnbits.utils.crypto import random_secret_and_hash
from ..helpers import funding_source, is_fake
from .helpers import pay_real_invoice
@pytest.mark.anyio
@pytest.mark.skipif(is_fake, reason="this only works in regtest")
@pytest.mark.skipif(
funding_source.__class__.__name__ in ["LndRestWallet", "LndWallet"],
reason="this should not raise for lnd",
)
async def test_pay_raise_unsupported(app):
payment_hash = "0" * 32
payment = Payment(
checking_id=payment_hash,
amount=1000,
wallet_id="fake_wallet_id",
bolt11="fake_holdinvoice",
payment_hash=payment_hash,
fee=0,
)
with pytest.raises(InvoiceError):
await create_invoice(
wallet_id="fake_wallet_id",
amount=1000,
memo="fake_holdinvoice",
payment_hash=payment_hash,
)
with pytest.raises(InvoiceError):
await settle_hold_invoice(payment, payment_hash)
with pytest.raises(InvoiceError):
await cancel_hold_invoice(payment)
@pytest.mark.anyio
@pytest.mark.skipif(is_fake, reason="this only works in regtest")
@pytest.mark.skipif(
funding_source.__class__.__name__ not in ["LndRestWallet", "LndWallet"],
reason="this only works for lndrest",
)
async def test_cancel_real_hold_invoice(app, from_wallet):
_, payment_hash = random_secret_and_hash()
payment = await create_invoice(
wallet_id=from_wallet.id,
amount=1000,
memo="test_cancel_holdinvoice",
payment_hash=payment_hash,
)
assert payment.amount == 1000 * 1000
assert payment.memo == "test_cancel_holdinvoice"
assert payment.status == "pending"
assert payment.wallet_id == from_wallet.id
payment = await cancel_hold_invoice(payment=payment)
assert payment.ok is True
updated_payment = await get_standalone_payment(payment_hash, incoming=True)
assert updated_payment
assert updated_payment.status == "failed"
@pytest.mark.anyio
@pytest.mark.skipif(is_fake, reason="this only works in regtest")
@pytest.mark.skipif(
funding_source.__class__.__name__ not in ["LndRestWallet", "LndWallet"],
reason="this only works for lndrest",
)
async def test_settle_real_hold_invoice(app, from_wallet):
preimage, payment_hash = random_secret_and_hash()
payment = await create_invoice(
wallet_id=from_wallet.id,
amount=1000,
memo="test_settle_holdinvoice",
payment_hash=payment_hash,
)
assert payment.amount == 1000 * 1000
assert payment.memo == "test_settle_holdinvoice"
assert payment.status == "pending"
assert payment.wallet_id == from_wallet.id
# invoice should still be open
with pytest.raises(InvoiceError):
await settle_hold_invoice(payment=payment, preimage=preimage)
def pay_invoice():
pay_real_invoice(payment.bolt11)
async def settle():
await asyncio.sleep(1)
invoice_response = await settle_hold_invoice(payment=payment, preimage=preimage)
assert invoice_response.ok is True
coro = asyncio.to_thread(pay_invoice)
task = asyncio.create_task(coro)
await asyncio.gather(task, settle())
await asyncio.sleep(1)
updated_payment = await get_standalone_payment(payment_hash, incoming=True)
assert updated_payment
assert updated_payment.status == "success"
+4 -7
View File
@@ -9,8 +9,7 @@ import secp256k1
from Cryptodome import Random
from Cryptodome.Cipher import AES
from Cryptodome.Util.Padding import pad, unpad
from websockets import ServerConnection
from websockets import serve as ws_serve
from websockets.legacy.server import serve as ws_serve
from lnbits.wallets.nwc import NWCWallet
from tests.wallets.helpers import (
@@ -75,9 +74,7 @@ def sign_event(pub_key, priv_key, event):
return event
async def handle( # noqa: C901
wallet, mock_settings, data, websocket: ServerConnection
):
async def handle(wallet, mock_settings, data, websocket, path): # noqa: C901
async for message in websocket:
if not wallet:
continue
@@ -165,8 +162,8 @@ async def run(data: WalletTest):
if mock_settings is None:
return
def handler(websocket):
return handle(wallet, mock_settings, data, websocket)
async def handler(websocket, path):
return await handle(wallet, mock_settings, data, websocket, path)
if mock_settings is not None:
async with ws_serve(handler, "localhost", mock_settings["port"]) as server: