Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a6ff7ed02 | ||
|
|
335c056a4b |
@@ -306,7 +306,7 @@ async def update_payment_checking_id(
|
||||
await (conn or db).execute(
|
||||
f"""
|
||||
UPDATE apipayments
|
||||
SET checking_id = :new_id, updated_at = {db.timestamp_placeholder("now")}
|
||||
SET checking_id = :new_id, updated_at = {db.timestamp_placeholder('now')}
|
||||
WHERE checking_id = :old_id
|
||||
""", # noqa: S608
|
||||
{
|
||||
@@ -321,15 +321,13 @@ async def update_payment(
|
||||
payment: Payment,
|
||||
new_checking_id: str | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> Payment:
|
||||
) -> None:
|
||||
payment.updated_at = datetime.now(timezone.utc)
|
||||
await (conn or db).update(
|
||||
"apipayments", payment, "WHERE checking_id = :checking_id"
|
||||
)
|
||||
if new_checking_id and new_checking_id != payment.checking_id:
|
||||
await update_payment_checking_id(payment.checking_id, new_checking_id, conn)
|
||||
payment.checking_id = new_checking_id
|
||||
return payment
|
||||
|
||||
|
||||
async def get_payments_history(
|
||||
@@ -401,6 +399,7 @@ async def get_payment_count_stats(
|
||||
user_id: str | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> list[PaymentCountStat]:
|
||||
|
||||
if not filters:
|
||||
filters = Filters()
|
||||
extra_stmts = []
|
||||
@@ -433,6 +432,7 @@ async def get_daily_stats(
|
||||
user_id: str | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> tuple[list[PaymentDailyStats], list[PaymentDailyStats]]:
|
||||
|
||||
if not filters:
|
||||
filters = Filters()
|
||||
|
||||
@@ -482,6 +482,7 @@ async def get_wallets_stats(
|
||||
user_id: str | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> list[PaymentWalletStats]:
|
||||
|
||||
if not filters:
|
||||
filters = Filters()
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ from .payments import (
|
||||
PaymentState,
|
||||
PaymentWalletStats,
|
||||
SettleInvoice,
|
||||
UpdatePaymentExtra,
|
||||
)
|
||||
from .tinyurl import TinyURL
|
||||
from .users import (
|
||||
@@ -91,7 +90,6 @@ __all__ = [
|
||||
"SimpleStatus",
|
||||
"TinyURL",
|
||||
"UpdateBalance",
|
||||
"UpdatePaymentExtra",
|
||||
"UpdateSuperuserPassword",
|
||||
"UpdateUser",
|
||||
"UpdateUserPassword",
|
||||
|
||||
@@ -35,11 +35,6 @@ class PaymentExtra(BaseModel):
|
||||
lnurl_response: str | None = None
|
||||
|
||||
|
||||
class UpdatePaymentExtra(BaseModel):
|
||||
payment_hash: str
|
||||
extra: dict = Field(default_factory=dict)
|
||||
|
||||
|
||||
class PayInvoice(BaseModel):
|
||||
payment_request: str
|
||||
description: str | None = None
|
||||
|
||||
@@ -171,15 +171,15 @@ async def create_fiat_invoice(
|
||||
|
||||
internal_payment.fiat_provider = fiat_provider_name
|
||||
internal_payment.extra["fiat_checking_id"] = fiat_invoice.checking_id
|
||||
# TODO: move to payment
|
||||
# todo: move to payent
|
||||
internal_payment.extra["fiat_payment_request"] = fiat_invoice.payment_request
|
||||
new_checking_id = (
|
||||
f"fiat_{fiat_provider_name}_"
|
||||
f"{fiat_invoice.checking_id or internal_payment.checking_id}"
|
||||
)
|
||||
internal_payment = await update_payment(
|
||||
internal_payment, new_checking_id, conn=conn
|
||||
)
|
||||
await update_payment(internal_payment, new_checking_id, conn=conn)
|
||||
internal_payment.checking_id = new_checking_id
|
||||
|
||||
return internal_payment
|
||||
|
||||
|
||||
@@ -374,7 +374,7 @@ async def update_pending_payment(
|
||||
status = await check_payment_status(payment)
|
||||
if status.failed:
|
||||
payment.status = PaymentState.FAILED
|
||||
payment = await update_payment(payment, conn=conn)
|
||||
await update_payment(payment, conn=conn)
|
||||
elif status.success:
|
||||
payment = await update_payment_success_status(payment, status, conn=conn)
|
||||
return payment
|
||||
@@ -876,7 +876,7 @@ async def update_payment_success_status(
|
||||
payment.status = PaymentState.SUCCESS
|
||||
payment.fee = -(abs(status.fee_msat or 0) + abs(service_fee_msat))
|
||||
payment.preimage = payment.preimage or status.preimage
|
||||
payment = await update_payment(payment, conn=conn)
|
||||
await update_payment(payment, conn=conn)
|
||||
return payment
|
||||
|
||||
|
||||
@@ -1099,9 +1099,8 @@ async def update_invoice_callback(checking_id: str) -> Payment | None:
|
||||
payment.fee = status.fee_msat or payment.fee
|
||||
# only overwrite preimage if status.preimage provides it
|
||||
payment.preimage = status.preimage or payment.preimage
|
||||
|
||||
payment.status = PaymentState.SUCCESS
|
||||
payment = await update_payment(payment)
|
||||
await update_payment(payment)
|
||||
if payment.fiat_provider:
|
||||
await handle_fiat_payment_confirmation(payment)
|
||||
return payment
|
||||
|
||||
@@ -36,7 +36,6 @@ from lnbits.decorators import (
|
||||
check_account_exists,
|
||||
check_admin,
|
||||
check_user_exists,
|
||||
optional_user_id,
|
||||
)
|
||||
from lnbits.helpers import (
|
||||
create_access_token,
|
||||
@@ -321,10 +320,7 @@ async def api_delete_user_api_token(
|
||||
|
||||
@auth_router.get("/{provider}", description="SSO Provider")
|
||||
async def login_with_sso_provider(
|
||||
request: Request,
|
||||
provider: str,
|
||||
user_id: str | None,
|
||||
auth_user_id: str | None = Depends(optional_user_id),
|
||||
request: Request, provider: str, user_id: str | None = None
|
||||
):
|
||||
provider_sso = _new_sso(provider)
|
||||
if not provider_sso:
|
||||
@@ -332,8 +328,6 @@ async def login_with_sso_provider(
|
||||
HTTPStatus.FORBIDDEN,
|
||||
f"Login by '{provider}' not allowed.",
|
||||
)
|
||||
if user_id and user_id != auth_user_id:
|
||||
raise HTTPException(HTTPStatus.FORBIDDEN, "User ID mismatch.")
|
||||
|
||||
provider_sso.redirect_uri = str(request.base_url) + f"api/v1/auth/{provider}/token"
|
||||
with provider_sso:
|
||||
|
||||
@@ -357,20 +357,16 @@ async def handle_revolut_event(event: dict):
|
||||
return
|
||||
|
||||
payment = await get_standalone_payment(f"fiat_revolut_order_{order_id}")
|
||||
if payment:
|
||||
await check_fiat_status(payment)
|
||||
return
|
||||
|
||||
if event_type == "ORDER_COMPLETED":
|
||||
if not payment:
|
||||
logger.warning(f"No payment found for Revolut order: '{order_id}'.")
|
||||
await _handle_revolut_subscription_order_paid(order_id)
|
||||
return
|
||||
|
||||
logger.info(f"Ignoring Revolut authorised order without payment: '{order_id}'.")
|
||||
await check_fiat_status(payment)
|
||||
return
|
||||
|
||||
if event_type == "SUBSCRIPTION_INITIATED":
|
||||
logger.info("Revolut subscription initiated event received.")
|
||||
await _handle_revolut_subscription_initiated(event)
|
||||
return
|
||||
|
||||
if event_type in [
|
||||
@@ -384,6 +380,23 @@ async def handle_revolut_event(event: dict):
|
||||
logger.warning(f"Unhandled Revolut event type: '{event_type}'.")
|
||||
|
||||
|
||||
async def _handle_revolut_subscription_initiated(event: dict):
|
||||
subscription_id = event.get("subscription_id")
|
||||
if not subscription_id:
|
||||
subscription_id = event.get("id")
|
||||
|
||||
if not subscription_id:
|
||||
logger.warning("Revolut subscription event missing subscription_id.")
|
||||
return
|
||||
|
||||
fiat_provider = await _get_revolut_provider()
|
||||
if not fiat_provider:
|
||||
return
|
||||
|
||||
subscription = await fiat_provider.get_subscription(subscription_id)
|
||||
await _handle_revolut_subscription(subscription, fiat_provider)
|
||||
|
||||
|
||||
async def _get_revolut_provider() -> RevolutWallet | None:
|
||||
fiat_provider = await get_fiat_provider("revolut")
|
||||
if not isinstance(fiat_provider, RevolutWallet):
|
||||
@@ -393,10 +406,7 @@ async def _get_revolut_provider() -> RevolutWallet | None:
|
||||
|
||||
|
||||
async def _handle_revolut_subscription(
|
||||
subscription: dict,
|
||||
fiat_provider: RevolutWallet,
|
||||
order_id: str | None = None,
|
||||
order: dict | None = None,
|
||||
subscription: dict, fiat_provider: RevolutWallet
|
||||
):
|
||||
subscription_id = subscription.get("id")
|
||||
if not subscription_id:
|
||||
@@ -410,17 +420,16 @@ async def _handle_revolut_subscription(
|
||||
logger.warning("Revolut subscription event missing LNbits metadata.")
|
||||
return
|
||||
|
||||
if not order_id:
|
||||
cycle_id = subscription.get("current_cycle_id")
|
||||
if not cycle_id:
|
||||
logger.warning("Revolut subscription missing current_cycle_id.")
|
||||
return
|
||||
cycle_id = subscription.get("current_cycle_id")
|
||||
if not cycle_id:
|
||||
logger.warning("Revolut subscription missing current_cycle_id.")
|
||||
return
|
||||
|
||||
cycle = await fiat_provider.get_subscription_cycle(subscription_id, cycle_id)
|
||||
order_id = cycle.get("order_id")
|
||||
if not order_id:
|
||||
logger.warning("Revolut subscription cycle missing order_id.")
|
||||
return
|
||||
cycle = await fiat_provider.get_subscription_cycle(subscription_id, cycle_id)
|
||||
order_id = cycle.get("order_id")
|
||||
if not order_id:
|
||||
logger.warning("Revolut subscription cycle missing order_id.")
|
||||
return
|
||||
|
||||
existing_payment = await get_standalone_payment(f"fiat_revolut_order_{order_id}")
|
||||
if existing_payment:
|
||||
@@ -430,8 +439,7 @@ async def _handle_revolut_subscription(
|
||||
await check_fiat_status(existing_payment)
|
||||
return
|
||||
|
||||
if not order:
|
||||
order = await fiat_provider.get_order(order_id)
|
||||
order = await fiat_provider.get_order(order_id)
|
||||
amount_minor = order.get("amount")
|
||||
currency = (order.get("currency") or "").upper()
|
||||
if amount_minor is None or not currency:
|
||||
@@ -467,9 +475,7 @@ async def _handle_revolut_subscription_order_paid(order_id: str):
|
||||
return
|
||||
|
||||
order = await fiat_provider.get_order(order_id)
|
||||
order_type = (order.get("type") or "").lower()
|
||||
order_state = (order.get("state") or "").upper()
|
||||
if order_type != "payment" or order_state != "COMPLETED":
|
||||
if order.get("type") != "payment" or order.get("state") != "completed":
|
||||
logger.warning(f"Revolut order is not a completed payment: '{order_id}'.")
|
||||
return
|
||||
|
||||
@@ -484,9 +490,7 @@ async def _handle_revolut_subscription_order_paid(order_id: str):
|
||||
logger.warning(f"Revolut subscription is not active: '{subscription_id}'.")
|
||||
return
|
||||
|
||||
await _handle_revolut_subscription(
|
||||
subscription, fiat_provider, order_id=order_id, order=order
|
||||
)
|
||||
await _handle_revolut_subscription_initiated(subscription)
|
||||
|
||||
|
||||
async def _create_revolut_subscription_payment(
|
||||
|
||||
@@ -20,7 +20,7 @@ from lnbits.decorators import (
|
||||
check_first_install,
|
||||
check_user_exists,
|
||||
)
|
||||
from lnbits.helpers import check_callback_url, template_renderer
|
||||
from lnbits.helpers import check_callback_url, extension_id_from_path, template_renderer
|
||||
from lnbits.settings import settings
|
||||
|
||||
from ..crud import get_user
|
||||
@@ -198,7 +198,9 @@ admin_ui_checks = [Depends(check_admin), Depends(check_admin_ui)]
|
||||
async def index(
|
||||
request: Request, user: User = Depends(check_user_exists)
|
||||
) -> HTMLResponse:
|
||||
return template_renderer().TemplateResponse(
|
||||
return template_renderer(
|
||||
extension_id=extension_id_from_path(request.url.path)
|
||||
).TemplateResponse(
|
||||
request,
|
||||
"base.html",
|
||||
{
|
||||
@@ -211,7 +213,9 @@ async def index(
|
||||
@generic_router.get("/node/public")
|
||||
@generic_router.get("/first_install", dependencies=[Depends(check_first_install)])
|
||||
async def index_public(request: Request) -> HTMLResponse:
|
||||
return template_renderer().TemplateResponse(request, "base.html", {"public": True})
|
||||
return template_renderer(
|
||||
extension_id=extension_id_from_path(request.url.path)
|
||||
).TemplateResponse(request, "base.html", {"public": True})
|
||||
|
||||
|
||||
@generic_router.get("/uuidv4/{hex_value}")
|
||||
|
||||
@@ -34,7 +34,6 @@ from lnbits.core.models import (
|
||||
PaymentWalletStats,
|
||||
SettleInvoice,
|
||||
SimpleStatus,
|
||||
UpdatePaymentExtra,
|
||||
)
|
||||
from lnbits.core.models.payments import UpdatePaymentLabels
|
||||
from lnbits.core.models.users import AccountId
|
||||
@@ -298,38 +297,6 @@ async def api_update_payment_labels(
|
||||
return SimpleStatus(success=True, message="Payment labels updated.")
|
||||
|
||||
|
||||
@payment_router.patch(
|
||||
"/extra",
|
||||
name="Update payment extra",
|
||||
description="Append new extra metadata to a payment.",
|
||||
response_model=Payment,
|
||||
)
|
||||
async def api_update_payment_extra(
|
||||
data: UpdatePaymentExtra,
|
||||
key_type: WalletTypeInfo = Depends(require_admin_key),
|
||||
) -> Payment:
|
||||
payment = await get_standalone_payment(
|
||||
data.payment_hash, wallet_id=key_type.wallet.id
|
||||
)
|
||||
if payment is None:
|
||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Payment does not exist.")
|
||||
if not payment.success:
|
||||
raise HTTPException(
|
||||
HTTPStatus.BAD_REQUEST, "Payment extra can only be updated after success."
|
||||
)
|
||||
|
||||
duplicate_keys = sorted(set(payment.extra).intersection(data.extra))
|
||||
if duplicate_keys:
|
||||
raise HTTPException(
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
f"Extra keys already exist: {', '.join(duplicate_keys)}.",
|
||||
)
|
||||
|
||||
payment.extra.update(data.extra)
|
||||
await update_payment(payment)
|
||||
return payment
|
||||
|
||||
|
||||
@payment_router.get("/fee-reserve")
|
||||
async def api_payments_fee_reserve(invoice: str = Query("invoice")) -> JSONResponse:
|
||||
invoice_obj = bolt11.decode(invoice)
|
||||
|
||||
+3
-3
@@ -131,15 +131,15 @@ class FiatSubscriptionResponse(BaseModel):
|
||||
|
||||
|
||||
class FiatPaymentSuccessStatus(FiatPaymentStatus):
|
||||
paid = True # type: ignore[reportIncompatibleVariableOverride]
|
||||
paid = True
|
||||
|
||||
|
||||
class FiatPaymentFailedStatus(FiatPaymentStatus):
|
||||
paid = False # type: ignore[reportIncompatibleVariableOverride]
|
||||
paid = False
|
||||
|
||||
|
||||
class FiatPaymentPendingStatus(FiatPaymentStatus):
|
||||
paid = None # type: ignore[reportIncompatibleVariableOverride]
|
||||
paid = None
|
||||
|
||||
|
||||
class FiatProvider(ABC):
|
||||
|
||||
+47
-1
@@ -52,7 +52,45 @@ def static_url_for(static: str, path: str) -> str:
|
||||
return f"/{static}/{path}?v={settings.server_startup_time}"
|
||||
|
||||
|
||||
def template_renderer(additional_folders: list | None = None) -> Jinja2Templates:
|
||||
def extension_id_from_path(path: str) -> str | None:
|
||||
parts = [part for part in path.split("/") if part]
|
||||
if not parts:
|
||||
return None
|
||||
|
||||
if len(parts) >= 3 and parts[0] == "upgrades":
|
||||
return parts[2]
|
||||
|
||||
ext_id = parts[0]
|
||||
ext_i18n_dir = Path(
|
||||
settings.lnbits_extensions_path, "extensions", ext_id, "static", "i18n"
|
||||
)
|
||||
if ext_i18n_dir.is_dir():
|
||||
return ext_id
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def extension_i18n_urls(extension_id: str | None) -> list[str]:
|
||||
if not extension_id:
|
||||
return []
|
||||
|
||||
i18n_dir = Path(
|
||||
settings.lnbits_extensions_path, "extensions", extension_id, "static", "i18n"
|
||||
)
|
||||
if not i18n_dir.is_dir():
|
||||
return []
|
||||
|
||||
files = [file.name for file in i18n_dir.glob("*.js") if file.is_file()]
|
||||
return [
|
||||
static_url_for(f"{extension_id}/static", f"i18n/{filename}")
|
||||
for filename in sorted(files, key=lambda name: (name != "en.js", name))
|
||||
]
|
||||
|
||||
|
||||
def template_renderer(
|
||||
additional_folders: list | None = None,
|
||||
extension_id: str | None = None,
|
||||
) -> Jinja2Templates:
|
||||
folders = [
|
||||
"lnbits/templates",
|
||||
settings.extension_builder_working_dir_path.as_posix(),
|
||||
@@ -86,6 +124,14 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
|
||||
t.env.globals["INCLUDED_CSS"] = vendor_files["css"]
|
||||
t.env.globals["INCLUDED_COMPONENTS"] = vendor_files["components"]
|
||||
|
||||
if not extension_id and additional_folders:
|
||||
for folder in additional_folders:
|
||||
parts = Path(folder).parts
|
||||
if parts and parts[-1] == "templates" and len(parts) >= 2:
|
||||
extension_id = parts[-2]
|
||||
break
|
||||
t.env.globals["INCLUDED_EXTENSION_I18N"] = extension_i18n_urls(extension_id)
|
||||
|
||||
# backwards compatibility for extensions (tpos)
|
||||
t.env.globals["LNBITS_DENOMINATION"] = settings.lnbits_denomination
|
||||
|
||||
|
||||
+3
-3
@@ -1047,7 +1047,7 @@ class EditableSettings(
|
||||
|
||||
|
||||
class UpdateSettings(EditableSettings):
|
||||
class Config(EditableSettings.Config):
|
||||
class Config:
|
||||
extra = Extra.forbid
|
||||
|
||||
|
||||
@@ -1198,11 +1198,11 @@ class ReadOnlySettings(
|
||||
|
||||
|
||||
class Settings(EditableSettings, ReadOnlySettings, TransientSettings, BaseSettings):
|
||||
class Config(EditableSettings.Config, BaseSettings.Config): # type: ignore[misc]
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
env_file_encoding = "utf-8"
|
||||
case_sensitive = False
|
||||
json_loads = list_parse_fallback # type: ignore[assignment]
|
||||
json_loads = list_parse_fallback
|
||||
|
||||
def is_user_allowed(self, user_id: str) -> bool:
|
||||
return (
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -212,15 +212,12 @@ body.bg-image .q-page-container {
|
||||
backdrop-filter: none; /* Ensure the page content is not affected */
|
||||
}
|
||||
|
||||
body.body--dark .q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark) {
|
||||
--q-dark: rgba(29, 29, 29, 0.3);
|
||||
background-color: var(--q-dark);
|
||||
}
|
||||
body.body--dark .q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark),
|
||||
body.body--dark .q-header,
|
||||
body.body--dark .q-drawer {
|
||||
--q-dark: rgba(29, 29, 29, 0.3);
|
||||
background-color: var(--q-dark);
|
||||
backdrop-filter: brightness(0.8);
|
||||
backdrop-filter: blur(6px) brightness(0.8);
|
||||
}
|
||||
|
||||
body.rounded-ui .q-card,
|
||||
@@ -391,11 +388,11 @@ body[data-theme=salvador].card-gradient.body--dark .q-drawer {
|
||||
}
|
||||
|
||||
body.card-shadow .q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark) {
|
||||
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.18);
|
||||
filter: drop-shadow(0 10px 24px rgba(0, 0, 0, 0.18));
|
||||
}
|
||||
|
||||
body.card-shadow.body--dark .q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark) {
|
||||
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.45);
|
||||
filter: drop-shadow(0 12px 28px rgba(0, 0, 0, 0.45));
|
||||
}
|
||||
|
||||
body.no-burger-background .q-drawer {
|
||||
|
||||
@@ -360,37 +360,18 @@ window.app.component('lnbits-payment-list', {
|
||||
paymentTableRowKey(row) {
|
||||
return row.payment_hash + row.amount
|
||||
},
|
||||
async exportCSV(detailed = false) {
|
||||
exportCSV(detailed = false) {
|
||||
// status is important for export but it is not in paymentsTable
|
||||
// because it is manually added with payment detail link and icons
|
||||
// and would cause duplication in the list
|
||||
const pagination = this.paymentsTable.pagination
|
||||
const maxPages = 100
|
||||
const limit = 1000
|
||||
let payments = []
|
||||
|
||||
this.paymentsCSV.loading = true
|
||||
try {
|
||||
for (let page = 0; page < maxPages; page++) {
|
||||
const query = {
|
||||
sortby: pagination.sortBy ?? 'time',
|
||||
direction: pagination.descending ? 'desc' : 'asc',
|
||||
limit,
|
||||
offset: page * limit
|
||||
}
|
||||
const params = new URLSearchParams(query)
|
||||
const response = await LNbits.api.getPayments(this.wallet, params)
|
||||
const pagePayments = response.data.data || []
|
||||
payments = payments.concat(pagePayments.map(this.mapPayment))
|
||||
|
||||
if (
|
||||
pagePayments.length < limit ||
|
||||
payments.length >= response.data.total
|
||||
) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const query = {
|
||||
sortby: pagination.sortBy ?? 'time',
|
||||
direction: pagination.descending ? 'desc' : 'asc'
|
||||
}
|
||||
const params = new URLSearchParams(query)
|
||||
LNbits.api.getPayments(this.wallet, params).then(response => {
|
||||
let payments = response.data.data.map(this.mapPayment)
|
||||
let columns = this.paymentsCSV.columns
|
||||
|
||||
if (detailed) {
|
||||
@@ -419,11 +400,7 @@ window.app.component('lnbits-payment-list', {
|
||||
payments,
|
||||
this.wallet.name + '-payments'
|
||||
)
|
||||
} catch (err) {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
} finally {
|
||||
this.paymentsCSV.loading = false
|
||||
}
|
||||
})
|
||||
},
|
||||
addFilterTag() {
|
||||
if (!this.exportTagName) return
|
||||
|
||||
@@ -8,10 +8,6 @@ window.app.component('lnbits-qrcode-lnurl', {
|
||||
prefix: {
|
||||
type: String,
|
||||
default: 'lnurlp'
|
||||
},
|
||||
href: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -25,10 +21,7 @@ window.app.component('lnbits-qrcode-lnurl', {
|
||||
if (this.tab == 'bech32') {
|
||||
const bytes = new TextEncoder().encode(this.url)
|
||||
const bech32 = NostrTools.nip19.encodeBytes('lnurl', bytes)
|
||||
this.lnurl =
|
||||
this.href && this.href.trim() !== ''
|
||||
? `${this.href}?lightning=${bech32.toUpperCase()}`
|
||||
: `lightning:${bech32.toUpperCase()}`
|
||||
this.lnurl = `lightning:${bech32.toUpperCase()}`
|
||||
} else if (this.tab == 'lud17') {
|
||||
if (this.url.startsWith('http://')) {
|
||||
this.lnurl = this.url.replace('http://', this.prefix + '://')
|
||||
|
||||
@@ -58,15 +58,11 @@ body.bg-image {
|
||||
}
|
||||
// transparent background for specific elements
|
||||
body.body--dark {
|
||||
.q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark) {
|
||||
--q-dark: #{color.adjust(#1d1d1d, $alpha: -0.7)};
|
||||
background-color: var(--q-dark);
|
||||
}
|
||||
|
||||
.q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark),
|
||||
.q-header,
|
||||
.q-drawer {
|
||||
--q-dark: #{color.adjust(#1d1d1d, $alpha: -0.7)};
|
||||
background-color: var(--q-dark);
|
||||
backdrop-filter: brightness(0.8);
|
||||
backdrop-filter: blur(6px) brightness(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,13 +61,13 @@ body.rounded-ui {
|
||||
|
||||
body.card-shadow {
|
||||
.q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark) {
|
||||
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.18);
|
||||
filter: drop-shadow(0 10px 24px rgba(0, 0, 0, 0.18));
|
||||
}
|
||||
}
|
||||
|
||||
body.card-shadow.body--dark {
|
||||
.q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark) {
|
||||
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.45);
|
||||
filter: drop-shadow(0 12px 28px rgba(0, 0, 0, 0.45));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -90,7 +90,9 @@
|
||||
window.g.isPublicPage = false
|
||||
{% endif %}
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endif %} {% for url in INCLUDED_EXTENSION_I18N %}
|
||||
<script src="{{ url }}"></script>
|
||||
{% endfor %}
|
||||
<!-- app init -->
|
||||
<script>
|
||||
window.app = Vue.createApp({
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
</q-card>
|
||||
</div>
|
||||
<div
|
||||
v-show="chartData.showPaymentTags"
|
||||
v-show="chartData.showPaymentStatus"
|
||||
class="col-lg-3 col-md-6 col-sm-12 text-center"
|
||||
>
|
||||
<q-card class="q-pt-sm">
|
||||
|
||||
@@ -94,15 +94,15 @@ class PaymentStatus(NamedTuple):
|
||||
|
||||
|
||||
class PaymentSuccessStatus(PaymentStatus):
|
||||
paid = True # type: ignore[reportIncompatibleVariableOverride]
|
||||
paid = True
|
||||
|
||||
|
||||
class PaymentFailedStatus(PaymentStatus):
|
||||
paid = False # type: ignore[reportIncompatibleVariableOverride]
|
||||
paid = False
|
||||
|
||||
|
||||
class PaymentPendingStatus(PaymentStatus):
|
||||
paid = None # type: ignore[reportIncompatibleVariableOverride]
|
||||
paid = None
|
||||
|
||||
|
||||
class Wallet(ABC):
|
||||
|
||||
@@ -8,7 +8,7 @@ from loguru import logger
|
||||
from pydantic import BaseModel
|
||||
from websockets import Subprotocol, connect
|
||||
|
||||
from lnbits import bolt11 as bolt11_lib
|
||||
from lnbits import bolt11
|
||||
from lnbits.helpers import normalize_endpoint
|
||||
from lnbits.settings import settings
|
||||
|
||||
@@ -164,13 +164,15 @@ class BlinkWallet(Wallet):
|
||||
ok=False, error_message=f"Unable to connect to {self.endpoint}."
|
||||
)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
async def pay_invoice(
|
||||
self, bolt11_invoice: str, fee_limit_msat: int
|
||||
) -> PaymentResponse:
|
||||
# https://dev.blink.sv/api/btc-ln-send
|
||||
# Future: add check fee estimate is < fee_limit_msat before paying invoice
|
||||
|
||||
payment_variables = {
|
||||
"input": {
|
||||
"paymentRequest": bolt11,
|
||||
"paymentRequest": bolt11_invoice,
|
||||
"walletId": self.wallet_id,
|
||||
"memo": "Payment memo",
|
||||
}
|
||||
@@ -188,7 +190,7 @@ class BlinkWallet(Wallet):
|
||||
error_message = errors[0].get("message")
|
||||
return PaymentResponse(ok=False, error_message=error_message)
|
||||
|
||||
checking_id = bolt11_lib.decode(bolt11).payment_hash
|
||||
checking_id = bolt11.decode(bolt11_invoice).payment_hash
|
||||
|
||||
payment_status = await self.get_payment_status(checking_id)
|
||||
fee_msat = payment_status.fee_msat
|
||||
@@ -197,7 +199,7 @@ class BlinkWallet(Wallet):
|
||||
ok=True, checking_id=checking_id, fee_msat=fee_msat, preimage=preimage
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.info(f"Failed to pay invoice {bolt11}")
|
||||
logger.info(f"Failed to pay invoice {bolt11_invoice}")
|
||||
logger.warning(exc)
|
||||
return PaymentResponse(
|
||||
error_message=f"Unable to connect to {self.endpoint}."
|
||||
|
||||
@@ -19,7 +19,7 @@ else:
|
||||
|
||||
from bolt11 import Bolt11Exception
|
||||
from bolt11 import decode as bolt11_decode
|
||||
from breez_sdk import ( # type: ignore[reportMissingImports]
|
||||
from breez_sdk import (
|
||||
BreezEvent,
|
||||
ConnectRequest,
|
||||
EnvironmentType,
|
||||
@@ -39,9 +39,7 @@ else:
|
||||
default_config,
|
||||
mnemonic_to_seed,
|
||||
)
|
||||
from breez_sdk import (
|
||||
PaymentStatus as BreezPaymentStatus, # type: ignore[reportMissingImports]
|
||||
)
|
||||
from breez_sdk import PaymentStatus as BreezPaymentStatus
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.settings import settings
|
||||
|
||||
@@ -18,7 +18,7 @@ else:
|
||||
from pathlib import Path
|
||||
|
||||
from bolt11 import decode as bolt11_decode
|
||||
from breez_sdk_liquid import ( # type: ignore[reportMissingImports]
|
||||
from breez_sdk_liquid import (
|
||||
ConnectRequest,
|
||||
EventListener,
|
||||
GetInfoResponse,
|
||||
|
||||
@@ -103,7 +103,7 @@ class FakeWallet(Wallet):
|
||||
preimage=preimage.hex(),
|
||||
)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
async def pay_invoice(self, bolt11: str, _: int) -> PaymentResponse:
|
||||
try:
|
||||
invoice = decode(bolt11)
|
||||
except Bolt11Exception as exc:
|
||||
@@ -130,7 +130,7 @@ class FakeWallet(Wallet):
|
||||
return PaymentPendingStatus()
|
||||
return PaymentFailedStatus()
|
||||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
async def get_payment_status(self, _: str) -> PaymentStatus:
|
||||
return PaymentPendingStatus()
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
|
||||
@@ -118,7 +118,7 @@ class LndWallet(Wallet):
|
||||
|
||||
cert = open(cert_path, "rb").read()
|
||||
creds = grpc.ssl_channel_credentials(cert)
|
||||
auth_creds = grpc.metadata_call_credentials(self.metadata_callback) # type: ignore[reportArgumentType]
|
||||
auth_creds = grpc.metadata_call_credentials(self.metadata_callback)
|
||||
composite_creds = grpc.composite_channel_credentials(creds, auth_creds)
|
||||
channel = grpc.aio.secure_channel(
|
||||
f"{self.endpoint}:{self.port}", composite_creds
|
||||
|
||||
@@ -88,8 +88,6 @@ class NWCWallet(Wallet):
|
||||
payment_data = await self.conn.call(
|
||||
"lookup_invoice", {"payment_hash": payment["checking_id"]}
|
||||
)
|
||||
if payment_data.get("payment_hash") != payment["checking_id"]:
|
||||
raise Exception("Mismatched payment hash")
|
||||
settled = (
|
||||
"settled_at" in payment_data
|
||||
and payment_data["settled_at"]
|
||||
@@ -266,8 +264,6 @@ class NWCWallet(Wallet):
|
||||
payment_data = await self.conn.call(
|
||||
"lookup_invoice", {"payment_hash": checking_id}
|
||||
)
|
||||
if payment_data.get("payment_hash") != checking_id:
|
||||
raise Exception("Mismatched payment hash")
|
||||
settled = payment_data.get("settled_at", None) and payment_data.get(
|
||||
"preimage", None
|
||||
)
|
||||
@@ -524,9 +520,8 @@ class NWCConnection:
|
||||
"""
|
||||
sub_id = cast(str, msg[1])
|
||||
event = cast(dict, msg[2])
|
||||
# Ensure the event is valid (do not trust relays)
|
||||
if not verify_event(event) or event.get("pubkey") != self.service_pubkey_hex:
|
||||
raise Exception("Invalid event")
|
||||
if not verify_event(event): # Ensure the event is valid (do not trust relays)
|
||||
raise Exception("Invalid event signature")
|
||||
tags = event["tags"]
|
||||
if event["kind"] == 13194: # An info event
|
||||
# info events are handled specially,
|
||||
@@ -692,7 +687,6 @@ class NWCConnection:
|
||||
"#p": [self.account_public_key_hex],
|
||||
"#e": [event["id"]],
|
||||
"since": event["created_at"],
|
||||
"authors": [self.service_pubkey_hex],
|
||||
}
|
||||
sub_id = self._get_new_subid()
|
||||
# register a future to receive the response asynchronously
|
||||
|
||||
Generated
+5
-8
@@ -24,7 +24,7 @@
|
||||
"clean-css-cli": "^5.6.3",
|
||||
"concat": "^1.0.3",
|
||||
"prettier": "^3.8.3",
|
||||
"pyright": "1.1.409",
|
||||
"pyright": "1.1.289",
|
||||
"sass": "^1.99.0",
|
||||
"terser": "^5.47.1"
|
||||
}
|
||||
@@ -1808,9 +1808,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pyright": {
|
||||
"version": "1.1.409",
|
||||
"resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.409.tgz",
|
||||
"integrity": "sha512-13VFQyw4mJzshZxcxiYbNjo1hG/WHSRDj70Y3lbJEHqCkI2dvBAUTti8VV6Ezsr5gT93pFvC0e/jAQS4JdHarA==",
|
||||
"version": "1.1.289",
|
||||
"resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.289.tgz",
|
||||
"integrity": "sha512-fG3STxnwAt3i7bxbXUPJdYNFrcOWHLwCSEOySH2foUqtYdzWLcxDez0Kgl1X8LMQx0arMJ6HRkKghxfRD1/z6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -1818,10 +1818,7 @@
|
||||
"pyright-langserver": "langserver.index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode.vue": {
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
"clean-css-cli": "^5.6.3",
|
||||
"concat": "^1.0.3",
|
||||
"prettier": "^3.8.3",
|
||||
"pyright": "1.1.409",
|
||||
"pyright": "1.1.289",
|
||||
"sass": "^1.99.0",
|
||||
"terser": "^5.47.1"
|
||||
},
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "lnbits"
|
||||
version = "1.5.5-rc2"
|
||||
version = "1.5.4"
|
||||
requires-python = ">=3.10,<3.13"
|
||||
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
||||
authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
|
||||
|
||||
@@ -72,42 +72,8 @@ async def test_auth_api_sso_login_and_callback(http_client: AsyncClient, mocker)
|
||||
login_sso = _FakeSSO()
|
||||
mocker.patch("lnbits.core.views.auth_api._new_sso", return_value=login_sso)
|
||||
|
||||
unauthenticated = await http_client.get(
|
||||
f"/api/v1/auth/{provider}", params={"user_id": user.id}
|
||||
)
|
||||
assert unauthenticated.status_code == 403
|
||||
assert unauthenticated.json()["detail"] == "User ID mismatch."
|
||||
|
||||
other_user = await create_user_account(
|
||||
Account(
|
||||
id=uuid4().hex,
|
||||
username=f"user_{uuid4().hex[:8]}",
|
||||
email=f"user_{uuid4().hex[:8]}@lnbits.com",
|
||||
)
|
||||
)
|
||||
other_login = await http_client.post(
|
||||
"/api/v1/auth/usr", json={"usr": other_user.id}
|
||||
)
|
||||
http_client.cookies.clear()
|
||||
assert other_login.status_code == 200
|
||||
other_headers = {
|
||||
"Authorization": f"Bearer {other_login.json()['access_token']}",
|
||||
}
|
||||
wrong_user = await http_client.get(
|
||||
f"/api/v1/auth/{provider}",
|
||||
params={"user_id": user.id},
|
||||
headers=other_headers,
|
||||
)
|
||||
assert wrong_user.status_code == 403
|
||||
assert wrong_user.json()["detail"] == "User ID mismatch."
|
||||
|
||||
login = await http_client.post("/api/v1/auth/usr", json={"usr": user.id})
|
||||
http_client.cookies.clear()
|
||||
assert login.status_code == 200
|
||||
headers = {"Authorization": f"Bearer {login.json()['access_token']}"}
|
||||
|
||||
response = await http_client.get(
|
||||
f"/api/v1/auth/{provider}", params={"user_id": user.id}, headers=headers
|
||||
f"/api/v1/auth/{provider}", params={"user_id": user.id}
|
||||
)
|
||||
assert response.status_code == 307
|
||||
assert response.headers["location"] == "https://example.com/sso/login"
|
||||
|
||||
@@ -194,7 +194,7 @@ async def test_callback_api_handles_revolut_subscription_event(
|
||||
settings.revolut_api_secret_key = "revolut-secret"
|
||||
settings.revolut_api_version = "2026-04-20"
|
||||
revolut_provider = RevolutWallet()
|
||||
get_subscription_mock = mocker.patch.object(
|
||||
mocker.patch.object(
|
||||
revolut_provider,
|
||||
"get_subscription",
|
||||
return_value={
|
||||
@@ -253,10 +253,23 @@ async def test_callback_api_handles_revolut_subscription_event(
|
||||
}
|
||||
)
|
||||
|
||||
get_subscription_mock.assert_not_awaited()
|
||||
create_wallet_invoice_mock.assert_not_awaited()
|
||||
update_payment_mock.assert_not_awaited()
|
||||
fiat_status_mock.assert_not_awaited()
|
||||
assert create_wallet_invoice_mock.await_count == 1
|
||||
called_wallet_id, invoice = create_wallet_invoice_mock.await_args.args
|
||||
assert called_wallet_id == "wallet_1"
|
||||
assert invoice.amount == 9.25
|
||||
assert invoice.memo == "Revolut Members"
|
||||
assert invoice.external_id == "SUBSCRIPTION_1"
|
||||
assert invoice.internal is True
|
||||
assert invoice.extra["fiat_method"] == "subscription"
|
||||
assert invoice.extra["subscription"]["checking_id"] == "order_ORDER_SUB_1"
|
||||
assert payment.fiat_provider == "revolut"
|
||||
assert payment.fee == -2
|
||||
assert payment.extra["fiat_checking_id"] == "order_ORDER_SUB_1"
|
||||
assert payment.checking_id == "fiat_revolut_order_ORDER_SUB_1"
|
||||
update_payment_mock.assert_awaited_once_with(
|
||||
payment, "fiat_revolut_order_ORDER_SUB_1"
|
||||
)
|
||||
fiat_status_mock.assert_awaited_once_with(payment)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@@ -340,9 +353,10 @@ async def test_callback_api_handles_revolut_subscription_order_event(
|
||||
|
||||
assert get_payment_mock.await_count == 2
|
||||
get_payment_mock.assert_any_await("fiat_revolut_order_ORDER_SUB_1")
|
||||
assert get_order_mock.await_count == 1
|
||||
assert get_order_mock.await_count == 2
|
||||
assert [call.args for call in get_subscription_mock.await_args_list] == [
|
||||
("SUBSCRIPTION_1",),
|
||||
("SUBSCRIPTION_1",),
|
||||
]
|
||||
assert create_wallet_invoice_mock.await_count == 1
|
||||
called_wallet_id, invoice = create_wallet_invoice_mock.await_args.args
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from typing import cast
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
@@ -107,7 +106,7 @@ async def test_lnurl_api_auth_and_pay_flow(mocker):
|
||||
await api_perform_lnurlauth(auth_response, wallet_info)
|
||||
|
||||
action_response = LnurlPayActionResponse(
|
||||
pr=cast(LightningInvoice, LightningInvoice(TEST_BOLT11)),
|
||||
pr=LightningInvoice(TEST_BOLT11),
|
||||
disposable=False,
|
||||
successAction=parse_obj_as(MessageAction, {"message": "paid"}),
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ import pytest
|
||||
from fastapi import HTTPException
|
||||
from pydantic import ValidationError
|
||||
|
||||
from lnbits.core.crud.payments import create_payment, get_payment, get_payments
|
||||
from lnbits.core.crud.payments import create_payment, get_payments
|
||||
from lnbits.core.models import Account, CreateInvoice, PaymentFilters, PaymentState
|
||||
from lnbits.core.models.payments import CancelInvoice, CreatePayment, SettleInvoice
|
||||
from lnbits.core.models.users import AccountId
|
||||
@@ -161,7 +161,7 @@ async def test_payment_api_fee_reserve_and_hold_invoice_actions(mocker):
|
||||
wallet.id, CreateInvoice(out=False, amount=42, memo="reserve")
|
||||
)
|
||||
reserve = await api_payments_fee_reserve(invoice.bolt11)
|
||||
assert json.loads(bytes(reserve.body))["fee_reserve"] >= 0
|
||||
assert json.loads(reserve.body)["fee_reserve"] >= 0
|
||||
|
||||
with pytest.raises(HTTPException, match="Invoice has no amount."):
|
||||
await api_payments_fee_reserve(ZERO_AMOUNT_INVOICE)
|
||||
@@ -218,164 +218,6 @@ async def test_payment_api_fee_reserve_and_hold_invoice_actions(mocker):
|
||||
cancel_mock.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_payment_extra_update_appends_new_keys(
|
||||
client,
|
||||
to_wallet,
|
||||
adminkey_headers_to,
|
||||
):
|
||||
payment_hash = uuid4().hex
|
||||
checking_id = await _create_payment(
|
||||
to_wallet.id,
|
||||
amount_msat=1_000,
|
||||
payment_hash=payment_hash,
|
||||
tag="splitpayments",
|
||||
)
|
||||
|
||||
response = await client.patch(
|
||||
"/api/v1/payments/extra",
|
||||
headers=adminkey_headers_to,
|
||||
json={
|
||||
"payment_hash": payment_hash,
|
||||
"extra": {"child": "daughter", "compliance_note": "reviewed"},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
extra = response.json()["extra"]
|
||||
assert extra["tag"] == "splitpayments"
|
||||
assert extra["child"] == "daughter"
|
||||
assert extra["compliance_note"] == "reviewed"
|
||||
|
||||
payment = await get_payment(checking_id)
|
||||
assert payment.extra == extra
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_payment_extra_update_creates_extra_when_missing(
|
||||
client,
|
||||
to_wallet,
|
||||
adminkey_headers_to,
|
||||
):
|
||||
payment_hash = uuid4().hex
|
||||
checking_id = await _create_payment(
|
||||
to_wallet.id,
|
||||
amount_msat=1_000,
|
||||
payment_hash=payment_hash,
|
||||
)
|
||||
|
||||
response = await client.patch(
|
||||
"/api/v1/payments/extra",
|
||||
headers=adminkey_headers_to,
|
||||
json={"payment_hash": payment_hash, "extra": {"note": "reviewed"}},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["extra"] == {"note": "reviewed"}
|
||||
|
||||
payment = await get_payment(checking_id)
|
||||
assert payment.extra == {"note": "reviewed"}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_payment_extra_update_rejects_existing_keys(
|
||||
client,
|
||||
to_wallet,
|
||||
adminkey_headers_to,
|
||||
):
|
||||
payment_hash = uuid4().hex
|
||||
checking_id = await _create_payment(
|
||||
to_wallet.id,
|
||||
amount_msat=1_000,
|
||||
payment_hash=payment_hash,
|
||||
tag="original",
|
||||
)
|
||||
|
||||
response = await client.patch(
|
||||
"/api/v1/payments/extra",
|
||||
headers=adminkey_headers_to,
|
||||
json={"payment_hash": payment_hash, "extra": {"tag": "overwritten"}},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json()["detail"] == "Extra keys already exist: tag."
|
||||
|
||||
payment = await get_payment(checking_id)
|
||||
assert payment.extra == {"tag": "original"}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_payment_extra_update_requires_admin_key(
|
||||
client,
|
||||
to_wallet,
|
||||
inkey_headers_to,
|
||||
):
|
||||
payment_hash = uuid4().hex
|
||||
await _create_payment(
|
||||
to_wallet.id,
|
||||
amount_msat=1_000,
|
||||
payment_hash=payment_hash,
|
||||
)
|
||||
|
||||
response = await client.patch(
|
||||
"/api/v1/payments/extra",
|
||||
headers=inkey_headers_to,
|
||||
json={"payment_hash": payment_hash, "extra": {"note": "invoice key"}},
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json()["detail"] == "Invalid adminkey."
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_payment_extra_update_is_wallet_scoped(
|
||||
client,
|
||||
from_wallet,
|
||||
adminkey_headers_to,
|
||||
):
|
||||
payment_hash = uuid4().hex
|
||||
await _create_payment(
|
||||
from_wallet.id,
|
||||
amount_msat=1_000,
|
||||
payment_hash=payment_hash,
|
||||
)
|
||||
|
||||
response = await client.patch(
|
||||
"/api/v1/payments/extra",
|
||||
headers=adminkey_headers_to,
|
||||
json={"payment_hash": payment_hash, "extra": {"note": "wrong wallet"}},
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
assert response.json()["detail"] == "Payment does not exist."
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_payment_extra_update_requires_successful_payment(
|
||||
client,
|
||||
to_wallet,
|
||||
adminkey_headers_to,
|
||||
):
|
||||
payment_hash = uuid4().hex
|
||||
await _create_payment(
|
||||
to_wallet.id,
|
||||
amount_msat=1_000,
|
||||
payment_hash=payment_hash,
|
||||
status=PaymentState.PENDING,
|
||||
)
|
||||
|
||||
response = await client.patch(
|
||||
"/api/v1/payments/extra",
|
||||
headers=adminkey_headers_to,
|
||||
json={"payment_hash": payment_hash, "extra": {"note": "too early"}},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert (
|
||||
response.json()["detail"] == "Payment extra can only be updated after success."
|
||||
)
|
||||
|
||||
|
||||
async def _create_payment(
|
||||
wallet_id: str,
|
||||
*,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from typing import cast
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
@@ -87,9 +86,7 @@ async def test_get_pr_from_lnurl_success_and_error(mocker: MockerFixture):
|
||||
mocker.patch(
|
||||
"lnbits.core.services.lnurl.execute_pay_request",
|
||||
mocker.AsyncMock(
|
||||
return_value=LnurlPayActionResponse(
|
||||
pr=cast(LightningInvoice, LightningInvoice(TEST_BOLT11))
|
||||
)
|
||||
return_value=LnurlPayActionResponse(pr=LightningInvoice(TEST_BOLT11))
|
||||
),
|
||||
)
|
||||
|
||||
@@ -109,7 +106,7 @@ async def test_fetch_lnurl_pay_request_converts_currency_and_stores_paylink(
|
||||
):
|
||||
pay_response = make_lnurl_pay_response(min_sendable_msat=1, text="Test")
|
||||
action_response = LnurlPayActionResponse(
|
||||
pr=cast(LightningInvoice, LightningInvoice(TEST_BOLT11)), disposable=False
|
||||
pr=LightningInvoice(TEST_BOLT11), disposable=False
|
||||
)
|
||||
mocker.patch(
|
||||
"lnbits.core.services.lnurl.fiat_amount_as_satoshis",
|
||||
@@ -146,7 +143,7 @@ async def test_store_paylink_appends_and_updates_existing():
|
||||
wallet = await _create_wallet()
|
||||
pay_response = make_lnurl_pay_response(min_sendable_msat=1, text="Test")
|
||||
action_response = LnurlPayActionResponse(
|
||||
pr=cast(LightningInvoice, LightningInvoice(TEST_BOLT11)), disposable=False
|
||||
pr=LightningInvoice(TEST_BOLT11), disposable=False
|
||||
)
|
||||
|
||||
await store_paylink(
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
@@ -67,7 +66,7 @@ async def test_create_user_account_no_check_rejects_duplicate_identity_fields(
|
||||
existing = _account(**existing_data)
|
||||
await create_account(existing)
|
||||
|
||||
resolved: dict[str, Any] = {
|
||||
resolved = {
|
||||
key: (value(existing) if callable(value) else value)
|
||||
for key, value in new_data.items()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from pytest_mock.plugin import MockerFixture
|
||||
|
||||
@@ -16,22 +14,22 @@ from lnbits.settings import (
|
||||
set_cli_settings,
|
||||
)
|
||||
|
||||
lnurlp_redirect_path: dict[str, Any] = {
|
||||
lnurlp_redirect_path = {
|
||||
"from_path": "/.well-known/lnurlp",
|
||||
"redirect_to_path": "/api/v1/well-known",
|
||||
}
|
||||
lnurlp_redirect_path_with_headers: dict[str, Any] = {
|
||||
lnurlp_redirect_path_with_headers = {
|
||||
"from_path": "/.well-known/lnurlp",
|
||||
"redirect_to_path": "/api/v1/well-known",
|
||||
"header_filters": {"accept": "application/nostr+json"},
|
||||
}
|
||||
|
||||
lnaddress_redirect_path: dict[str, Any] = {
|
||||
lnaddress_redirect_path = {
|
||||
"from_path": "/.well-known/lnurlp",
|
||||
"redirect_to_path": "/api/v1/well-known",
|
||||
}
|
||||
|
||||
nostrrelay_redirect_path: dict[str, Any] = {
|
||||
nostrrelay_redirect_path = {
|
||||
"from_path": "/",
|
||||
"redirect_to_path": "/api/v1/relay-info",
|
||||
"header_filters": {"accept": "application/nostr+json"},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import os
|
||||
from typing import cast
|
||||
|
||||
import pytest
|
||||
from loguru import logger
|
||||
@@ -30,7 +29,7 @@ logger.info(f"settings.blink_api_endpoint: {settings.blink_api_endpoint}")
|
||||
logger.info(f"settings.blink_token: {settings.blink_token}")
|
||||
|
||||
set_funding_source()
|
||||
funding_source = cast(BlinkWallet, get_funding_source())
|
||||
funding_source = get_funding_source()
|
||||
assert isinstance(funding_source, BlinkWallet)
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import importlib
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
import pytest
|
||||
@@ -81,9 +80,7 @@ def _check_calls(expected_calls):
|
||||
for func_call in func_calls:
|
||||
req = func_call["request_data"]
|
||||
args = req["args"] if "args" in req else {}
|
||||
kwargs: dict[str, Any] = (
|
||||
_eval_dict(req["kwargs"]) or {} if "kwargs" in req else {}
|
||||
)
|
||||
kwargs = _eval_dict(req["kwargs"]) if "kwargs" in req else {}
|
||||
|
||||
if "klass" in req:
|
||||
*rest, cls = req["klass"].split(".")
|
||||
@@ -169,7 +166,7 @@ def _mock_field(field):
|
||||
return response
|
||||
|
||||
|
||||
def _eval_dict(data: dict | None) -> dict[str, Any] | None:
|
||||
def _eval_dict(data: dict | None) -> dict | None:
|
||||
fn_prefix = "__eval__:"
|
||||
if not data:
|
||||
return data
|
||||
@@ -218,9 +215,9 @@ def _data_mock(data: dict) -> Mock:
|
||||
def _raise(error: dict | None):
|
||||
if not error:
|
||||
return Exception()
|
||||
data: dict[str, Any] = error["data"] if "data" in error else {}
|
||||
data = error["data"] if "data" in error else None
|
||||
if "module" not in error or "class" not in error:
|
||||
return Exception(data or None)
|
||||
return Exception(data)
|
||||
|
||||
error_module = importlib.import_module(error["module"])
|
||||
error_class = getattr(error_module, error["class"])
|
||||
|
||||
@@ -1075,11 +1075,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.15"
|
||||
version = "3.14"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/05/b1/efac073e0c297ecf2fb33c346989a529d4e19164f1759102dee5953ee17e/idna-3.14.tar.gz", hash = "sha256:466d810d7a2cc1022bea9b037c39728d51ae7dad40d480fc9b7d7ecf98ba8ee3", size = 198272, upload-time = "2026-05-10T20:32:15.935Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/3c/3f62dee257eb3d6b2c1ef2a09d36d9793c7111156a73b5654d2c2305e5ce/idna-3.14-py3-none-any.whl", hash = "sha256:e677eaf072e290f7b725f9acf0b3a2bd55f9fd6f7c70abe5f0e34823d0accf69", size = 72184, upload-time = "2026-05-10T20:32:14.295Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1275,7 +1275,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "lnbits"
|
||||
version = "1.5.5rc2"
|
||||
version = "1.5.4"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiosqlite" },
|
||||
|
||||
Reference in New Issue
Block a user