Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88b01e6079 | ||
|
|
b0d0901969 | ||
|
|
6563610b6c | ||
|
|
c4aa0b598c | ||
|
|
aab76e691d | ||
|
|
3ed30be93e | ||
|
|
37f99e7f96 | ||
|
|
a16078a6ba | ||
|
|
c4c03d96a3 | ||
|
|
ab3248da15 | ||
|
|
b4b37cd733 | ||
|
|
26eb7a449c | ||
|
|
695e9b6471 | ||
|
|
5c9511ccfe | ||
|
|
c7b7832a88 |
@@ -159,6 +159,14 @@ mkdir data
|
|||||||
docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits/lnbits
|
docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits/lnbits
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The LNbits Docker image comes with no extensions installed. User-installed extensions will be stored by default in a container directory.
|
||||||
|
It is recommended to point the `LNBITS_EXTENSIONS_PATH` environment variable to a directory that is mapped to a Docker volume. This way, the extensions will not be reinstalled when the container is destroyed.
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker run ... -e "LNBITS_EXTENSIONS_PATH='/app/data/extensions'" --volume ${PWD}/data/:/app/data ...
|
||||||
|
```
|
||||||
|
|
||||||
Build the image yourself.
|
Build the image yourself.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
+5
-8
@@ -9,16 +9,13 @@ from .decorators import (
|
|||||||
from .exceptions import InvoiceError, PaymentError
|
from .exceptions import InvoiceError, PaymentError
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# decorators
|
"InvoiceError",
|
||||||
"require_admin_key",
|
"PaymentError",
|
||||||
"require_invoice_key",
|
|
||||||
"check_admin",
|
"check_admin",
|
||||||
"check_super_user",
|
"check_super_user",
|
||||||
"check_user_exists",
|
"check_user_exists",
|
||||||
# services
|
|
||||||
"pay_invoice",
|
|
||||||
"create_invoice",
|
"create_invoice",
|
||||||
# exceptions
|
"pay_invoice",
|
||||||
"PaymentError",
|
"require_admin_key",
|
||||||
"InvoiceError",
|
"require_invoice_key",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ from .middleware import (
|
|||||||
add_ip_block_middleware,
|
add_ip_block_middleware,
|
||||||
add_ratelimit_middleware,
|
add_ratelimit_middleware,
|
||||||
)
|
)
|
||||||
from .requestvars import g
|
|
||||||
from .tasks import (
|
from .tasks import (
|
||||||
check_pending_payments,
|
check_pending_payments,
|
||||||
internal_invoice_listener,
|
internal_invoice_listener,
|
||||||
@@ -171,8 +170,6 @@ def create_app() -> FastAPI:
|
|||||||
name="library",
|
name="library",
|
||||||
)
|
)
|
||||||
|
|
||||||
g().base_url = f"http://{settings.host}:{settings.port}"
|
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]
|
CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ from .views.admin_api import admin_router
|
|||||||
from .views.api import api_router
|
from .views.api import api_router
|
||||||
from .views.audit_api import audit_router
|
from .views.audit_api import audit_router
|
||||||
from .views.auth_api import auth_router
|
from .views.auth_api import auth_router
|
||||||
|
from .views.callback_api import callback_router
|
||||||
from .views.extension_api import extension_router
|
from .views.extension_api import extension_router
|
||||||
|
from .views.fiat_api import fiat_router
|
||||||
|
|
||||||
# this compat is needed for usermanager extension
|
# this compat is needed for usermanager extension
|
||||||
from .views.generic import generic_router
|
from .views.generic import generic_router
|
||||||
@@ -34,10 +36,12 @@ def init_core_routers(app: FastAPI):
|
|||||||
app.include_router(wallet_router)
|
app.include_router(wallet_router)
|
||||||
app.include_router(api_router)
|
app.include_router(api_router)
|
||||||
app.include_router(websocket_router)
|
app.include_router(websocket_router)
|
||||||
|
app.include_router(callback_router)
|
||||||
app.include_router(tinyurl_router)
|
app.include_router(tinyurl_router)
|
||||||
app.include_router(webpush_router)
|
app.include_router(webpush_router)
|
||||||
app.include_router(users_router)
|
app.include_router(users_router)
|
||||||
app.include_router(audit_router)
|
app.include_router(audit_router)
|
||||||
|
app.include_router(fiat_router)
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["core_app", "core_app_extra", "db"]
|
__all__ = ["core_app", "core_app_extra", "db"]
|
||||||
|
|||||||
@@ -85,87 +85,78 @@ from .webpush import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# audit
|
|
||||||
"create_audit_entry",
|
|
||||||
# db_versions
|
|
||||||
"get_db_version",
|
|
||||||
"get_db_versions",
|
|
||||||
"update_migration_version",
|
|
||||||
"delete_dbversion",
|
|
||||||
# extensions
|
|
||||||
"create_installed_extension",
|
|
||||||
"create_user_extension",
|
|
||||||
"delete_installed_extension",
|
|
||||||
"drop_extension_db",
|
|
||||||
"get_installed_extension",
|
|
||||||
"get_installed_extensions",
|
|
||||||
"get_user_active_extensions_ids",
|
|
||||||
"get_user_extension",
|
|
||||||
"update_installed_extension",
|
|
||||||
"update_installed_extension_state",
|
|
||||||
"update_user_extension",
|
|
||||||
"get_user_extensions",
|
|
||||||
# payments
|
|
||||||
"DateTrunc",
|
"DateTrunc",
|
||||||
"check_internal",
|
"check_internal",
|
||||||
"create_payment",
|
|
||||||
"delete_expired_invoices",
|
|
||||||
"delete_wallet_payment",
|
|
||||||
"get_latest_payments_by_extension",
|
|
||||||
"get_payment",
|
|
||||||
"get_payments",
|
|
||||||
"get_payments_history",
|
|
||||||
"get_payments_paginated",
|
|
||||||
"get_standalone_payment",
|
|
||||||
"get_wallet_payment",
|
|
||||||
"is_internal_status_success",
|
|
||||||
"mark_webhook_sent",
|
|
||||||
"update_payment",
|
|
||||||
"update_payment_checking_id",
|
|
||||||
"update_payment_extra",
|
|
||||||
# settings
|
|
||||||
"create_admin_settings",
|
|
||||||
"delete_admin_settings",
|
|
||||||
"get_admin_settings",
|
|
||||||
"get_super_settings",
|
|
||||||
"update_admin_settings",
|
|
||||||
"update_super_user",
|
|
||||||
"reset_core_settings",
|
|
||||||
# tinyurl
|
|
||||||
"create_tinyurl",
|
|
||||||
"delete_tinyurl",
|
|
||||||
"get_tinyurl",
|
|
||||||
"get_tinyurl_by_url",
|
|
||||||
# users
|
|
||||||
"create_account",
|
"create_account",
|
||||||
|
"create_admin_settings",
|
||||||
|
"create_audit_entry",
|
||||||
|
"create_installed_extension",
|
||||||
|
"create_payment",
|
||||||
|
"create_tinyurl",
|
||||||
|
"create_user_extension",
|
||||||
|
"create_wallet",
|
||||||
|
"create_webpush_subscription",
|
||||||
"delete_account",
|
"delete_account",
|
||||||
"delete_accounts_no_wallets",
|
"delete_accounts_no_wallets",
|
||||||
|
"delete_admin_settings",
|
||||||
|
"delete_dbversion",
|
||||||
|
"delete_expired_invoices",
|
||||||
|
"delete_installed_extension",
|
||||||
|
"delete_tinyurl",
|
||||||
|
"delete_unused_wallets",
|
||||||
|
"delete_wallet",
|
||||||
|
"delete_wallet_by_id",
|
||||||
|
"delete_wallet_payment",
|
||||||
|
"delete_webpush_subscription",
|
||||||
|
"delete_webpush_subscriptions",
|
||||||
|
"drop_extension_db",
|
||||||
|
"force_delete_wallet",
|
||||||
"get_account",
|
"get_account",
|
||||||
"get_account_by_email",
|
"get_account_by_email",
|
||||||
"get_account_by_pubkey",
|
"get_account_by_pubkey",
|
||||||
"get_account_by_username",
|
"get_account_by_username",
|
||||||
"get_account_by_username_or_email",
|
"get_account_by_username_or_email",
|
||||||
"get_accounts",
|
"get_accounts",
|
||||||
"get_user",
|
"get_admin_settings",
|
||||||
"get_user_from_account",
|
"get_db_version",
|
||||||
"get_user_access_control_lists",
|
"get_db_versions",
|
||||||
"update_account",
|
"get_installed_extension",
|
||||||
# wallets
|
"get_installed_extensions",
|
||||||
"create_wallet",
|
"get_latest_payments_by_extension",
|
||||||
"delete_unused_wallets",
|
"get_payment",
|
||||||
"delete_wallet",
|
"get_payments",
|
||||||
"delete_wallet_by_id",
|
"get_payments_history",
|
||||||
"force_delete_wallet",
|
"get_payments_paginated",
|
||||||
|
"get_standalone_payment",
|
||||||
|
"get_super_settings",
|
||||||
|
"get_tinyurl",
|
||||||
|
"get_tinyurl_by_url",
|
||||||
"get_total_balance",
|
"get_total_balance",
|
||||||
|
"get_user",
|
||||||
|
"get_user_access_control_lists",
|
||||||
|
"get_user_active_extensions_ids",
|
||||||
|
"get_user_extension",
|
||||||
|
"get_user_extensions",
|
||||||
|
"get_user_from_account",
|
||||||
"get_wallet",
|
"get_wallet",
|
||||||
"get_wallet_for_key",
|
"get_wallet_for_key",
|
||||||
|
"get_wallet_payment",
|
||||||
"get_wallets",
|
"get_wallets",
|
||||||
"remove_deleted_wallets",
|
|
||||||
"update_wallet",
|
|
||||||
# webpush
|
|
||||||
"create_webpush_subscription",
|
|
||||||
"delete_webpush_subscription",
|
|
||||||
"delete_webpush_subscriptions",
|
|
||||||
"get_webpush_subscription",
|
"get_webpush_subscription",
|
||||||
"get_webpush_subscriptions_for_user",
|
"get_webpush_subscriptions_for_user",
|
||||||
|
"is_internal_status_success",
|
||||||
|
"mark_webhook_sent",
|
||||||
|
"remove_deleted_wallets",
|
||||||
|
"reset_core_settings",
|
||||||
|
"update_account",
|
||||||
|
"update_admin_settings",
|
||||||
|
"update_installed_extension",
|
||||||
|
"update_installed_extension_state",
|
||||||
|
"update_migration_version",
|
||||||
|
"update_payment",
|
||||||
|
"update_payment_checking_id",
|
||||||
|
"update_payment_extra",
|
||||||
|
"update_super_user",
|
||||||
|
"update_user_extension",
|
||||||
|
"update_wallet",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from time import time
|
from time import time
|
||||||
from typing import Any, Optional, Tuple
|
from typing import Any, Optional
|
||||||
|
|
||||||
from lnbits.core.crud.wallets import get_total_balance, get_wallet, get_wallets_ids
|
from lnbits.core.crud.wallets import get_total_balance, get_wallet, get_wallets_ids
|
||||||
from lnbits.core.db import db
|
from lnbits.core.db import db
|
||||||
@@ -394,7 +394,7 @@ async def get_daily_stats(
|
|||||||
filters: Optional[Filters[PaymentFilters]] = None,
|
filters: Optional[Filters[PaymentFilters]] = None,
|
||||||
user_id: Optional[str] = None,
|
user_id: Optional[str] = None,
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> Tuple[list[PaymentDailyStats], list[PaymentDailyStats]]:
|
) -> tuple[list[PaymentDailyStats], list[PaymentDailyStats]]:
|
||||||
|
|
||||||
if not filters:
|
if not filters:
|
||||||
filters = Filters()
|
filters = Filters()
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ async def get_accounts(
|
|||||||
accounts.username,
|
accounts.username,
|
||||||
accounts.email,
|
accounts.email,
|
||||||
accounts.pubkey,
|
accounts.pubkey,
|
||||||
|
accounts.external_id,
|
||||||
SUM(COALESCE((
|
SUM(COALESCE((
|
||||||
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
||||||
), 0)) as balance_msat,
|
), 0)) as balance_msat,
|
||||||
@@ -128,8 +129,8 @@ async def get_account_by_username(
|
|||||||
if len(username) == 0:
|
if len(username) == 0:
|
||||||
return None
|
return None
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"SELECT * FROM accounts WHERE username = :username",
|
"SELECT * FROM accounts WHERE LOWER(username) = :username",
|
||||||
{"username": username},
|
{"username": username.lower()},
|
||||||
Account,
|
Account,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -138,8 +139,8 @@ async def get_account_by_pubkey(
|
|||||||
pubkey: str, conn: Optional[Connection] = None
|
pubkey: str, conn: Optional[Connection] = None
|
||||||
) -> Optional[Account]:
|
) -> Optional[Account]:
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"SELECT * FROM accounts WHERE pubkey = :pubkey",
|
"SELECT * FROM accounts WHERE LOWER(pubkey) = :pubkey",
|
||||||
{"pubkey": pubkey},
|
{"pubkey": pubkey.lower()},
|
||||||
Account,
|
Account,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -150,8 +151,8 @@ async def get_account_by_email(
|
|||||||
if len(email) == 0:
|
if len(email) == 0:
|
||||||
return None
|
return None
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"SELECT * FROM accounts WHERE email = :email",
|
"SELECT * FROM accounts WHERE LOWER(email) = :email",
|
||||||
{"email": email},
|
{"email": email.lower()},
|
||||||
Account,
|
Account,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -160,8 +161,11 @@ async def get_account_by_username_or_email(
|
|||||||
username_or_email: str, conn: Optional[Connection] = None
|
username_or_email: str, conn: Optional[Connection] = None
|
||||||
) -> Optional[Account]:
|
) -> Optional[Account]:
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"SELECT * FROM accounts WHERE email = :value or username = :value",
|
"""
|
||||||
{"value": username_or_email},
|
SELECT * FROM accounts
|
||||||
|
WHERE LOWER(email) = :value or LOWER(username) = :value
|
||||||
|
""",
|
||||||
|
{"value": username_or_email.lower()},
|
||||||
Account,
|
Account,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -183,6 +187,7 @@ async def get_user_from_account(
|
|||||||
email=account.email,
|
email=account.email,
|
||||||
username=account.username,
|
username=account.username,
|
||||||
pubkey=account.pubkey,
|
pubkey=account.pubkey,
|
||||||
|
external_id=account.external_id,
|
||||||
extra=account.extra,
|
extra=account.extra,
|
||||||
created_at=account.created_at,
|
created_at=account.created_at,
|
||||||
updated_at=account.updated_at,
|
updated_at=account.updated_at,
|
||||||
@@ -190,6 +195,7 @@ async def get_user_from_account(
|
|||||||
wallets=wallets,
|
wallets=wallets,
|
||||||
admin=account.is_admin,
|
admin=account.is_admin,
|
||||||
super_user=account.is_super_user,
|
super_user=account.is_super_user,
|
||||||
|
fiat_providers=account.fiat_providers,
|
||||||
has_password=account.password_hash is not None,
|
has_password=account.password_hash is not None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -711,3 +711,15 @@ async def m031_add_color_and_icon_to_wallets(db: Connection):
|
|||||||
Adds icon and color columns to wallets.
|
Adds icon and color columns to wallets.
|
||||||
"""
|
"""
|
||||||
await db.execute("ALTER TABLE wallets ADD COLUMN extra TEXT")
|
await db.execute("ALTER TABLE wallets ADD COLUMN extra TEXT")
|
||||||
|
|
||||||
|
|
||||||
|
async def m032_add_external_id_to_accounts(db: Connection):
|
||||||
|
"""
|
||||||
|
Adds external_id column to accounts.
|
||||||
|
Used for external account linking.
|
||||||
|
"""
|
||||||
|
await db.execute("ALTER TABLE accounts ADD COLUMN external_id TEXT")
|
||||||
|
|
||||||
|
|
||||||
|
async def m033_update_payment_table(db: Connection):
|
||||||
|
await db.execute("ALTER TABLE apipayments ADD COLUMN fiat_provider TEXT")
|
||||||
|
|||||||
@@ -48,62 +48,54 @@ from .wallets import BaseWallet, CreateWallet, KeyType, Wallet, WalletTypeInfo
|
|||||||
from .webpush import CreateWebPushSubscription, WebPushSubscription
|
from .webpush import CreateWebPushSubscription, WebPushSubscription
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# audit
|
|
||||||
"AuditEntry",
|
|
||||||
"AuditFilters",
|
|
||||||
# lnurl
|
|
||||||
"CreateLnurl",
|
|
||||||
"CreateLnurlAuth",
|
|
||||||
"PayLnurlWData",
|
|
||||||
# misc
|
|
||||||
"BalanceDelta",
|
|
||||||
"Callback",
|
|
||||||
"ConversionData",
|
|
||||||
"CoreAppExtra",
|
|
||||||
"DbVersion",
|
|
||||||
"SimpleStatus",
|
|
||||||
# payments
|
|
||||||
"CreateInvoice",
|
|
||||||
"CreatePayment",
|
|
||||||
"DecodePayment",
|
|
||||||
"PayInvoice",
|
|
||||||
"Payment",
|
|
||||||
"PaymentCountField",
|
|
||||||
"PaymentCountStat",
|
|
||||||
"PaymentDailyStats",
|
|
||||||
"PaymentsStatusCount",
|
|
||||||
"PaymentWalletStats",
|
|
||||||
"PaymentExtra",
|
|
||||||
"PaymentFilters",
|
|
||||||
"PaymentHistoryPoint",
|
|
||||||
"PaymentState",
|
|
||||||
# tinyurl
|
|
||||||
"TinyURL",
|
|
||||||
# users
|
|
||||||
"AccessTokenPayload",
|
"AccessTokenPayload",
|
||||||
"Account",
|
"Account",
|
||||||
"AccountFilters",
|
"AccountFilters",
|
||||||
"AccountOverview",
|
"AccountOverview",
|
||||||
"UserAcls",
|
"AuditEntry",
|
||||||
|
"AuditFilters",
|
||||||
|
"BalanceDelta",
|
||||||
|
"BaseWallet",
|
||||||
|
"Callback",
|
||||||
|
"ConversionData",
|
||||||
|
"CoreAppExtra",
|
||||||
|
"CreateInvoice",
|
||||||
|
"CreateLnurl",
|
||||||
|
"CreateLnurlAuth",
|
||||||
|
"CreatePayment",
|
||||||
"CreateUser",
|
"CreateUser",
|
||||||
"RegisterUser",
|
"CreateWallet",
|
||||||
|
"CreateWebPushSubscription",
|
||||||
|
"DbVersion",
|
||||||
|
"DecodePayment",
|
||||||
|
"KeyType",
|
||||||
"LoginUsernamePassword",
|
"LoginUsernamePassword",
|
||||||
"LoginUsr",
|
"LoginUsr",
|
||||||
|
"PayInvoice",
|
||||||
|
"PayLnurlWData",
|
||||||
|
"Payment",
|
||||||
|
"PaymentCountField",
|
||||||
|
"PaymentCountStat",
|
||||||
|
"PaymentDailyStats",
|
||||||
|
"PaymentExtra",
|
||||||
|
"PaymentFilters",
|
||||||
|
"PaymentHistoryPoint",
|
||||||
|
"PaymentState",
|
||||||
|
"PaymentWalletStats",
|
||||||
|
"PaymentsStatusCount",
|
||||||
|
"RegisterUser",
|
||||||
"ResetUserPassword",
|
"ResetUserPassword",
|
||||||
|
"SimpleStatus",
|
||||||
|
"TinyURL",
|
||||||
"UpdateBalance",
|
"UpdateBalance",
|
||||||
"UpdateSuperuserPassword",
|
"UpdateSuperuserPassword",
|
||||||
"UpdateUser",
|
"UpdateUser",
|
||||||
"UpdateUserPassword",
|
"UpdateUserPassword",
|
||||||
"UpdateUserPubkey",
|
"UpdateUserPubkey",
|
||||||
"User",
|
"User",
|
||||||
|
"UserAcls",
|
||||||
"UserExtra",
|
"UserExtra",
|
||||||
# wallets
|
|
||||||
"BaseWallet",
|
|
||||||
"CreateWallet",
|
|
||||||
"KeyType",
|
|
||||||
"Wallet",
|
"Wallet",
|
||||||
"WalletTypeInfo",
|
"WalletTypeInfo",
|
||||||
# webpush
|
|
||||||
"CreateWebPushSubscription",
|
|
||||||
"WebPushSubscription",
|
"WebPushSubscription",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -8,6 +8,13 @@ from fastapi import Query
|
|||||||
from pydantic import BaseModel, Field, validator
|
from pydantic import BaseModel, Field, validator
|
||||||
|
|
||||||
from lnbits.db import FilterModel
|
from lnbits.db import FilterModel
|
||||||
|
from lnbits.fiat import get_fiat_provider
|
||||||
|
from lnbits.fiat.base import (
|
||||||
|
FiatPaymentFailedStatus,
|
||||||
|
FiatPaymentPendingStatus,
|
||||||
|
FiatPaymentStatus,
|
||||||
|
FiatPaymentSuccessStatus,
|
||||||
|
)
|
||||||
from lnbits.utils.exchange_rates import allowed_currencies
|
from lnbits.utils.exchange_rates import allowed_currencies
|
||||||
from lnbits.wallets import get_funding_source
|
from lnbits.wallets import get_funding_source
|
||||||
from lnbits.wallets.base import (
|
from lnbits.wallets.base import (
|
||||||
@@ -60,6 +67,8 @@ class Payment(BaseModel):
|
|||||||
amount: int
|
amount: int
|
||||||
fee: int
|
fee: int
|
||||||
bolt11: str
|
bolt11: str
|
||||||
|
# payment_request: str | None
|
||||||
|
fiat_provider: str | None = None
|
||||||
status: str = PaymentState.PENDING
|
status: str = PaymentState.PENDING
|
||||||
memo: str | None = None
|
memo: str | None = None
|
||||||
expiry: datetime | None = None
|
expiry: datetime | None = None
|
||||||
@@ -107,14 +116,23 @@ class Payment(BaseModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_internal(self) -> bool:
|
def is_internal(self) -> bool:
|
||||||
return self.checking_id.startswith("internal_")
|
return self.checking_id.startswith("internal_") or self.checking_id.startswith(
|
||||||
|
"fiat_"
|
||||||
|
)
|
||||||
|
|
||||||
async def check_status(self) -> PaymentStatus:
|
async def check_status(
|
||||||
|
self, skip_internal_payment_notifications: bool | None = False
|
||||||
|
) -> PaymentStatus:
|
||||||
if self.is_internal:
|
if self.is_internal:
|
||||||
if self.success:
|
if self.success:
|
||||||
return PaymentSuccessStatus()
|
return PaymentSuccessStatus()
|
||||||
if self.failed:
|
if self.failed:
|
||||||
return PaymentFailedStatus()
|
return PaymentFailedStatus()
|
||||||
|
if self.is_in and self.fiat_provider:
|
||||||
|
fiat_status = await self.check_fiat_status(
|
||||||
|
skip_internal_payment_notifications
|
||||||
|
)
|
||||||
|
return PaymentStatus(paid=fiat_status.paid)
|
||||||
return PaymentPendingStatus()
|
return PaymentPendingStatus()
|
||||||
funding_source = get_funding_source()
|
funding_source = get_funding_source()
|
||||||
if self.is_out:
|
if self.is_out:
|
||||||
@@ -123,6 +141,39 @@ class Payment(BaseModel):
|
|||||||
status = await funding_source.get_invoice_status(self.checking_id)
|
status = await funding_source.get_invoice_status(self.checking_id)
|
||||||
return status
|
return status
|
||||||
|
|
||||||
|
async def check_fiat_status(
|
||||||
|
self, skip_internal_payment_notifications: bool | None = False
|
||||||
|
) -> FiatPaymentStatus:
|
||||||
|
if not self.is_internal:
|
||||||
|
return FiatPaymentPendingStatus()
|
||||||
|
if self.success:
|
||||||
|
return FiatPaymentSuccessStatus()
|
||||||
|
if self.failed:
|
||||||
|
return FiatPaymentFailedStatus()
|
||||||
|
|
||||||
|
if not self.fiat_provider:
|
||||||
|
return FiatPaymentPendingStatus()
|
||||||
|
|
||||||
|
checking_id = self.extra.get("fiat_checking_id")
|
||||||
|
if not checking_id:
|
||||||
|
return FiatPaymentPendingStatus()
|
||||||
|
|
||||||
|
fiat_provider = await get_fiat_provider(self.fiat_provider)
|
||||||
|
if not fiat_provider:
|
||||||
|
return FiatPaymentPendingStatus()
|
||||||
|
fiat_status = await fiat_provider.get_invoice_status(checking_id)
|
||||||
|
|
||||||
|
if skip_internal_payment_notifications:
|
||||||
|
return fiat_status
|
||||||
|
|
||||||
|
if fiat_status.success:
|
||||||
|
# notify receivers asynchronously
|
||||||
|
from lnbits.tasks import internal_invoice_queue
|
||||||
|
|
||||||
|
await internal_invoice_queue.put(self.checking_id)
|
||||||
|
|
||||||
|
return fiat_status
|
||||||
|
|
||||||
|
|
||||||
class PaymentFilters(FilterModel):
|
class PaymentFilters(FilterModel):
|
||||||
__search_fields__ = ["memo", "amount", "wallet_id", "tag", "status", "time"]
|
__search_fields__ = ["memo", "amount", "wallet_id", "tag", "status", "time"]
|
||||||
@@ -206,6 +257,7 @@ class CreateInvoice(BaseModel):
|
|||||||
webhook: str | None = None
|
webhook: str | None = None
|
||||||
bolt11: str | None = None
|
bolt11: str | None = None
|
||||||
lnurl_callback: str | None = None
|
lnurl_callback: str | None = None
|
||||||
|
fiat_provider: str | None = None
|
||||||
|
|
||||||
@validator("unit")
|
@validator("unit")
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
"""Keycloak SSO Login Helper
|
"""Keycloak SSO Login Helper"""
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,12 @@ from pydantic import BaseModel, Field
|
|||||||
|
|
||||||
from lnbits.core.models.misc import SimpleItem
|
from lnbits.core.models.misc import SimpleItem
|
||||||
from lnbits.db import FilterModel
|
from lnbits.db import FilterModel
|
||||||
from lnbits.helpers import is_valid_email_address, is_valid_pubkey, is_valid_username
|
from lnbits.helpers import (
|
||||||
|
is_valid_email_address,
|
||||||
|
is_valid_external_id,
|
||||||
|
is_valid_pubkey,
|
||||||
|
is_valid_username,
|
||||||
|
)
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
from .wallets import Wallet
|
from .wallets import Wallet
|
||||||
@@ -93,6 +98,7 @@ class UserAcls(BaseModel):
|
|||||||
|
|
||||||
class Account(BaseModel):
|
class Account(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
|
external_id: str | None = None # for external account linking
|
||||||
username: str | None = None
|
username: str | None = None
|
||||||
password_hash: str | None = None
|
password_hash: str | None = None
|
||||||
pubkey: str | None = None
|
pubkey: str | None = None
|
||||||
@@ -104,11 +110,13 @@ class Account(BaseModel):
|
|||||||
|
|
||||||
is_super_user: bool = Field(default=False, no_database=True)
|
is_super_user: bool = Field(default=False, no_database=True)
|
||||||
is_admin: bool = Field(default=False, no_database=True)
|
is_admin: bool = Field(default=False, no_database=True)
|
||||||
|
fiat_providers: list[str] = Field(default=[], no_database=True)
|
||||||
|
|
||||||
def __init__(self, **data):
|
def __init__(self, **data):
|
||||||
super().__init__(**data)
|
super().__init__(**data)
|
||||||
self.is_super_user = settings.is_super_user(self.id)
|
self.is_super_user = settings.is_super_user(self.id)
|
||||||
self.is_admin = settings.is_admin_user(self.id)
|
self.is_admin = settings.is_admin_user(self.id)
|
||||||
|
self.fiat_providers = settings.get_fiat_providers_for_user(self.id)
|
||||||
|
|
||||||
def hash_password(self, password: str) -> str:
|
def hash_password(self, password: str) -> str:
|
||||||
"""sets and returns the hashed password"""
|
"""sets and returns the hashed password"""
|
||||||
@@ -130,6 +138,11 @@ class Account(BaseModel):
|
|||||||
raise ValueError("Invalid email.")
|
raise ValueError("Invalid email.")
|
||||||
if self.pubkey and not is_valid_pubkey(self.pubkey):
|
if self.pubkey and not is_valid_pubkey(self.pubkey):
|
||||||
raise ValueError("Invalid pubkey.")
|
raise ValueError("Invalid pubkey.")
|
||||||
|
if self.external_id and not is_valid_external_id(self.external_id):
|
||||||
|
raise ValueError(
|
||||||
|
"Invalid external id. Max length is 256 characters. "
|
||||||
|
"Space and newlines are not allowed."
|
||||||
|
)
|
||||||
user_uuid4 = UUID(hex=self.id, version=4)
|
user_uuid4 = UUID(hex=self.id, version=4)
|
||||||
if user_uuid4.hex != self.id:
|
if user_uuid4.hex != self.id:
|
||||||
raise ValueError("User ID is not valid UUID4 hex string.")
|
raise ValueError("User ID is not valid UUID4 hex string.")
|
||||||
@@ -143,7 +156,14 @@ class AccountOverview(Account):
|
|||||||
|
|
||||||
|
|
||||||
class AccountFilters(FilterModel):
|
class AccountFilters(FilterModel):
|
||||||
__search_fields__ = ["user", "email", "username", "pubkey", "wallet_id"]
|
__search_fields__ = [
|
||||||
|
"user",
|
||||||
|
"email",
|
||||||
|
"username",
|
||||||
|
"pubkey",
|
||||||
|
"external_id",
|
||||||
|
"wallet_id",
|
||||||
|
]
|
||||||
__sort_fields__ = [
|
__sort_fields__ = [
|
||||||
"balance_msat",
|
"balance_msat",
|
||||||
"email",
|
"email",
|
||||||
@@ -157,6 +177,7 @@ class AccountFilters(FilterModel):
|
|||||||
user: str | None = None
|
user: str | None = None
|
||||||
username: str | None = None
|
username: str | None = None
|
||||||
pubkey: str | None = None
|
pubkey: str | None = None
|
||||||
|
external_id: str | None = None
|
||||||
wallet_id: str | None = None
|
wallet_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@@ -167,10 +188,12 @@ class User(BaseModel):
|
|||||||
email: str | None = None
|
email: str | None = None
|
||||||
username: str | None = None
|
username: str | None = None
|
||||||
pubkey: str | None = None
|
pubkey: str | None = None
|
||||||
|
external_id: str | None = None # for external account linking
|
||||||
extensions: list[str] = []
|
extensions: list[str] = []
|
||||||
wallets: list[Wallet] = []
|
wallets: list[Wallet] = []
|
||||||
admin: bool = False
|
admin: bool = False
|
||||||
super_user: bool = False
|
super_user: bool = False
|
||||||
|
fiat_providers: list[str] = []
|
||||||
has_password: bool = False
|
has_password: bool = False
|
||||||
extra: UserExtra = UserExtra()
|
extra: UserExtra = UserExtra()
|
||||||
|
|
||||||
@@ -207,13 +230,13 @@ class CreateUser(BaseModel):
|
|||||||
password: str | None = Query(default=None, min_length=8, max_length=50)
|
password: str | None = Query(default=None, min_length=8, max_length=50)
|
||||||
password_repeat: str | None = Query(default=None, min_length=8, max_length=50)
|
password_repeat: str | None = Query(default=None, min_length=8, max_length=50)
|
||||||
pubkey: str = Query(default=None, max_length=64)
|
pubkey: str = Query(default=None, max_length=64)
|
||||||
|
external_id: str = Query(default=None, max_length=256)
|
||||||
extensions: list[str] | None = None
|
extensions: list[str] | None = None
|
||||||
extra: UserExtra | None = None
|
extra: UserExtra | None = None
|
||||||
|
|
||||||
|
|
||||||
class UpdateUser(BaseModel):
|
class UpdateUser(BaseModel):
|
||||||
user_id: str
|
user_id: str
|
||||||
email: str | None = Query(default=None)
|
|
||||||
username: str | None = Query(default=..., min_length=2, max_length=20)
|
username: str | None = Query(default=..., min_length=2, max_length=20)
|
||||||
extra: UserExtra | None = None
|
extra: UserExtra | None = None
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,15 @@ from .payments import (
|
|||||||
calculate_fiat_amounts,
|
calculate_fiat_amounts,
|
||||||
check_transaction_status,
|
check_transaction_status,
|
||||||
check_wallet_limits,
|
check_wallet_limits,
|
||||||
|
create_fiat_invoice,
|
||||||
create_invoice,
|
create_invoice,
|
||||||
|
create_wallet_invoice,
|
||||||
fee_reserve,
|
fee_reserve,
|
||||||
fee_reserve_total,
|
fee_reserve_total,
|
||||||
|
get_payments_daily_stats,
|
||||||
pay_invoice,
|
pay_invoice,
|
||||||
service_fee,
|
service_fee,
|
||||||
|
update_pending_payment,
|
||||||
update_pending_payments,
|
update_pending_payments,
|
||||||
update_wallet_balance,
|
update_wallet_balance,
|
||||||
)
|
)
|
||||||
@@ -30,36 +34,33 @@ from .users import (
|
|||||||
from .websockets import websocket_manager, websocket_updater
|
from .websockets import websocket_manager, websocket_updater
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# funding source
|
|
||||||
"get_balance_delta",
|
|
||||||
"switch_to_voidwallet",
|
|
||||||
# lnurl
|
|
||||||
"redeem_lnurl_withdraw",
|
|
||||||
"perform_lnurlauth",
|
|
||||||
# notifications
|
|
||||||
"enqueue_notification",
|
|
||||||
"send_payment_notification",
|
|
||||||
# payments
|
|
||||||
"calculate_fiat_amounts",
|
"calculate_fiat_amounts",
|
||||||
|
"check_admin_settings",
|
||||||
"check_transaction_status",
|
"check_transaction_status",
|
||||||
"check_wallet_limits",
|
"check_wallet_limits",
|
||||||
"create_invoice",
|
|
||||||
"fee_reserve",
|
|
||||||
"fee_reserve_total",
|
|
||||||
"pay_invoice",
|
|
||||||
"service_fee",
|
|
||||||
"update_pending_payments",
|
|
||||||
"update_wallet_balance",
|
|
||||||
# settings
|
|
||||||
"check_webpush_settings",
|
"check_webpush_settings",
|
||||||
"update_cached_settings",
|
"create_fiat_invoice",
|
||||||
# users
|
"create_invoice",
|
||||||
"check_admin_settings",
|
|
||||||
"create_user_account",
|
"create_user_account",
|
||||||
"create_user_account_no_ckeck",
|
"create_user_account_no_ckeck",
|
||||||
|
"create_wallet_invoice",
|
||||||
|
"enqueue_notification",
|
||||||
|
"fee_reserve",
|
||||||
|
"fee_reserve_total",
|
||||||
|
"get_balance_delta",
|
||||||
|
"get_payments_daily_stats",
|
||||||
|
"pay_invoice",
|
||||||
|
"perform_lnurlauth",
|
||||||
|
"redeem_lnurl_withdraw",
|
||||||
|
"send_payment_notification",
|
||||||
|
"service_fee",
|
||||||
|
"switch_to_voidwallet",
|
||||||
|
"update_cached_settings",
|
||||||
|
"update_pending_payment",
|
||||||
|
"update_pending_payments",
|
||||||
"update_user_account",
|
"update_user_account",
|
||||||
"update_user_extensions",
|
"update_user_extensions",
|
||||||
# websockets
|
"update_wallet_balance",
|
||||||
"websocket_manager",
|
"websocket_manager",
|
||||||
"websocket_updater",
|
"websocket_updater",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import time
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from lnbits.core.crud import get_wallet
|
||||||
|
from lnbits.core.crud.payments import create_payment, get_standalone_payment
|
||||||
|
from lnbits.core.models import CreatePayment, Payment, PaymentState
|
||||||
|
from lnbits.core.models.misc import SimpleStatus
|
||||||
|
from lnbits.db import Connection
|
||||||
|
from lnbits.fiat import get_fiat_provider
|
||||||
|
from lnbits.settings import settings
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_fiat_payment_confirmation(
|
||||||
|
payment: Payment, conn: Optional[Connection] = None
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
await _credit_fiat_service_fee_wallet(payment, conn=conn)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(e)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await _debit_fiat_service_faucet_wallet(payment, conn=conn)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(e)
|
||||||
|
|
||||||
|
|
||||||
|
async def _credit_fiat_service_fee_wallet(
|
||||||
|
payment: Payment, conn: Optional[Connection] = None
|
||||||
|
):
|
||||||
|
fiat_provider_name = payment.fiat_provider
|
||||||
|
if not fiat_provider_name:
|
||||||
|
return
|
||||||
|
if payment.fee == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
limits = settings.get_fiat_provider_limits(fiat_provider_name)
|
||||||
|
if not limits:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not limits.service_fee_wallet_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
memo = (
|
||||||
|
f"Service fee for fiat payment of "
|
||||||
|
f"{abs(payment.sat)} sats. "
|
||||||
|
f"Provider: {fiat_provider_name}. "
|
||||||
|
f"Wallet: '{payment.wallet_id}'."
|
||||||
|
)
|
||||||
|
create_payment_model = CreatePayment(
|
||||||
|
wallet_id=limits.service_fee_wallet_id,
|
||||||
|
bolt11=payment.bolt11,
|
||||||
|
payment_hash=payment.payment_hash,
|
||||||
|
amount_msat=abs(payment.fee),
|
||||||
|
memo=memo,
|
||||||
|
)
|
||||||
|
await create_payment(
|
||||||
|
checking_id=f"service_fee_{payment.payment_hash}",
|
||||||
|
data=create_payment_model,
|
||||||
|
status=PaymentState.SUCCESS,
|
||||||
|
conn=conn,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _debit_fiat_service_faucet_wallet(
|
||||||
|
payment: Payment, conn: Optional[Connection] = None
|
||||||
|
):
|
||||||
|
fiat_provider_name = payment.fiat_provider
|
||||||
|
if not fiat_provider_name:
|
||||||
|
return
|
||||||
|
|
||||||
|
limits = settings.get_fiat_provider_limits(fiat_provider_name)
|
||||||
|
if not limits:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not limits.service_faucet_wallet_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
faucet_wallet = await get_wallet(limits.service_faucet_wallet_id, conn=conn)
|
||||||
|
if not faucet_wallet:
|
||||||
|
raise ValueError(
|
||||||
|
f"Fiat provider '{fiat_provider_name}' faucet wallet not found."
|
||||||
|
)
|
||||||
|
|
||||||
|
memo = (
|
||||||
|
f"Faucet payment of {abs(payment.sat)} sats. "
|
||||||
|
f"Provider: {fiat_provider_name}. "
|
||||||
|
f"Wallet: '{payment.wallet_id}'."
|
||||||
|
)
|
||||||
|
create_payment_model = CreatePayment(
|
||||||
|
wallet_id=limits.service_faucet_wallet_id,
|
||||||
|
bolt11=payment.bolt11,
|
||||||
|
payment_hash=payment.payment_hash,
|
||||||
|
amount_msat=-abs(payment.amount),
|
||||||
|
memo=memo,
|
||||||
|
extra=payment.extra,
|
||||||
|
)
|
||||||
|
await create_payment(
|
||||||
|
checking_id=f"internal_fiat_{fiat_provider_name}_"
|
||||||
|
f"faucet_{payment.payment_hash}",
|
||||||
|
data=create_payment_model,
|
||||||
|
status=PaymentState.SUCCESS,
|
||||||
|
conn=conn,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_stripe_event(event: dict):
|
||||||
|
event_id = event.get("id")
|
||||||
|
event_object = event.get("data", {}).get("object", {})
|
||||||
|
object_type = event_object.get("object")
|
||||||
|
payment_hash = event_object.get("metadata", {}).get("payment_hash")
|
||||||
|
logger.debug(
|
||||||
|
f"Handling Stripe event: '{event_id}'. Type: '{object_type}'."
|
||||||
|
f" Payment hash: '{payment_hash}'."
|
||||||
|
)
|
||||||
|
if not payment_hash:
|
||||||
|
logger.warning("Stripe event does not contain a payment hash.")
|
||||||
|
return
|
||||||
|
|
||||||
|
payment = await get_standalone_payment(payment_hash)
|
||||||
|
if not payment:
|
||||||
|
logger.warning(f"No payment found for hash: '{payment_hash}'.")
|
||||||
|
return
|
||||||
|
await payment.check_fiat_status()
|
||||||
|
|
||||||
|
|
||||||
|
def check_stripe_signature(
|
||||||
|
payload: bytes,
|
||||||
|
sig_header: Optional[str],
|
||||||
|
secret: Optional[str],
|
||||||
|
tolerance_seconds=300,
|
||||||
|
):
|
||||||
|
if not sig_header:
|
||||||
|
logger.warning("Stripe-Signature header is missing.")
|
||||||
|
raise ValueError("Stripe-Signature header is missing.")
|
||||||
|
|
||||||
|
if not secret:
|
||||||
|
logger.warning("Stripe webhook signing secret is not set.")
|
||||||
|
raise ValueError("Stripe webhook cannot be verified.")
|
||||||
|
|
||||||
|
# Split the Stripe-Signature header
|
||||||
|
items = dict(i.split("=") for i in sig_header.split(","))
|
||||||
|
timestamp = int(items["t"])
|
||||||
|
signature = items["v1"]
|
||||||
|
|
||||||
|
# Check timestamp tolerance
|
||||||
|
if abs(time.time() - timestamp) > tolerance_seconds:
|
||||||
|
logger.warning("Timestamp outside tolerance.")
|
||||||
|
logger.debug(
|
||||||
|
f"Current time: {time.time()}, "
|
||||||
|
f"Timestamp: {timestamp}, "
|
||||||
|
f"Tolerance: {tolerance_seconds} seconds"
|
||||||
|
)
|
||||||
|
|
||||||
|
raise ValueError("Timestamp outside tolerance." f"Timestamp: {timestamp}")
|
||||||
|
|
||||||
|
signed_payload = f"{timestamp}.{payload.decode()}"
|
||||||
|
|
||||||
|
# Compute HMAC SHA256 using the webhook secret
|
||||||
|
computed_signature = hmac.new(
|
||||||
|
key=secret.encode(), msg=signed_payload.encode(), digestmod=hashlib.sha256
|
||||||
|
).hexdigest()
|
||||||
|
|
||||||
|
# Compare signatures using constant time comparison
|
||||||
|
if hmac.compare_digest(computed_signature, signature) is not True:
|
||||||
|
logger.warning("Stripe signature verification failed.")
|
||||||
|
raise ValueError("Stripe signature verification failed.")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_connection(provider: str) -> SimpleStatus:
|
||||||
|
"""
|
||||||
|
Test the connection to Stripe by checking if the API key is valid.
|
||||||
|
This function should be called when setting up or testing the Stripe integration.
|
||||||
|
"""
|
||||||
|
fiat_provider = await get_fiat_provider(provider)
|
||||||
|
status = await fiat_provider.status()
|
||||||
|
if status.error_message:
|
||||||
|
return SimpleStatus(
|
||||||
|
success=False,
|
||||||
|
message=f"Cconnection test failed: {status.error_message}",
|
||||||
|
)
|
||||||
|
|
||||||
|
return SimpleStatus(
|
||||||
|
success=True,
|
||||||
|
message="Connection test successful." f" Balance: {status.balance}.",
|
||||||
|
)
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -48,7 +47,7 @@ async def send_nostr_dm(
|
|||||||
return dm_event.to_dict()
|
return dm_event.to_dict()
|
||||||
|
|
||||||
|
|
||||||
async def fetch_nip5_details(identifier: str) -> Tuple[str, list[str]]:
|
async def fetch_nip5_details(identifier: str) -> tuple[str, list[str]]:
|
||||||
identifier, domain = identifier.split("@")
|
identifier, domain = identifier.split("@")
|
||||||
if not identifier or not domain:
|
if not identifier or not domain:
|
||||||
raise ValueError("Invalid NIP5 identifier")
|
raise ValueError("Invalid NIP5 identifier")
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import smtplib
|
|||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import Optional, Tuple
|
from typing import Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -186,7 +186,7 @@ def is_message_type_enabled(message_type: NotificationType) -> bool:
|
|||||||
|
|
||||||
def _notification_message_to_text(
|
def _notification_message_to_text(
|
||||||
notification_message: NotificationMessage,
|
notification_message: NotificationMessage,
|
||||||
) -> Tuple[str, str]:
|
) -> tuple[str, str]:
|
||||||
message_type = notification_message.message_type.value
|
message_type = notification_message.message_type.value
|
||||||
meesage_value = NOTIFICATION_TEMPLATES.get(message_type, message_type)
|
meesage_value = NOTIFICATION_TEMPLATES.get(message_type, message_type)
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import json
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
import httpx
|
||||||
from bolt11 import Bolt11, MilliSatoshi, Tags
|
from bolt11 import Bolt11, MilliSatoshi, Tags
|
||||||
from bolt11 import decode as bolt11_decode
|
from bolt11 import decode as bolt11_decode
|
||||||
from bolt11 import encode as bolt11_encode
|
from bolt11 import encode as bolt11_encode
|
||||||
@@ -11,11 +13,14 @@ from loguru import logger
|
|||||||
from lnbits.core.crud.payments import get_daily_stats
|
from lnbits.core.crud.payments import get_daily_stats
|
||||||
from lnbits.core.db import db
|
from lnbits.core.db import db
|
||||||
from lnbits.core.models import PaymentDailyStats, PaymentFilters
|
from lnbits.core.models import PaymentDailyStats, PaymentFilters
|
||||||
|
from lnbits.core.models.payments import CreateInvoice
|
||||||
from lnbits.db import Connection, Filters
|
from lnbits.db import Connection, Filters
|
||||||
from lnbits.decorators import check_user_extension_access
|
from lnbits.decorators import check_user_extension_access
|
||||||
from lnbits.exceptions import InvoiceError, PaymentError
|
from lnbits.exceptions import InvoiceError, PaymentError
|
||||||
|
from lnbits.fiat import get_fiat_provider
|
||||||
|
from lnbits.helpers import check_callback_url
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.tasks import create_task
|
from lnbits.tasks import create_task, internal_invoice_queue_put
|
||||||
from lnbits.utils.crypto import fake_privkey, random_secret_and_hash
|
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.utils.exchange_rates import fiat_amount_as_satoshis, satoshis_amount_as_fiat
|
||||||
from lnbits.wallets import fake_wallet, get_funding_source
|
from lnbits.wallets import fake_wallet, get_funding_source
|
||||||
@@ -94,6 +99,122 @@ async def pay_invoice(
|
|||||||
return payment
|
return payment
|
||||||
|
|
||||||
|
|
||||||
|
async def create_fiat_invoice(
|
||||||
|
wallet_id: str, invoice_data: CreateInvoice, conn: Optional[Connection] = None
|
||||||
|
):
|
||||||
|
fiat_provider_name = invoice_data.fiat_provider
|
||||||
|
if not fiat_provider_name:
|
||||||
|
raise ValueError("Fiat provider is required for fiat invoices.")
|
||||||
|
if not settings.is_fiat_provider_enabled(fiat_provider_name):
|
||||||
|
raise ValueError(
|
||||||
|
f"Fiat provider '{fiat_provider_name}' is not enabled.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if invoice_data.unit == "sat":
|
||||||
|
raise ValueError("Fiat provider cannot be used with satoshis.")
|
||||||
|
amount_sat = await fiat_amount_as_satoshis(invoice_data.amount, invoice_data.unit)
|
||||||
|
await _check_fiat_invoice_limits(amount_sat, fiat_provider_name, conn)
|
||||||
|
|
||||||
|
invoice_data.internal = True # use FakeWallet for fiat invoices
|
||||||
|
if not invoice_data.memo:
|
||||||
|
invoice_data.memo = settings.lnbits_site_title + f" ({fiat_provider_name})"
|
||||||
|
|
||||||
|
internal_payment = await create_wallet_invoice(wallet_id, invoice_data)
|
||||||
|
|
||||||
|
fiat_provider = await get_fiat_provider(fiat_provider_name)
|
||||||
|
fiat_invoice = await fiat_provider.create_invoice(
|
||||||
|
amount=invoice_data.amount,
|
||||||
|
payment_hash=internal_payment.payment_hash,
|
||||||
|
currency=invoice_data.unit,
|
||||||
|
memo=invoice_data.memo,
|
||||||
|
)
|
||||||
|
if fiat_invoice.failed:
|
||||||
|
logger.warning(fiat_invoice.error_message)
|
||||||
|
internal_payment.status = PaymentState.FAILED
|
||||||
|
await update_payment(internal_payment, conn=conn)
|
||||||
|
raise ValueError(
|
||||||
|
f"Cannot create payment request for '{fiat_provider_name}'.",
|
||||||
|
)
|
||||||
|
|
||||||
|
internal_payment.fee = -abs(
|
||||||
|
service_fee_fiat(internal_payment.msat, fiat_provider_name)
|
||||||
|
)
|
||||||
|
|
||||||
|
internal_payment.fiat_provider = fiat_provider_name
|
||||||
|
internal_payment.extra["fiat_checking_id"] = fiat_invoice.checking_id
|
||||||
|
# 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}"
|
||||||
|
)
|
||||||
|
await update_payment(internal_payment, new_checking_id, conn=conn)
|
||||||
|
internal_payment.checking_id = new_checking_id
|
||||||
|
|
||||||
|
return internal_payment
|
||||||
|
|
||||||
|
|
||||||
|
async def create_wallet_invoice(wallet_id: str, data: CreateInvoice) -> Payment:
|
||||||
|
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:
|
||||||
|
try:
|
||||||
|
description_hash = bytes.fromhex(data.description_hash)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ValueError(
|
||||||
|
"'description_hash' must be a valid hex string"
|
||||||
|
) from exc
|
||||||
|
if data.unhashed_description:
|
||||||
|
try:
|
||||||
|
unhashed_description = bytes.fromhex(data.unhashed_description)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ValueError(
|
||||||
|
"'unhashed_description' must be a valid hex string",
|
||||||
|
) from exc
|
||||||
|
# do not save memo if description_hash or unhashed_description is set
|
||||||
|
memo = ""
|
||||||
|
|
||||||
|
payment = await create_invoice(
|
||||||
|
wallet_id=wallet_id,
|
||||||
|
amount=data.amount,
|
||||||
|
memo=memo,
|
||||||
|
currency=data.unit,
|
||||||
|
description_hash=description_hash,
|
||||||
|
unhashed_description=unhashed_description,
|
||||||
|
expiry=data.expiry,
|
||||||
|
extra=data.extra,
|
||||||
|
webhook=data.webhook,
|
||||||
|
internal=data.internal,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
|
||||||
async def create_invoice(
|
async def create_invoice(
|
||||||
*,
|
*,
|
||||||
wallet_id: str,
|
wallet_id: str,
|
||||||
@@ -226,6 +347,26 @@ def service_fee(amount_msat: int, internal: bool = False) -> int:
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def service_fee_fiat(amount_msat: int, fiat_provider_name: str) -> int:
|
||||||
|
"""
|
||||||
|
Calculate the service fee for a fiat provider based on the amount in msat.
|
||||||
|
Return the fee in msat.
|
||||||
|
"""
|
||||||
|
limits = settings.get_fiat_provider_limits(fiat_provider_name)
|
||||||
|
if not limits:
|
||||||
|
return 0
|
||||||
|
amount_msat = abs(amount_msat)
|
||||||
|
fee_max = limits.service_max_fee_sats * 1000
|
||||||
|
if not limits.service_fee_wallet_id:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
fee_percentage = int(amount_msat / 100 * limits.service_fee_percent)
|
||||||
|
if fee_max > 0 and fee_percentage > fee_max:
|
||||||
|
return fee_max
|
||||||
|
else:
|
||||||
|
return fee_percentage
|
||||||
|
|
||||||
|
|
||||||
async def update_wallet_balance(
|
async def update_wallet_balance(
|
||||||
wallet: Wallet,
|
wallet: Wallet,
|
||||||
amount: int,
|
amount: int,
|
||||||
@@ -284,10 +425,7 @@ async def update_wallet_balance(
|
|||||||
)
|
)
|
||||||
payment.status = PaymentState.SUCCESS
|
payment.status = PaymentState.SUCCESS
|
||||||
await update_payment(payment, conn=conn)
|
await update_payment(payment, conn=conn)
|
||||||
# notify receiver asynchronously
|
await internal_invoice_queue_put(payment.checking_id)
|
||||||
from lnbits.tasks import internal_invoice_queue
|
|
||||||
|
|
||||||
await internal_invoice_queue.put(payment.checking_id)
|
|
||||||
|
|
||||||
|
|
||||||
async def check_wallet_limits(
|
async def check_wallet_limits(
|
||||||
@@ -586,41 +724,28 @@ async def _pay_external_invoice(
|
|||||||
logger.debug(f"payment timeout, {checking_id} is still pending")
|
logger.debug(f"payment timeout, {checking_id} is still pending")
|
||||||
return payment
|
return payment
|
||||||
|
|
||||||
if payment_response.checking_id and payment_response.checking_id != checking_id:
|
# payment failed
|
||||||
logger.warning(
|
if (
|
||||||
f"backend sent unexpected checking_id (expected: {checking_id} got:"
|
payment_response.checking_id is None
|
||||||
f" {payment_response.checking_id})"
|
or payment_response.ok is False
|
||||||
)
|
or payment_response.checking_id != checking_id
|
||||||
if payment_response.checking_id and payment_response.ok is not False:
|
):
|
||||||
# payment.ok can be True (paid) or None (pending)!
|
|
||||||
logger.debug(f"updating payment {checking_id}")
|
|
||||||
payment.status = (
|
|
||||||
PaymentState.SUCCESS
|
|
||||||
if payment_response.ok is True
|
|
||||||
else PaymentState.PENDING
|
|
||||||
)
|
|
||||||
payment.fee = -(abs(payment_response.fee_msat or 0) + abs(service_fee_msat))
|
|
||||||
payment.preimage = payment_response.preimage
|
|
||||||
await update_payment(payment, payment_response.checking_id, conn=conn)
|
|
||||||
payment.checking_id = payment_response.checking_id
|
|
||||||
if payment.success:
|
|
||||||
await send_payment_notification(wallet, payment)
|
|
||||||
logger.success(f"payment successful {payment_response.checking_id}")
|
|
||||||
elif payment_response.checking_id is None and payment_response.ok is False:
|
|
||||||
# payment failed
|
|
||||||
logger.debug(f"payment failed {checking_id}, {payment_response.error_message}")
|
|
||||||
payment.status = PaymentState.FAILED
|
payment.status = PaymentState.FAILED
|
||||||
await update_payment(payment, conn=conn)
|
await update_payment(payment, conn=conn)
|
||||||
raise PaymentError(
|
message = payment_response.error_message or "without an error message."
|
||||||
f"Payment failed: {payment_response.error_message}"
|
raise PaymentError(f"Payment failed: {message}", status="failed")
|
||||||
or "Payment failed, but backend didn't give us an error message.",
|
|
||||||
status="failed",
|
# payment.ok can be True (paid) or None (pending)!
|
||||||
)
|
payment.status = (
|
||||||
else:
|
PaymentState.SUCCESS if payment_response.ok is True else PaymentState.PENDING
|
||||||
logger.warning(
|
)
|
||||||
"didn't receive checking_id from backend, payment may be stuck in"
|
payment.fee = -(abs(payment_response.fee_msat or 0) + abs(service_fee_msat))
|
||||||
f" database: {checking_id}"
|
payment.preimage = payment_response.preimage
|
||||||
)
|
await update_payment(payment, payment_response.checking_id, conn=conn)
|
||||||
|
payment.checking_id = payment_response.checking_id
|
||||||
|
if payment.success:
|
||||||
|
await send_payment_notification(wallet, payment)
|
||||||
|
logger.success(f"payment successful {payment_response.checking_id}")
|
||||||
|
|
||||||
return payment
|
return payment
|
||||||
|
|
||||||
@@ -728,3 +853,47 @@ async def _credit_service_fee_wallet(
|
|||||||
status=PaymentState.SUCCESS,
|
status=PaymentState.SUCCESS,
|
||||||
conn=conn,
|
conn=conn,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _check_fiat_invoice_limits(
|
||||||
|
amount_sat: int, fiat_provider_name: str, conn: Optional[Connection] = None
|
||||||
|
):
|
||||||
|
limits = settings.get_fiat_provider_limits(fiat_provider_name)
|
||||||
|
if not limits:
|
||||||
|
raise ValueError(
|
||||||
|
f"Fiat provider '{fiat_provider_name}' does not have limits configured.",
|
||||||
|
)
|
||||||
|
|
||||||
|
min_amount_sat = limits.service_min_amount_sats
|
||||||
|
if min_amount_sat and (amount_sat < min_amount_sat):
|
||||||
|
raise ValueError(
|
||||||
|
f"Minimum amount is {min_amount_sat} " f"sats for '{fiat_provider_name}'.",
|
||||||
|
)
|
||||||
|
max_amount_sats = limits.service_max_amount_sats
|
||||||
|
if max_amount_sats and (amount_sat > max_amount_sats):
|
||||||
|
raise ValueError(
|
||||||
|
f"Maximum amount is {max_amount_sats} " f"sats for '{fiat_provider_name}'.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if limits.service_max_fee_sats > 0 or limits.service_fee_percent > 0:
|
||||||
|
if not limits.service_fee_wallet_id:
|
||||||
|
raise ValueError(
|
||||||
|
f"Fiat provider '{fiat_provider_name}' service fee wallet missing.",
|
||||||
|
)
|
||||||
|
fees_wallet = await get_wallet(limits.service_fee_wallet_id, conn=conn)
|
||||||
|
if not fees_wallet:
|
||||||
|
raise ValueError(
|
||||||
|
f"Fiat provider '{fiat_provider_name}' service fee wallet not found.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if limits.service_faucet_wallet_id:
|
||||||
|
faucet_wallet = await get_wallet(limits.service_faucet_wallet_id, conn=conn)
|
||||||
|
if not faucet_wallet:
|
||||||
|
raise ValueError(
|
||||||
|
f"Fiat provider '{fiat_provider_name}' faucet wallet not found.",
|
||||||
|
)
|
||||||
|
if faucet_wallet.balance < amount_sat:
|
||||||
|
raise ValueError(
|
||||||
|
f"The amount exceeds the '{fiat_provider_name}'"
|
||||||
|
"faucet wallet balance.",
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import traceback
|
import traceback
|
||||||
from typing import Callable, Coroutine
|
from collections.abc import Coroutine
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,287 @@
|
|||||||
|
<q-tab-panel name="fiat_providers">
|
||||||
|
<h6 class="q-my-none q-mb-sm">
|
||||||
|
<span v-text="$t('fiat_providers')"></span>
|
||||||
|
<q-btn
|
||||||
|
round
|
||||||
|
flat
|
||||||
|
@click="hideInputsToggle()"
|
||||||
|
:icon="hideInputToggle ? 'visibility_off' : 'visibility'"
|
||||||
|
></q-btn>
|
||||||
|
</h6>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<q-list bordered class="rounded-borders">
|
||||||
|
<q-expansion-item header-class="text-primary text-bold">
|
||||||
|
<template v-slot:header>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-avatar>
|
||||||
|
<img
|
||||||
|
:src="'{{ static_url_for('static', 'images/stripe_logo.ico') }}'"
|
||||||
|
/>
|
||||||
|
</q-avatar>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<q-item-section> Stripe </q-item-section>
|
||||||
|
|
||||||
|
<q-item-section side>
|
||||||
|
<div class="row items-center">
|
||||||
|
<q-toggle
|
||||||
|
size="md"
|
||||||
|
:label="$t('enabled')"
|
||||||
|
v-model="formData.stripe_enabled"
|
||||||
|
color="green"
|
||||||
|
unchecked-icon="clear"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-item-section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<q-card class="q-pb-xl">
|
||||||
|
<q-expansion-item :label="$t('api')" default-opened>
|
||||||
|
<q-card-section class="q-pa-md">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
type="text"
|
||||||
|
v-model="formData.stripe_api_endpoint"
|
||||||
|
:label="$t('endpoint')"
|
||||||
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
class="q-mt-md"
|
||||||
|
:type="hideInputToggle ? 'password' : 'text'"
|
||||||
|
v-model="formData.stripe_api_secret_key"
|
||||||
|
:label="$t('secret_key')"
|
||||||
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
class="q-mt-md"
|
||||||
|
type="text"
|
||||||
|
v-model="formData.stripe_payment_success_url"
|
||||||
|
:label="$t('callback_success_url')"
|
||||||
|
:hint="$t('callback_success_url_hint')"
|
||||||
|
></q-input>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section class="q-pa-md">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<q-btn
|
||||||
|
outline
|
||||||
|
color="grey"
|
||||||
|
class="float-right"
|
||||||
|
:label="$t('check_connection')"
|
||||||
|
@click="checkFiatProvider('stripe')"
|
||||||
|
></q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-expansion-item>
|
||||||
|
|
||||||
|
<q-expansion-item :label="$t('webhook')" default-opened>
|
||||||
|
<q-card-section>
|
||||||
|
<span v-text="$t('webhook_stripe_description')"></span>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
class="q-mt-md"
|
||||||
|
type="text"
|
||||||
|
disable
|
||||||
|
v-model="formData.stripe_payment_webhook_url"
|
||||||
|
:label="$t('webhook_url')"
|
||||||
|
:hint="$t('webhook_url_hint')"
|
||||||
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
class="q-mt-md"
|
||||||
|
:type="hideInputToggle ? 'password' : 'text'"
|
||||||
|
v-model="formData.stripe_webhook_signing_secret"
|
||||||
|
:label="$t('signing_secret')"
|
||||||
|
:hint="$t('signing_secret_hint')"
|
||||||
|
></q-input>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<span v-text="$t('webhook_events_list')"></span>
|
||||||
|
<ul>
|
||||||
|
<li><code>checkout.session.async_payment_failed</code></li>
|
||||||
|
<li><code>checkout.session.async_payment_succeeded</code></li>
|
||||||
|
<li><code>checkout.session.completed</code></li>
|
||||||
|
<li><code>checkout.session.expired</code></li>
|
||||||
|
</ul>
|
||||||
|
</q-card-section>
|
||||||
|
</q-expansion-item>
|
||||||
|
<q-expansion-item :label="$t('service_fee')">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4 col-sm-12">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
class="q-ma-sm"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
v-model="formData.stripe_limits.service_fee_percent"
|
||||||
|
@update:model-value="touchSettings()"
|
||||||
|
:label="$t('service_fee_label')"
|
||||||
|
:hint="$t('service_fee_hint')"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 col-sm-12">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
class="q-ma-sm"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
v-model="formData.stripe_limits.service_max_fee_sats"
|
||||||
|
@update:model-value="touchSettings()"
|
||||||
|
:label="$t('service_fee_max')"
|
||||||
|
:hint="$t('service_fee_max_hint')"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 col-sm-12">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
class="q-ma-sm"
|
||||||
|
type="text"
|
||||||
|
v-model="formData.stripe_limits.service_fee_wallet_id"
|
||||||
|
@update:model-value="touchSettings()"
|
||||||
|
:label="$t('fee_wallet_label')"
|
||||||
|
:hint="$t('fee_wallet_hint')"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-expansion-item>
|
||||||
|
<q-expansion-item :label="$t('amount_limits')">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4 col-sm-12">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
class="q-ma-sm"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
v-model="formData.stripe_limits.service_min_amount_sats"
|
||||||
|
@update:model-value="touchSettings()"
|
||||||
|
:label="$t('min_incoming_payment_amount')"
|
||||||
|
:hint="$t('min_incoming_payment_amount_desc')"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 col-sm-12">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
class="q-ma-sm"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
v-model="formData.stripe_limits.service_max_amount_sats"
|
||||||
|
@update:model-value="touchSettings()"
|
||||||
|
:label="$t('max_incoming_payment_amount')"
|
||||||
|
:hint="$t('max_incoming_payment_amount_desc')"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 col-sm-12">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
class="q-ma-sm"
|
||||||
|
v-model="formData.stripe_limits.service_faucet_wallet_id"
|
||||||
|
@update:model-value="touchSettings()"
|
||||||
|
:label="$t('faucest_wallet_id')"
|
||||||
|
:hint="$t('faucest_wallet_id_hint')"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-item>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label v-text="$t('faucest_wallet')"></q-item-label>
|
||||||
|
<q-item-label caption>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<span
|
||||||
|
v-text="$t('faucest_wallet_desc_1', {provider: 'stripe'})"
|
||||||
|
></span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span
|
||||||
|
v-text="$t('faucest_wallet_desc_2', {provider: 'stripe'})"
|
||||||
|
></span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span v-text="$t('faucest_wallet_desc_3')"></span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span
|
||||||
|
v-text="$t('faucest_wallet_desc_4', {provider: 'stripe'})"
|
||||||
|
></span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span v-text="$t('faucest_wallet_desc_5')"></span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<br />
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-card-section>
|
||||||
|
</q-expansion-item>
|
||||||
|
<q-expansion-item :label="$t('allowed_users')">
|
||||||
|
<q-card-section>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
v-model="formAddStripeUser"
|
||||||
|
@keydown.enter="addAllowedUser"
|
||||||
|
type="text"
|
||||||
|
:label="$t('allowed_users_label')"
|
||||||
|
:hint="$t('allowed_users_hint_feature', {feature: 'Stripe'})"
|
||||||
|
>
|
||||||
|
<q-btn
|
||||||
|
@click="addStripeAllowedUser"
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
icon="add"
|
||||||
|
></q-btn>
|
||||||
|
</q-input>
|
||||||
|
<div>
|
||||||
|
<q-chip
|
||||||
|
v-for="user in formData.stripe_limits.allowed_users"
|
||||||
|
@update:model-value="touchSettings()"
|
||||||
|
:key="user"
|
||||||
|
removable
|
||||||
|
@remove="removeStripeAllowedUser(user)"
|
||||||
|
color="primary"
|
||||||
|
text-color="white"
|
||||||
|
:label="user"
|
||||||
|
class="ellipsis"
|
||||||
|
>
|
||||||
|
</q-chip>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-expansion-item>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
|
||||||
|
<q-separator />
|
||||||
|
|
||||||
|
<q-expansion-item header-class="text-primary text-bold">
|
||||||
|
<template v-slot:header>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-avatar>
|
||||||
|
<img
|
||||||
|
:src="'{{ static_url_for('static', 'images/square_logo.png') }}'"
|
||||||
|
/>
|
||||||
|
</q-avatar>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<q-item-section> Square </q-item-section>
|
||||||
|
|
||||||
|
<q-item-section side>
|
||||||
|
<div class="row items-center">Disabled</div>
|
||||||
|
</q-item-section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<q-card>
|
||||||
|
<q-card-section> Coming Soon </q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
</q-list>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-tab-panel>
|
||||||
@@ -115,6 +115,13 @@ import window_vars with context %} {% block scripts %} {{ window_vars(user) }}
|
|||||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||||
><span v-text="$t('exchanges')"></span></q-tooltip
|
><span v-text="$t('exchanges')"></span></q-tooltip
|
||||||
></q-tab>
|
></q-tab>
|
||||||
|
<q-tab
|
||||||
|
name="fiat_providers"
|
||||||
|
icon="credit_score"
|
||||||
|
:label="$q.screen.gt.sm ? $t('fiat_providers') : null"
|
||||||
|
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||||
|
><span v-text="$t('fiat_providers')"></span></q-tooltip
|
||||||
|
></q-tab>
|
||||||
<q-tab
|
<q-tab
|
||||||
name="users"
|
name="users"
|
||||||
icon="group"
|
icon="group"
|
||||||
@@ -183,7 +190,8 @@ import window_vars with context %} {% block scripts %} {{ window_vars(user) }}
|
|||||||
>
|
>
|
||||||
{% include "admin/_tab_funding.html" %} {% include
|
{% include "admin/_tab_funding.html" %} {% include
|
||||||
"admin/_tab_users.html" %} {% include "admin/_tab_server.html"
|
"admin/_tab_users.html" %} {% include "admin/_tab_server.html"
|
||||||
%} {% include "admin/_tab_exchange_providers.html" %} {% include
|
%} {% include "admin/_tab_exchange_providers.html" %}{% include
|
||||||
|
"admin/_tab_fiat_providers.html" %} {% include
|
||||||
"admin/_tab_extensions.html" %} {% include
|
"admin/_tab_extensions.html" %} {% include
|
||||||
"admin/_tab_notifications.html" %} {% include
|
"admin/_tab_notifications.html" %} {% include
|
||||||
"admin/_tab_security.html" %} {% include "admin/_tab_theme.html"
|
"admin/_tab_security.html" %} {% include "admin/_tab_theme.html"
|
||||||
|
|||||||
@@ -272,10 +272,21 @@
|
|||||||
class="q-mb-md"
|
class="q-mb-md"
|
||||||
>
|
>
|
||||||
</q-input>
|
</q-input>
|
||||||
|
<q-input
|
||||||
|
v-model="user.external_id"
|
||||||
|
:label="$t('external_id')"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
readonly
|
||||||
|
class="q-mb-md"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
v-model="user.extra.picture"
|
v-model="user.extra.picture"
|
||||||
:label="$t('picture')"
|
:label="$t('picture')"
|
||||||
filled
|
filled
|
||||||
|
dense
|
||||||
class="q-mb-md"
|
class="q-mb-md"
|
||||||
>
|
>
|
||||||
</q-input>
|
</q-input>
|
||||||
@@ -357,89 +368,89 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="g.allowedThemes.includes('classic')"
|
v-if="allowedThemes.includes('classic')"
|
||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
@click="themeChoiceFunc('classic')"
|
@click="changeTheme('classic')"
|
||||||
icon="circle"
|
icon="circle"
|
||||||
color="deep-purple"
|
color="deep-purple"
|
||||||
size="md"
|
size="md"
|
||||||
><q-tooltip>classic</q-tooltip>
|
><q-tooltip>classic</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="g.allowedThemes.includes('bitcoin')"
|
v-if="allowedThemes.includes('bitcoin')"
|
||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
@click="themeChoiceFunc('bitcoin')"
|
@click="changeTheme('bitcoin')"
|
||||||
icon="circle"
|
icon="circle"
|
||||||
color="deep-orange"
|
color="deep-orange"
|
||||||
size="md"
|
size="md"
|
||||||
><q-tooltip>bitcoin</q-tooltip>
|
><q-tooltip>bitcoin</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="g.allowedThemes.includes('mint')"
|
v-if="allowedThemes.includes('mint')"
|
||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
@click="themeChoiceFunc('mint')"
|
@click="changeTheme('mint')"
|
||||||
icon="circle"
|
icon="circle"
|
||||||
color="green"
|
color="green"
|
||||||
size="md"
|
size="md"
|
||||||
><q-tooltip>mint</q-tooltip> </q-btn
|
><q-tooltip>mint</q-tooltip> </q-btn
|
||||||
><q-btn
|
><q-btn
|
||||||
v-if="g.allowedThemes.includes('autumn')"
|
v-if="allowedThemes.includes('autumn')"
|
||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
@click="themeChoiceFunc('autumn')"
|
@click="changeTheme('autumn')"
|
||||||
icon="circle"
|
icon="circle"
|
||||||
color="brown"
|
color="brown"
|
||||||
size="md"
|
size="md"
|
||||||
><q-tooltip>autumn</q-tooltip>
|
><q-tooltip>autumn</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="g.allowedThemes.includes('monochrome')"
|
v-if="allowedThemes.includes('monochrome')"
|
||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
@click="themeChoiceFunc('monochrome')"
|
@click="changeTheme('monochrome')"
|
||||||
icon="circle"
|
icon="circle"
|
||||||
color="grey"
|
color="grey"
|
||||||
size="md"
|
size="md"
|
||||||
><q-tooltip>monochrome</q-tooltip>
|
><q-tooltip>monochrome</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="g.allowedThemes.includes('salvador')"
|
v-if="allowedThemes.includes('salvador')"
|
||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
@click="themeChoiceFunc('salvador')"
|
@click="changeTheme('salvador')"
|
||||||
icon="circle"
|
icon="circle"
|
||||||
color="blue-10"
|
color="blue-10"
|
||||||
size="md"
|
size="md"
|
||||||
><q-tooltip>elSalvador</q-tooltip>
|
><q-tooltip>elSalvador</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="g.allowedThemes.includes('freedom')"
|
v-if="allowedThemes.includes('freedom')"
|
||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
@click="themeChoiceFunc('freedom')"
|
@click="changeTheme('freedom')"
|
||||||
icon="circle"
|
icon="circle"
|
||||||
color="pink-13"
|
color="pink-13"
|
||||||
size="md"
|
size="md"
|
||||||
><q-tooltip>freedom</q-tooltip>
|
><q-tooltip>freedom</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="g.allowedThemes.includes('cyber')"
|
v-if="allowedThemes.includes('cyber')"
|
||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
@click="themeChoiceFunc('cyber')"
|
@click="changeTheme('cyber')"
|
||||||
icon="circle"
|
icon="circle"
|
||||||
color="light-green-9"
|
color="light-green-9"
|
||||||
size="md"
|
size="md"
|
||||||
><q-tooltip>cyber</q-tooltip>
|
><q-tooltip>cyber</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="g.allowedThemes.includes('flamingo')"
|
v-if="allowedThemes.includes('flamingo')"
|
||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
@click="themeChoiceFunc('flamingo')"
|
@click="changeTheme('flamingo')"
|
||||||
icon="circle"
|
icon="circle"
|
||||||
color="pink-3"
|
color="pink-3"
|
||||||
size="md"
|
size="md"
|
||||||
@@ -453,9 +464,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
<q-input
|
<q-input
|
||||||
v-model="bgimageSelection"
|
v-model="bgimageChoice"
|
||||||
:label="$t('background_image')"
|
:label="$t('background_image')"
|
||||||
@update:model-value="bgimageChoiceFunc"
|
@update:model-value="applyBackgroundImage"
|
||||||
>
|
>
|
||||||
<q-tooltip
|
<q-tooltip
|
||||||
><span v-text="$t('background_image')"></span
|
><span v-text="$t('background_image')"></span
|
||||||
@@ -468,19 +479,18 @@
|
|||||||
<span v-text="$t('gradient_background')"></span>
|
<span v-text="$t('gradient_background')"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
<q-btn
|
<q-toggle
|
||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
round
|
round
|
||||||
@click="toggleGradient"
|
|
||||||
icon="gradient"
|
icon="gradient"
|
||||||
size="sm"
|
|
||||||
v-model="gradientChoice"
|
v-model="gradientChoice"
|
||||||
|
@update:model-value="applyGradient"
|
||||||
>
|
>
|
||||||
<q-tooltip
|
<q-tooltip
|
||||||
><span v-text="$t('toggle_gradient')"></span
|
><span v-text="$t('toggle_gradient')"></span
|
||||||
></q-tooltip>
|
></q-tooltip>
|
||||||
</q-btn>
|
</q-toggle>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -489,10 +499,11 @@
|
|||||||
<span v-text="$t('toggle_darkmode')"></span>
|
<span v-text="$t('toggle_darkmode')"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
<q-btn
|
<q-toggle
|
||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
round
|
round
|
||||||
|
v-model="darkChoice"
|
||||||
@click="toggleDarkMode"
|
@click="toggleDarkMode"
|
||||||
:icon="($q.dark.isActive) ? 'brightness_3' : 'wb_sunny'"
|
:icon="($q.dark.isActive) ? 'brightness_3' : 'wb_sunny'"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -500,7 +511,7 @@
|
|||||||
<q-tooltip
|
<q-tooltip
|
||||||
><span v-text="$t('toggle_darkmode')"></span
|
><span v-text="$t('toggle_darkmode')"></span
|
||||||
></q-tooltip>
|
></q-tooltip>
|
||||||
</q-btn>
|
</q-toggle>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-mb-md">
|
<div class="row q-mb-md">
|
||||||
@@ -509,7 +520,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
<q-select
|
<q-select
|
||||||
v-model="borderSelection"
|
v-model="borderChoice"
|
||||||
:options="borderOptions"
|
:options="borderOptions"
|
||||||
label="Borders"
|
label="Borders"
|
||||||
@update:model-value="applyBorder"
|
@update:model-value="applyBorder"
|
||||||
@@ -537,7 +548,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
<q-select
|
<q-select
|
||||||
v-model="reactionSelection"
|
v-model="reactionChoice"
|
||||||
:options="reactionOptions"
|
:options="reactionOptions"
|
||||||
label="Reactions"
|
label="Reactions"
|
||||||
@update:model-value="reactionChoiceFunc"
|
@update:model-value="reactionChoiceFunc"
|
||||||
|
|||||||
@@ -166,11 +166,12 @@
|
|||||||
v-if="authAction === 'login' && allowedRegister"
|
v-if="authAction === 'login' && allowedRegister"
|
||||||
class="q-mb-none"
|
class="q-mb-none"
|
||||||
>
|
>
|
||||||
Not registered? Create an
|
Not registered?
|
||||||
<span
|
<a
|
||||||
|
href="#"
|
||||||
class="text-secondary cursor-pointer"
|
class="text-secondary cursor-pointer"
|
||||||
@click="showRegister('username-password')"
|
@click.prevent="showRegister('username-password')"
|
||||||
>Account</span
|
>Create an Account</a
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
@@ -182,11 +183,12 @@
|
|||||||
<p v-else-if="authAction === 'register'" class="q-mb-none">
|
<p v-else-if="authAction === 'register'" class="q-mb-none">
|
||||||
<span v-text="$t('existing_account_question')"></span>
|
<span v-text="$t('existing_account_question')"></span>
|
||||||
|
|
||||||
<span
|
<a
|
||||||
|
href="#"
|
||||||
class="text-secondary cursor-pointer q-ml-sm"
|
class="text-secondary cursor-pointer q-ml-sm"
|
||||||
@click="showLogin('username-password')"
|
@click.prevent="showLogin('username-password')"
|
||||||
v-text="$t('login')"
|
v-text="$t('login')"
|
||||||
></span>
|
></a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</username-password>
|
</username-password>
|
||||||
|
|||||||
@@ -211,7 +211,12 @@
|
|||||||
</q-card>
|
</q-card>
|
||||||
<div id="hiddenQrCodeContainer" style="display: none">
|
<div id="hiddenQrCodeContainer" style="display: none">
|
||||||
<lnbits-qrcode
|
<lnbits-qrcode
|
||||||
:value="'lightning:' + this.receive.paymentReq"
|
v-if="receive.fiatPaymentReq"
|
||||||
|
:value="receive.fiatPaymentReq"
|
||||||
|
></lnbits-qrcode>
|
||||||
|
<lnbits-qrcode
|
||||||
|
v-else
|
||||||
|
:value="'lightning:' + (this.receive.paymentReq || '').toUpperCase()"
|
||||||
></lnbits-qrcode>
|
></lnbits-qrcode>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -619,14 +624,30 @@
|
|||||||
:readonly="receive.lnurl && receive.lnurl.fixed"
|
:readonly="receive.lnurl && receive.lnurl.fixed"
|
||||||
></q-input>
|
></q-input>
|
||||||
{% else %}
|
{% else %}
|
||||||
<q-select
|
<div class="row">
|
||||||
filled
|
<div class="col-10">
|
||||||
dense
|
<q-select
|
||||||
v-model="receive.unit"
|
filled
|
||||||
type="text"
|
dense
|
||||||
:label="$t('unit')"
|
v-model="receive.unit"
|
||||||
:options="receive.units"
|
type="text"
|
||||||
></q-select>
|
:label="$t('unit')"
|
||||||
|
:options="receive.units"
|
||||||
|
></q-select>
|
||||||
|
</div>
|
||||||
|
<div class="col-2">
|
||||||
|
<q-btn
|
||||||
|
v-if="g.fiatTracking"
|
||||||
|
@click="swapBalancePriority"
|
||||||
|
class="float-right"
|
||||||
|
color="primary"
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
icon="swap_vert"
|
||||||
|
></q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
ref="setAmount"
|
ref="setAmount"
|
||||||
filled
|
filled
|
||||||
@@ -644,9 +665,59 @@
|
|||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
|
type="textarea"
|
||||||
|
rows="2"
|
||||||
v-model.trim="receive.data.memo"
|
v-model.trim="receive.data.memo"
|
||||||
:label="$t('memo')"
|
:label="$t('memo')"
|
||||||
></q-input>
|
></q-input>
|
||||||
|
<div v-if="g.user.fiat_providers?.length" class="q-mt-md">
|
||||||
|
<q-list bordered dense class="rounded-borders">
|
||||||
|
<q-item-label dense header>
|
||||||
|
<span v-text="$t('select_payment_provider')"></span>
|
||||||
|
</q-item-label>
|
||||||
|
<q-separator></q-separator>
|
||||||
|
<q-item
|
||||||
|
:active="!receive.fiatProvider"
|
||||||
|
@click="receive.fiatProvider = ''"
|
||||||
|
active-class="bg-teal-1 text-grey-8 text-weight-bold"
|
||||||
|
clickable
|
||||||
|
v-ripple
|
||||||
|
>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-avatar square>
|
||||||
|
<img
|
||||||
|
:src="'{{ static_url_for('static', 'images/logos/lnbits.png') }}'"
|
||||||
|
/>
|
||||||
|
</q-avatar>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<span
|
||||||
|
v-text="$t('pay_with', {provider: 'Lightning Network'})"
|
||||||
|
></span>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-separator></q-separator>
|
||||||
|
<q-item
|
||||||
|
:active="receive.fiatProvider === 'stripe'"
|
||||||
|
@click="receive.fiatProvider = 'stripe'"
|
||||||
|
active-class="bg-teal-1 text-grey-8 text-weight-bold"
|
||||||
|
clickable
|
||||||
|
v-ripple
|
||||||
|
>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-avatar>
|
||||||
|
<img
|
||||||
|
:src="'{{ static_url_for('static', 'images/stripe_logo.ico') }}'"
|
||||||
|
/>
|
||||||
|
</q-avatar>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<span v-text="$t('pay_with', {provider: 'Stripe'})"></span>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="receive.status == 'pending'" class="row q-mt-lg">
|
<div v-if="receive.status == 'pending'" class="row q-mt-lg">
|
||||||
<q-btn
|
<q-btn
|
||||||
unelevated
|
unelevated
|
||||||
@@ -680,7 +751,14 @@
|
|||||||
class="q-pa-lg q-pt-xl lnbits__dialog-card"
|
class="q-pa-lg q-pt-xl lnbits__dialog-card"
|
||||||
>
|
>
|
||||||
<div class="text-center q-mb-lg">
|
<div class="text-center q-mb-lg">
|
||||||
<a :href="'lightning:' + receive.paymentReq">
|
<a
|
||||||
|
v-if="receive.fiatPaymentReq"
|
||||||
|
:href="receive.fiatPaymentReq"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<div v-html="invoiceQrCode"></div>
|
||||||
|
</a>
|
||||||
|
<a v-else :href="'lightning:' + receive.paymentReq">
|
||||||
<div v-html="invoiceQrCode"></div>
|
<div v-html="invoiceQrCode"></div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -691,25 +769,27 @@
|
|||||||
<h5 v-if="receive.unit != 'sat'" class="q-mt-none q-mb-sm">
|
<h5 v-if="receive.unit != 'sat'" class="q-mt-none q-mb-sm">
|
||||||
<span v-text="formattedSatAmount"></span>
|
<span v-text="formattedSatAmount"></span>
|
||||||
</h5>
|
</h5>
|
||||||
<q-chip v-if="hasNfc" outline square color="positive">
|
<div v-if="!receive.fiatPaymentReq">
|
||||||
<q-avatar
|
<q-chip v-if="hasNfc" outline square color="positive">
|
||||||
icon="nfc"
|
<q-avatar
|
||||||
color="positive"
|
icon="nfc"
|
||||||
text-color="white"
|
color="positive"
|
||||||
></q-avatar>
|
text-color="white"
|
||||||
<span v-text="$t('nfc_supported')"></span>
|
></q-avatar>
|
||||||
</q-chip>
|
<span v-text="$t('nfc_supported')"></span>
|
||||||
<span
|
</q-chip>
|
||||||
v-else
|
<span
|
||||||
class="text-caption text-grey"
|
v-else
|
||||||
v-text="$t('nfc_not_supported')"
|
class="text-caption text-grey"
|
||||||
></span>
|
v-text="$t('nfc_not_supported')"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-mt-lg">
|
<div class="row q-mt-lg">
|
||||||
<q-btn
|
<q-btn
|
||||||
outline
|
outline
|
||||||
color="grey"
|
color="grey"
|
||||||
@click="copyText(receive.paymentReq)"
|
@click="copyText(receive.fiatPaymentReq || receive.paymentReq)"
|
||||||
:label="$t('copy_invoice')"
|
:label="$t('copy_invoice')"
|
||||||
></q-btn>
|
></q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
|
|||||||
@@ -147,6 +147,14 @@
|
|||||||
class="q-mb-md"
|
class="q-mb-md"
|
||||||
>
|
>
|
||||||
</q-input>
|
</q-input>
|
||||||
|
<q-input
|
||||||
|
v-model="activeUser.data.external_id"
|
||||||
|
:label="$t('external_id')"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
class="q-mb-md"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
<q-input
|
<q-input
|
||||||
v-model="activeUser.data.extra.picture"
|
v-model="activeUser.data.extra.picture"
|
||||||
:label="$t('picture')"
|
:label="$t('picture')"
|
||||||
|
|||||||
@@ -411,23 +411,13 @@ async def update(
|
|||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid user ID.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid user ID.")
|
||||||
if data.username and not is_valid_username(data.username):
|
if data.username and not is_valid_username(data.username):
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid username.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid username.")
|
||||||
if data.email != user.email:
|
|
||||||
raise HTTPException(
|
|
||||||
HTTPStatus.BAD_REQUEST,
|
|
||||||
"Email mismatch.",
|
|
||||||
)
|
|
||||||
if (
|
if (
|
||||||
data.username
|
data.username
|
||||||
and user.username != data.username
|
and user.username != data.username
|
||||||
and await get_account_by_username(data.username)
|
and await get_account_by_username(data.username)
|
||||||
):
|
):
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
|
||||||
if (
|
|
||||||
data.email
|
|
||||||
and data.email != user.email
|
|
||||||
and await get_account_by_email(data.email)
|
|
||||||
):
|
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Email already exists.")
|
|
||||||
|
|
||||||
account = await get_account(user.id)
|
account = await get_account(user.id)
|
||||||
if not account:
|
if not account:
|
||||||
@@ -435,8 +425,6 @@ async def update(
|
|||||||
|
|
||||||
if data.username:
|
if data.username:
|
||||||
account.username = data.username
|
account.username = data.username
|
||||||
if data.email:
|
|
||||||
account.email = data.email
|
|
||||||
if data.extra:
|
if data.extra:
|
||||||
account.extra = data.extra
|
account.extra = data.extra
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
from fastapi import APIRouter, Request
|
||||||
|
|
||||||
|
from lnbits.core.models.misc import SimpleStatus
|
||||||
|
from lnbits.core.services.fiat_providers import (
|
||||||
|
check_stripe_signature,
|
||||||
|
handle_stripe_event,
|
||||||
|
)
|
||||||
|
from lnbits.settings import settings
|
||||||
|
|
||||||
|
callback_router = APIRouter(prefix="/api/v1/callback", tags=["callback"])
|
||||||
|
|
||||||
|
|
||||||
|
@callback_router.post("/{provider_name}")
|
||||||
|
async def api_generic_webhook_handler(
|
||||||
|
provider_name: str, request: Request
|
||||||
|
) -> SimpleStatus:
|
||||||
|
|
||||||
|
if provider_name.lower() == "stripe":
|
||||||
|
payload = await request.body()
|
||||||
|
sig_header = request.headers.get("Stripe-Signature")
|
||||||
|
check_stripe_signature(
|
||||||
|
payload, sig_header, settings.stripe_webhook_signing_secret
|
||||||
|
)
|
||||||
|
event = await request.json()
|
||||||
|
await handle_stripe_event(event)
|
||||||
|
|
||||||
|
return SimpleStatus(
|
||||||
|
success=True,
|
||||||
|
message=f"Callback received successfully from '{provider_name}'.",
|
||||||
|
)
|
||||||
|
|
||||||
|
return SimpleStatus(
|
||||||
|
success=False,
|
||||||
|
message=f"Unknown fiat provider '{provider_name}'.",
|
||||||
|
)
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
from http import HTTPStatus
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
|
from lnbits.core.models.misc import SimpleStatus
|
||||||
|
from lnbits.core.services.fiat_providers import test_connection
|
||||||
|
from lnbits.decorators import check_admin
|
||||||
|
|
||||||
|
fiat_router = APIRouter(tags=["Fiat API"], prefix="/api/v1/fiat")
|
||||||
|
|
||||||
|
|
||||||
|
@fiat_router.put(
|
||||||
|
"/check/{provider}",
|
||||||
|
status_code=HTTPStatus.OK,
|
||||||
|
dependencies=[Depends(check_admin)],
|
||||||
|
)
|
||||||
|
async def api_test_fiat_provider(provider: str) -> SimpleStatus:
|
||||||
|
return await test_connection(provider)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import Annotated, List, Optional, Union
|
from typing import Annotated, Optional, Union
|
||||||
from urllib.parse import urlencode, urlparse
|
from urllib.parse import urlencode, urlparse
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
@@ -70,7 +70,7 @@ async def robots():
|
|||||||
|
|
||||||
@generic_router.get("/extensions", name="extensions", response_class=HTMLResponse)
|
@generic_router.get("/extensions", name="extensions", response_class=HTMLResponse)
|
||||||
async def extensions(request: Request, user: User = Depends(check_user_exists)):
|
async def extensions(request: Request, user: User = Depends(check_user_exists)):
|
||||||
installed_exts: List[InstallableExtension] = await get_installed_extensions()
|
installed_exts: list[InstallableExtension] = await get_installed_extensions()
|
||||||
installed_exts_ids = [e.id for e in installed_exts]
|
installed_exts_ids = [e.id for e in installed_exts]
|
||||||
|
|
||||||
installable_exts = await InstallableExtension.get_installable_extensions()
|
installable_exts = await InstallableExtension.get_installable_extensions()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import List, Optional
|
from typing import Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from fastapi import APIRouter, Body, Depends, HTTPException
|
from fastapi import APIRouter, Body, Depends, HTTPException
|
||||||
@@ -91,7 +91,7 @@ async def api_get_info(
|
|||||||
@node_router.get("/channels")
|
@node_router.get("/channels")
|
||||||
async def api_get_channels(
|
async def api_get_channels(
|
||||||
node: Node = Depends(require_node),
|
node: Node = Depends(require_node),
|
||||||
) -> Optional[List[NodeChannel]]:
|
) -> Optional[list[NodeChannel]]:
|
||||||
return await node.get_channels()
|
return await node.get_channels()
|
||||||
|
|
||||||
|
|
||||||
@@ -121,7 +121,7 @@ async def api_delete_channel(
|
|||||||
output_index: Optional[int],
|
output_index: Optional[int],
|
||||||
force: bool = False,
|
force: bool = False,
|
||||||
node: Node = Depends(require_node),
|
node: Node = Depends(require_node),
|
||||||
) -> Optional[List[NodeChannel]]:
|
) -> Optional[list[NodeChannel]]:
|
||||||
return await node.close_channel(
|
return await node.close_channel(
|
||||||
short_id,
|
short_id,
|
||||||
(
|
(
|
||||||
@@ -170,7 +170,7 @@ async def api_get_invoices(
|
|||||||
|
|
||||||
|
|
||||||
@node_router.get("/peers")
|
@node_router.get("/peers")
|
||||||
async def api_get_peers(node: Node = Depends(require_node)) -> List[NodePeerInfo]:
|
async def api_get_peers(node: Node = Depends(require_node)) -> list[NodePeerInfo]:
|
||||||
return await node.get_peers()
|
return await node.get_peers()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import json
|
|||||||
import ssl
|
import ssl
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from math import ceil
|
from math import ceil
|
||||||
from typing import List, Optional
|
from typing import Optional
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
@@ -34,13 +34,8 @@ from lnbits.core.models import (
|
|||||||
PaymentFilters,
|
PaymentFilters,
|
||||||
PaymentHistoryPoint,
|
PaymentHistoryPoint,
|
||||||
PaymentWalletStats,
|
PaymentWalletStats,
|
||||||
Wallet,
|
|
||||||
)
|
)
|
||||||
from lnbits.core.models.users import User
|
from lnbits.core.models.users import User
|
||||||
from lnbits.core.services.payments import (
|
|
||||||
get_payments_daily_stats,
|
|
||||||
update_pending_payment,
|
|
||||||
)
|
|
||||||
from lnbits.db import Filters, Page
|
from lnbits.db import Filters, Page
|
||||||
from lnbits.decorators import (
|
from lnbits.decorators import (
|
||||||
WalletTypeInfo,
|
WalletTypeInfo,
|
||||||
@@ -67,9 +62,12 @@ from ..crud import (
|
|||||||
get_wallet_for_key,
|
get_wallet_for_key,
|
||||||
)
|
)
|
||||||
from ..services import (
|
from ..services import (
|
||||||
create_invoice,
|
create_fiat_invoice,
|
||||||
|
create_wallet_invoice,
|
||||||
fee_reserve_total,
|
fee_reserve_total,
|
||||||
|
get_payments_daily_stats,
|
||||||
pay_invoice,
|
pay_invoice,
|
||||||
|
update_pending_payment,
|
||||||
update_pending_payments,
|
update_pending_payments,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -81,7 +79,7 @@ payment_router = APIRouter(prefix="/api/v1/payments", tags=["Payments"])
|
|||||||
name="Payment List",
|
name="Payment List",
|
||||||
summary="get list of payments",
|
summary="get list of payments",
|
||||||
response_description="list of payments",
|
response_description="list of payments",
|
||||||
response_model=List[Payment],
|
response_model=list[Payment],
|
||||||
openapi_extra=generate_filter_params_openapi(PaymentFilters),
|
openapi_extra=generate_filter_params_openapi(PaymentFilters),
|
||||||
)
|
)
|
||||||
async def api_payments(
|
async def api_payments(
|
||||||
@@ -100,7 +98,7 @@ async def api_payments(
|
|||||||
@payment_router.get(
|
@payment_router.get(
|
||||||
"/history",
|
"/history",
|
||||||
name="Get payments history",
|
name="Get payments history",
|
||||||
response_model=List[PaymentHistoryPoint],
|
response_model=list[PaymentHistoryPoint],
|
||||||
openapi_extra=generate_filter_params_openapi(PaymentFilters),
|
openapi_extra=generate_filter_params_openapi(PaymentFilters),
|
||||||
)
|
)
|
||||||
async def api_payments_history(
|
async def api_payments_history(
|
||||||
@@ -115,7 +113,7 @@ async def api_payments_history(
|
|||||||
@payment_router.get(
|
@payment_router.get(
|
||||||
"/stats/count",
|
"/stats/count",
|
||||||
name="Get payments history for all users",
|
name="Get payments history for all users",
|
||||||
response_model=List[PaymentCountStat],
|
response_model=list[PaymentCountStat],
|
||||||
openapi_extra=generate_filter_params_openapi(PaymentFilters),
|
openapi_extra=generate_filter_params_openapi(PaymentFilters),
|
||||||
)
|
)
|
||||||
async def api_payments_counting_stats(
|
async def api_payments_counting_stats(
|
||||||
@@ -137,7 +135,7 @@ async def api_payments_counting_stats(
|
|||||||
@payment_router.get(
|
@payment_router.get(
|
||||||
"/stats/wallets",
|
"/stats/wallets",
|
||||||
name="Get payments history for all users",
|
name="Get payments history for all users",
|
||||||
response_model=List[PaymentWalletStats],
|
response_model=list[PaymentWalletStats],
|
||||||
openapi_extra=generate_filter_params_openapi(PaymentFilters),
|
openapi_extra=generate_filter_params_openapi(PaymentFilters),
|
||||||
)
|
)
|
||||||
async def api_payments_wallets_stats(
|
async def api_payments_wallets_stats(
|
||||||
@@ -158,7 +156,7 @@ async def api_payments_wallets_stats(
|
|||||||
@payment_router.get(
|
@payment_router.get(
|
||||||
"/stats/daily",
|
"/stats/daily",
|
||||||
name="Get payments history per day",
|
name="Get payments history per day",
|
||||||
response_model=List[PaymentDailyStats],
|
response_model=list[PaymentDailyStats],
|
||||||
openapi_extra=generate_filter_params_openapi(PaymentFilters),
|
openapi_extra=generate_filter_params_openapi(PaymentFilters),
|
||||||
)
|
)
|
||||||
async def api_payments_daily_stats(
|
async def api_payments_daily_stats(
|
||||||
@@ -198,69 +196,6 @@ async def api_payments_paginated(
|
|||||||
return page
|
return page
|
||||||
|
|
||||||
|
|
||||||
async def _api_payments_create_invoice(data: CreateInvoice, wallet: Wallet):
|
|
||||||
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:
|
|
||||||
try:
|
|
||||||
description_hash = bytes.fromhex(data.description_hash)
|
|
||||||
except ValueError as exc:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
|
||||||
detail="'description_hash' must be a valid hex string",
|
|
||||||
) from exc
|
|
||||||
if data.unhashed_description:
|
|
||||||
try:
|
|
||||||
unhashed_description = bytes.fromhex(data.unhashed_description)
|
|
||||||
except ValueError as exc:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
|
||||||
detail="'unhashed_description' must be a valid hex string",
|
|
||||||
) from exc
|
|
||||||
# do not save memo if description_hash or unhashed_description is set
|
|
||||||
memo = ""
|
|
||||||
|
|
||||||
payment = await create_invoice(
|
|
||||||
wallet_id=wallet.id,
|
|
||||||
amount=data.amount,
|
|
||||||
memo=memo,
|
|
||||||
currency=data.unit,
|
|
||||||
description_hash=description_hash,
|
|
||||||
unhashed_description=unhashed_description,
|
|
||||||
expiry=data.expiry,
|
|
||||||
extra=data.extra,
|
|
||||||
webhook=data.webhook,
|
|
||||||
internal=data.internal,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
|
|
||||||
@payment_router.get(
|
@payment_router.get(
|
||||||
"/all/paginated",
|
"/all/paginated",
|
||||||
name="Payment List",
|
name="Payment List",
|
||||||
@@ -308,6 +243,7 @@ async def api_payments_create(
|
|||||||
invoice_data: CreateInvoice,
|
invoice_data: CreateInvoice,
|
||||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||||
) -> Payment:
|
) -> Payment:
|
||||||
|
wallet_id = wallet.wallet.id
|
||||||
if invoice_data.out is True and wallet.key_type == KeyType.admin:
|
if invoice_data.out is True and wallet.key_type == KeyType.admin:
|
||||||
if not invoice_data.bolt11:
|
if not invoice_data.bolt11:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -315,21 +251,24 @@ async def api_payments_create(
|
|||||||
detail="Missing BOLT11 invoice",
|
detail="Missing BOLT11 invoice",
|
||||||
)
|
)
|
||||||
payment = await pay_invoice(
|
payment = await pay_invoice(
|
||||||
wallet_id=wallet.wallet.id,
|
wallet_id=wallet_id,
|
||||||
payment_request=invoice_data.bolt11,
|
payment_request=invoice_data.bolt11,
|
||||||
extra=invoice_data.extra,
|
extra=invoice_data.extra,
|
||||||
)
|
)
|
||||||
return payment
|
return payment
|
||||||
|
|
||||||
elif not invoice_data.out:
|
if invoice_data.out:
|
||||||
# invoice key
|
|
||||||
return await _api_payments_create_invoice(invoice_data, wallet.wallet)
|
|
||||||
else:
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
status_code=HTTPStatus.FORBIDDEN,
|
||||||
detail="Invoice (or Admin) key required.",
|
detail="Invoice (or Admin) key required.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# If the payment is not outgoing, we can create a new invoice.
|
||||||
|
if invoice_data.fiat_provider:
|
||||||
|
return await create_fiat_invoice(wallet_id, invoice_data)
|
||||||
|
|
||||||
|
return await create_wallet_invoice(wallet_id, invoice_data)
|
||||||
|
|
||||||
|
|
||||||
@payment_router.get("/fee-reserve")
|
@payment_router.get("/fee-reserve")
|
||||||
async def api_payments_fee_reserve(invoice: str = Query("invoice")) -> JSONResponse:
|
async def api_payments_fee_reserve(invoice: str = Query("invoice")) -> JSONResponse:
|
||||||
@@ -439,6 +378,9 @@ async def api_payment(payment_hash, x_api_key: Optional[str] = Header(None)):
|
|||||||
return {"paid": True, "preimage": payment.preimage, "details": payment}
|
return {"paid": True, "preimage": payment.preimage, "details": payment}
|
||||||
return {"paid": True, "preimage": payment.preimage}
|
return {"paid": True, "preimage": payment.preimage}
|
||||||
|
|
||||||
|
if payment.failed:
|
||||||
|
return {"paid": False, "status": "failed", "details": payment}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
status = await payment.check_status()
|
status = await payment.check_status()
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import base64
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import List, Optional
|
from typing import Optional
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import shortuuid
|
import shortuuid
|
||||||
@@ -100,6 +100,7 @@ async def api_create_user(data: CreateUser) -> CreateUser:
|
|||||||
username=data.username,
|
username=data.username,
|
||||||
email=data.email,
|
email=data.email,
|
||||||
pubkey=data.pubkey,
|
pubkey=data.pubkey,
|
||||||
|
external_id=data.external_id,
|
||||||
extra=data.extra,
|
extra=data.extra,
|
||||||
)
|
)
|
||||||
account.validate_fields()
|
account.validate_fields()
|
||||||
@@ -132,6 +133,7 @@ async def api_update_user(
|
|||||||
username=data.username,
|
username=data.username,
|
||||||
email=data.email,
|
email=data.email,
|
||||||
pubkey=data.pubkey,
|
pubkey=data.pubkey,
|
||||||
|
external_id=data.external_id,
|
||||||
extra=data.extra or UserExtra(),
|
extra=data.extra or UserExtra(),
|
||||||
)
|
)
|
||||||
await update_user_account(account)
|
await update_user_account(account)
|
||||||
@@ -214,7 +216,7 @@ async def api_users_toggle_admin(user_id: str) -> SimpleStatus:
|
|||||||
|
|
||||||
|
|
||||||
@users_router.get("/user/{user_id}/wallet", name="Get wallets for user")
|
@users_router.get("/user/{user_id}/wallet", name="Get wallets for user")
|
||||||
async def api_users_get_user_wallet(user_id: str) -> List[Wallet]:
|
async def api_users_get_user_wallet(user_id: str) -> list[Wallet]:
|
||||||
return await get_wallets(user_id)
|
return await get_wallets(user_id)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+16
-3
@@ -1,5 +1,5 @@
|
|||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import Annotated, Literal, Optional, Type, Union
|
from typing import Annotated, Literal, Optional, Union
|
||||||
|
|
||||||
import jwt
|
import jwt
|
||||||
from fastapi import Cookie, Depends, Query, Request, Security
|
from fastapi import Cookie, Depends, Query, Request, Security
|
||||||
@@ -28,7 +28,7 @@ from lnbits.core.models import (
|
|||||||
WalletTypeInfo,
|
WalletTypeInfo,
|
||||||
)
|
)
|
||||||
from lnbits.db import Connection, Filter, Filters, TFilterModel
|
from lnbits.db import Connection, Filter, Filters, TFilterModel
|
||||||
from lnbits.helpers import path_segments
|
from lnbits.helpers import normalize_path, path_segments
|
||||||
from lnbits.settings import AuthMethods, settings
|
from lnbits.settings import AuthMethods, settings
|
||||||
|
|
||||||
oauth2_scheme = OAuth2PasswordBearer(
|
oauth2_scheme = OAuth2PasswordBearer(
|
||||||
@@ -223,7 +223,7 @@ async def check_super_user(user: Annotated[User, Depends(check_user_exists)]) ->
|
|||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
def parse_filters(model: Type[TFilterModel]):
|
def parse_filters(model: type[TFilterModel]):
|
||||||
"""
|
"""
|
||||||
Parses the query params as filters.
|
Parses the query params as filters.
|
||||||
:param model: model used for validation of filter values
|
:param model: model used for validation of filter values
|
||||||
@@ -346,3 +346,16 @@ async def _check_account_api_access(
|
|||||||
raise HTTPException(HTTPStatus.FORBIDDEN, "Path not allowed.")
|
raise HTTPException(HTTPStatus.FORBIDDEN, "Path not allowed.")
|
||||||
if not endpoint.supports_method(method):
|
if not endpoint.supports_method(method):
|
||||||
raise HTTPException(HTTPStatus.FORBIDDEN, "Method not allowed.")
|
raise HTTPException(HTTPStatus.FORBIDDEN, "Method not allowed.")
|
||||||
|
|
||||||
|
|
||||||
|
def url_for_interceptor(original_method):
|
||||||
|
def normalize_url(self, *args, **kwargs):
|
||||||
|
url = original_method(self, *args, **kwargs)
|
||||||
|
return url.replace(path=normalize_path(url.path))
|
||||||
|
|
||||||
|
return normalize_url
|
||||||
|
|
||||||
|
|
||||||
|
# Upgraded extensions modify the path.
|
||||||
|
# This interceptor ensures that the path is normalized.
|
||||||
|
Request.url_for = url_for_interceptor(Request.url_for) # type: ignore[method-assign]
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ class InvoiceError(Exception):
|
|||||||
self.status = status
|
self.status = status
|
||||||
|
|
||||||
|
|
||||||
|
class UnsupportedError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def render_html_error(request: Request, exc: Exception) -> Optional[Response]:
|
def render_html_error(request: Request, exc: Exception) -> Optional[Response]:
|
||||||
# Only the browser sends "text/html" request
|
# Only the browser sends "text/html" request
|
||||||
# not fail proof, but everything else get's a JSON response
|
# not fail proof, but everything else get's a JSON response
|
||||||
@@ -147,7 +151,7 @@ def register_exception_handlers(app: FastAPI):
|
|||||||
status_code = HTTPStatus.NOT_FOUND
|
status_code = HTTPStatus.NOT_FOUND
|
||||||
message: str = "Page not found."
|
message: str = "Page not found."
|
||||||
|
|
||||||
if path in settings.lnbits_all_extensions_ids:
|
if settings.is_ready_to_install_extension_id(path):
|
||||||
status_code = HTTPStatus.FORBIDDEN
|
status_code = HTTPStatus.FORBIDDEN
|
||||||
message = f"Extension '{path}' not installed. Ask the admin to install it."
|
message = f"Extension '{path}' not installed. Ask the admin to install it."
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from lnbits.fiat.base import FiatProvider
|
||||||
|
from lnbits.settings import settings
|
||||||
|
|
||||||
|
from .stripe import StripeWallet
|
||||||
|
|
||||||
|
fiat_module = importlib.import_module("lnbits.fiat")
|
||||||
|
|
||||||
|
|
||||||
|
class FiatProviderType(Enum):
|
||||||
|
stripe = "StripeWallet"
|
||||||
|
|
||||||
|
|
||||||
|
async def get_fiat_provider(name: str) -> FiatProvider:
|
||||||
|
if name not in FiatProviderType.__members__:
|
||||||
|
raise ValueError(f"Fiat provider '{name}' is not supported.")
|
||||||
|
|
||||||
|
fiat_provider = fiat_providers.get(name)
|
||||||
|
if fiat_provider:
|
||||||
|
status = await fiat_provider.status(only_check_settings=True)
|
||||||
|
if status.error_message:
|
||||||
|
await fiat_provider.cleanup()
|
||||||
|
del fiat_providers[name]
|
||||||
|
else:
|
||||||
|
return fiat_provider
|
||||||
|
fiat_providers[name] = _init_fiat_provider(FiatProviderType[name])
|
||||||
|
return fiat_providers[name]
|
||||||
|
|
||||||
|
|
||||||
|
def _init_fiat_provider(fiat_provider: FiatProviderType) -> FiatProvider:
|
||||||
|
if not settings.is_fiat_provider_enabled(fiat_provider.name):
|
||||||
|
raise ValueError(f"Fiat provider '{fiat_provider.name}' not enabled.")
|
||||||
|
provider_constructor = getattr(fiat_module, fiat_provider.value)
|
||||||
|
return provider_constructor()
|
||||||
|
|
||||||
|
|
||||||
|
fiat_providers: dict[str, FiatProvider] = {}
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"StripeWallet",
|
||||||
|
]
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from collections.abc import AsyncGenerator, Coroutine
|
||||||
|
from typing import TYPE_CHECKING, NamedTuple
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class FiatStatusResponse(NamedTuple):
|
||||||
|
error_message: str | None = None
|
||||||
|
balance: float = 0
|
||||||
|
|
||||||
|
|
||||||
|
class FiatInvoiceResponse(NamedTuple):
|
||||||
|
ok: bool
|
||||||
|
checking_id: str | None = None # payment_hash, rpc_id
|
||||||
|
payment_request: str | None = None
|
||||||
|
error_message: str | None = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def success(self) -> bool:
|
||||||
|
return self.ok is True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pending(self) -> bool:
|
||||||
|
return self.ok is None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def failed(self) -> bool:
|
||||||
|
return self.ok is False
|
||||||
|
|
||||||
|
|
||||||
|
class FiatPaymentResponse(NamedTuple):
|
||||||
|
# when ok is None it means we don't know if this succeeded
|
||||||
|
ok: bool | None = None
|
||||||
|
checking_id: str | None = None # payment_hash, rcp_id
|
||||||
|
fee: float | None = None
|
||||||
|
error_message: str | None = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def success(self) -> bool:
|
||||||
|
return self.ok is True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pending(self) -> bool:
|
||||||
|
return self.ok is None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def failed(self) -> bool:
|
||||||
|
return self.ok is False
|
||||||
|
|
||||||
|
|
||||||
|
class FiatPaymentStatus(NamedTuple):
|
||||||
|
paid: bool | None = None
|
||||||
|
fee: float | None = None # todo: what fee is this?
|
||||||
|
|
||||||
|
@property
|
||||||
|
def success(self) -> bool:
|
||||||
|
return self.paid is True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pending(self) -> bool:
|
||||||
|
return self.paid is not True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def failed(self) -> bool:
|
||||||
|
return self.paid is False
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
if self.success:
|
||||||
|
return "success"
|
||||||
|
if self.failed:
|
||||||
|
return "failed"
|
||||||
|
return "pending"
|
||||||
|
|
||||||
|
|
||||||
|
class FiatPaymentSuccessStatus(FiatPaymentStatus):
|
||||||
|
paid = True
|
||||||
|
|
||||||
|
|
||||||
|
class FiatPaymentFailedStatus(FiatPaymentStatus):
|
||||||
|
paid = False
|
||||||
|
|
||||||
|
|
||||||
|
class FiatPaymentPendingStatus(FiatPaymentStatus):
|
||||||
|
paid = None
|
||||||
|
|
||||||
|
|
||||||
|
class FiatProvider(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
async def cleanup(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def status(
|
||||||
|
self, only_check_settings: bool | None = False
|
||||||
|
) -> Coroutine[None, None, FiatStatusResponse]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def create_invoice(
|
||||||
|
self,
|
||||||
|
amount: float,
|
||||||
|
payment_hash: str,
|
||||||
|
currency: str,
|
||||||
|
memo: str | None = None,
|
||||||
|
**kwargs,
|
||||||
|
) -> Coroutine[None, None, FiatInvoiceResponse]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def pay_invoice(
|
||||||
|
self,
|
||||||
|
payment_request: str,
|
||||||
|
) -> Coroutine[None, None, FiatPaymentResponse]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_invoice_status(
|
||||||
|
self, checking_id: str
|
||||||
|
) -> Coroutine[None, None, FiatPaymentStatus]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_payment_status(
|
||||||
|
self, checking_id: str
|
||||||
|
) -> Coroutine[None, None, FiatPaymentStatus]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def paid_invoices_stream(
|
||||||
|
self,
|
||||||
|
) -> AsyncGenerator[str, None]:
|
||||||
|
yield ""
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
from collections.abc import AsyncGenerator
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from typing import Optional
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from lnbits.helpers import normalize_endpoint
|
||||||
|
from lnbits.settings import settings
|
||||||
|
|
||||||
|
from .base import (
|
||||||
|
FiatInvoiceResponse,
|
||||||
|
FiatPaymentFailedStatus,
|
||||||
|
FiatPaymentPendingStatus,
|
||||||
|
FiatPaymentResponse,
|
||||||
|
FiatPaymentStatus,
|
||||||
|
FiatPaymentSuccessStatus,
|
||||||
|
FiatProvider,
|
||||||
|
FiatStatusResponse,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class StripeWallet(FiatProvider):
|
||||||
|
"""https://docs.stripe.com/api"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
logger.debug("Initializing StripeWallet")
|
||||||
|
self._settings_fields = self._settings_connection_fields()
|
||||||
|
if not settings.stripe_api_endpoint:
|
||||||
|
raise ValueError("Cannot initialize StripeWallet: missing endpoint.")
|
||||||
|
|
||||||
|
if not settings.stripe_api_secret_key:
|
||||||
|
raise ValueError("Cannot initialize StripeWallet: missing API secret key.")
|
||||||
|
self.endpoint = normalize_endpoint(settings.stripe_api_endpoint)
|
||||||
|
self.headers = {
|
||||||
|
"Authorization": f"Bearer {settings.stripe_api_secret_key}",
|
||||||
|
"User-Agent": settings.user_agent,
|
||||||
|
}
|
||||||
|
self.client = httpx.AsyncClient(base_url=self.endpoint, headers=self.headers)
|
||||||
|
logger.info("StripeWallet initialized.")
|
||||||
|
|
||||||
|
async def cleanup(self):
|
||||||
|
try:
|
||||||
|
await self.client.aclose()
|
||||||
|
except RuntimeError as e:
|
||||||
|
logger.warning(f"Error closing stripe wallet connection: {e}")
|
||||||
|
|
||||||
|
async def status(
|
||||||
|
self, only_check_settings: Optional[bool] = False
|
||||||
|
) -> FiatStatusResponse:
|
||||||
|
if only_check_settings:
|
||||||
|
if self._settings_fields != self._settings_connection_fields():
|
||||||
|
return FiatStatusResponse("Connection settings have changed.", 0)
|
||||||
|
return FiatStatusResponse(balance=0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = await self.client.get(url="/v1/balance", timeout=15)
|
||||||
|
r.raise_for_status()
|
||||||
|
data = r.json()
|
||||||
|
|
||||||
|
available_balance = data.get("available", [{}])[0].get("amount", 0)
|
||||||
|
# pending_balance = data.get("pending", {}).get("amount", 0)
|
||||||
|
|
||||||
|
return FiatStatusResponse(balance=available_balance)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return FiatStatusResponse("Server error: 'invalid json response'", 0)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(exc)
|
||||||
|
return FiatStatusResponse(f"Unable to connect to {self.endpoint}.", 0)
|
||||||
|
|
||||||
|
async def create_invoice(
|
||||||
|
self,
|
||||||
|
amount: float,
|
||||||
|
payment_hash: str,
|
||||||
|
currency: str,
|
||||||
|
memo: Optional[str] = None,
|
||||||
|
**kwargs,
|
||||||
|
) -> FiatInvoiceResponse:
|
||||||
|
amount_cents = int(amount * 100)
|
||||||
|
form_data = [
|
||||||
|
("mode", "payment"),
|
||||||
|
(
|
||||||
|
"success_url",
|
||||||
|
settings.stripe_payment_success_url or "https://lnbits.com",
|
||||||
|
),
|
||||||
|
("metadata[payment_hash]", payment_hash),
|
||||||
|
("line_items[0][price_data][currency]", currency.lower()),
|
||||||
|
("line_items[0][price_data][product_data][name]", memo or "LNbits Invoice"),
|
||||||
|
("line_items[0][price_data][unit_amount]", amount_cents),
|
||||||
|
("line_items[0][quantity]", "1"),
|
||||||
|
]
|
||||||
|
encoded_data = urlencode(form_data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
headers = self.headers.copy()
|
||||||
|
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||||
|
r = await self.client.post(
|
||||||
|
url="/v1/checkout/sessions", headers=headers, content=encoded_data
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
data = r.json()
|
||||||
|
|
||||||
|
session_id = data.get("id")
|
||||||
|
if not session_id:
|
||||||
|
return FiatInvoiceResponse(
|
||||||
|
ok=False, error_message="Server error: 'missing session id'"
|
||||||
|
)
|
||||||
|
payment_request = data.get("url")
|
||||||
|
if not payment_request:
|
||||||
|
return FiatInvoiceResponse(
|
||||||
|
ok=False, error_message="Server error: 'missing payment URL'"
|
||||||
|
)
|
||||||
|
|
||||||
|
return FiatInvoiceResponse(
|
||||||
|
ok=True, checking_id=session_id, payment_request=payment_request
|
||||||
|
)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return FiatInvoiceResponse(
|
||||||
|
ok=False, error_message="Server error: 'invalid json response'"
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(exc)
|
||||||
|
return FiatInvoiceResponse(
|
||||||
|
ok=False, error_message=f"Unable to connect to {self.endpoint}."
|
||||||
|
)
|
||||||
|
|
||||||
|
async def pay_invoice(self, payment_request: str) -> FiatPaymentResponse:
|
||||||
|
raise NotImplementedError("Stripe does not support paying invoices directly.")
|
||||||
|
|
||||||
|
async def get_invoice_status(self, checking_id: str) -> FiatPaymentStatus:
|
||||||
|
try:
|
||||||
|
r = await self.client.get(
|
||||||
|
url=f"/v1/checkout/sessions/{checking_id}",
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
data = r.json()
|
||||||
|
payment_status = data.get("payment_status")
|
||||||
|
if not payment_status:
|
||||||
|
return FiatPaymentPendingStatus()
|
||||||
|
if payment_status == "paid":
|
||||||
|
# todo: handle fee
|
||||||
|
return FiatPaymentSuccessStatus()
|
||||||
|
|
||||||
|
expires_at = data.get("expires_at")
|
||||||
|
_24_hours_ago = datetime.now(timezone.utc) - timedelta(hours=24)
|
||||||
|
if expires_at and expires_at < _24_hours_ago.timestamp():
|
||||||
|
# be defensive: add a 24 hour buffer
|
||||||
|
return FiatPaymentFailedStatus()
|
||||||
|
|
||||||
|
return FiatPaymentPendingStatus()
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug(f"Error getting invoice status: {exc}")
|
||||||
|
return FiatPaymentPendingStatus()
|
||||||
|
|
||||||
|
async def get_payment_status(self, checking_id: str) -> FiatPaymentStatus:
|
||||||
|
raise NotImplementedError("Stripe does not support outgoing payments.")
|
||||||
|
|
||||||
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
|
logger.warning(
|
||||||
|
"Stripe does not support paid invoices stream. Use webhooks instead."
|
||||||
|
)
|
||||||
|
mock_queue: asyncio.Queue[str] = asyncio.Queue(0)
|
||||||
|
while settings.lnbits_running:
|
||||||
|
value = await mock_queue.get()
|
||||||
|
yield value
|
||||||
|
|
||||||
|
def _settings_connection_fields(self) -> str:
|
||||||
|
return "-".join(
|
||||||
|
[str(settings.stripe_api_endpoint), str(settings.stripe_api_secret_key)]
|
||||||
|
)
|
||||||
+22
-4
@@ -3,7 +3,7 @@ import json
|
|||||||
import re
|
import re
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Optional, Type
|
from typing import Any, Optional
|
||||||
from urllib import request
|
from urllib import request
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
@@ -17,7 +17,6 @@ from pydantic.schema import field_schema
|
|||||||
|
|
||||||
from lnbits.jinja2_templating import Jinja2Templates
|
from lnbits.jinja2_templating import Jinja2Templates
|
||||||
from lnbits.nodes import get_node_class
|
from lnbits.nodes import get_node_class
|
||||||
from lnbits.requestvars import g
|
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.utils.crypto import AESCipher
|
from lnbits.utils.crypto import AESCipher
|
||||||
|
|
||||||
@@ -42,7 +41,7 @@ def urlsafe_short_hash() -> str:
|
|||||||
|
|
||||||
|
|
||||||
def url_for(endpoint: str, external: Optional[bool] = False, **params: Any) -> str:
|
def url_for(endpoint: str, external: Optional[bool] = False, **params: Any) -> str:
|
||||||
base = g().base_url if external else ""
|
base = f"http://{settings.host}:{settings.port}" if external else ""
|
||||||
url_params = "?"
|
url_params = "?"
|
||||||
for key, value in params.items():
|
for key, value in params.items():
|
||||||
url_params += f"{key}={value}&"
|
url_params += f"{key}={value}&"
|
||||||
@@ -154,7 +153,7 @@ def get_current_extension_name() -> str:
|
|||||||
return ext_name
|
return ext_name
|
||||||
|
|
||||||
|
|
||||||
def generate_filter_params_openapi(model: Type[FilterModel], keep_optional=False):
|
def generate_filter_params_openapi(model: type[FilterModel], keep_optional=False):
|
||||||
"""
|
"""
|
||||||
Generate openapi documentation for Filters. This is intended to be used along
|
Generate openapi documentation for Filters. This is intended to be used along
|
||||||
parse_filters (see example)
|
parse_filters (see example)
|
||||||
@@ -198,6 +197,14 @@ def is_valid_username(username: str) -> bool:
|
|||||||
return re.fullmatch(username_regex, username) is not None
|
return re.fullmatch(username_regex, username) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_external_id(external_id: str) -> bool:
|
||||||
|
if len(external_id) > 256:
|
||||||
|
return False
|
||||||
|
if " " in external_id or "\n" in external_id:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def is_valid_pubkey(pubkey: str) -> bool:
|
def is_valid_pubkey(pubkey: str) -> bool:
|
||||||
if len(pubkey) != 64:
|
if len(pubkey) != 64:
|
||||||
return False
|
return False
|
||||||
@@ -355,3 +362,14 @@ def safe_upload_file_path(filename: str, directory: str = "images") -> Path:
|
|||||||
# Prevent filename with subdirectories
|
# Prevent filename with subdirectories
|
||||||
file_path = image_folder / filename.split("/")[-1]
|
file_path = image_folder / filename.split("/")[-1]
|
||||||
return file_path.resolve()
|
return file_path.resolve()
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_endpoint(endpoint: str, add_proto=True) -> str:
|
||||||
|
endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
|
||||||
|
if add_proto:
|
||||||
|
if endpoint.startswith("ws://") or endpoint.startswith("wss://"):
|
||||||
|
return endpoint
|
||||||
|
endpoint = (
|
||||||
|
f"https://{endpoint}" if not endpoint.startswith("http") else endpoint
|
||||||
|
)
|
||||||
|
return endpoint
|
||||||
|
|||||||
+2
-2
@@ -58,9 +58,9 @@ class LnurlErrorResponseHandler(APIRoute):
|
|||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"LnurlErrorResponse",
|
||||||
|
"LnurlErrorResponseHandler",
|
||||||
"decode",
|
"decode",
|
||||||
"encode",
|
"encode",
|
||||||
"handle",
|
"handle",
|
||||||
"LnurlErrorResponse",
|
|
||||||
"LnurlErrorResponseHandler",
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import asyncio
|
|||||||
import json
|
import json
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import Any, List, Optional, Union
|
from typing import Any, Optional, Union
|
||||||
|
|
||||||
from fastapi import FastAPI, Request, Response
|
from fastapi import FastAPI, Request, Response
|
||||||
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
||||||
@@ -61,7 +61,7 @@ class InstalledExtensionMiddleware:
|
|||||||
await self.app(scope, receive, send)
|
await self.app(scope, receive, send)
|
||||||
|
|
||||||
def _response_by_accepted_type(
|
def _response_by_accepted_type(
|
||||||
self, scope: Scope, headers: List[Any], msg: str, status_code: HTTPStatus
|
self, scope: Scope, headers: list[Any], msg: str, status_code: HTTPStatus
|
||||||
) -> Union[HTMLResponse, JSONResponse]:
|
) -> Union[HTMLResponse, JSONResponse]:
|
||||||
"""
|
"""
|
||||||
Build an HTTP response containing the `msg` as HTTP body and the `status_code`
|
Build an HTTP response containing the `msg` as HTTP body and the `status_code`
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
import contextvars
|
|
||||||
import types
|
|
||||||
|
|
||||||
request_global = contextvars.ContextVar(
|
|
||||||
"request_global", default=types.SimpleNamespace()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def g() -> types.SimpleNamespace:
|
|
||||||
return request_global.get()
|
|
||||||
@@ -565,6 +565,34 @@ class StrikeFundingSource(LNbitsSettings):
|
|||||||
strike_api_key: str | None = Field(default=None, env="STRIKE_API_KEY")
|
strike_api_key: str | None = Field(default=None, env="STRIKE_API_KEY")
|
||||||
|
|
||||||
|
|
||||||
|
class FiatProviderLimits(BaseModel):
|
||||||
|
# empty list means all users are allowed to receive payments via Stripe
|
||||||
|
allowed_users: list[str] = Field(default=[])
|
||||||
|
|
||||||
|
service_max_fee_sats: int = Field(default=0)
|
||||||
|
service_fee_percent: float = Field(default=0)
|
||||||
|
service_fee_wallet_id: str | None = Field(default=None)
|
||||||
|
|
||||||
|
service_min_amount_sats: int = Field(default=0)
|
||||||
|
service_max_amount_sats: int = Field(default=0)
|
||||||
|
service_faucet_wallet_id: str | None = Field(default="")
|
||||||
|
|
||||||
|
|
||||||
|
class StripeFiatProvider(LNbitsSettings):
|
||||||
|
stripe_enabled: bool = Field(default=False)
|
||||||
|
stripe_api_endpoint: str = Field(default="https://api.stripe.com")
|
||||||
|
stripe_api_secret_key: str | None = Field(default=None)
|
||||||
|
stripe_payment_success_url: str = Field(default="https://lnbits.com")
|
||||||
|
|
||||||
|
stripe_payment_webhook_url: str = Field(
|
||||||
|
default="https://your-lnbits-domain-here.com/api/v1/callback/stripe"
|
||||||
|
)
|
||||||
|
# Use this secret to verify that events come from Stripe.
|
||||||
|
stripe_webhook_signing_secret: str | None = Field(default=None)
|
||||||
|
|
||||||
|
stripe_limits: FiatProviderLimits = Field(default_factory=FiatProviderLimits)
|
||||||
|
|
||||||
|
|
||||||
class LightningSettings(LNbitsSettings):
|
class LightningSettings(LNbitsSettings):
|
||||||
lightning_invoice_expiry: int = Field(default=3600, gt=0)
|
lightning_invoice_expiry: int = Field(default=3600, gt=0)
|
||||||
|
|
||||||
@@ -597,6 +625,40 @@ class FundingSourcesSettings(
|
|||||||
lnbits_funding_source_pay_invoice_wait_seconds: int = Field(default=5, ge=0)
|
lnbits_funding_source_pay_invoice_wait_seconds: int = Field(default=5, ge=0)
|
||||||
|
|
||||||
|
|
||||||
|
class FiatProvidersSettings(StripeFiatProvider):
|
||||||
|
|
||||||
|
def is_fiat_provider_enabled(self, provider: str | None) -> bool:
|
||||||
|
"""
|
||||||
|
Checks if a specific fiat provider is enabled.
|
||||||
|
"""
|
||||||
|
if not provider:
|
||||||
|
return False
|
||||||
|
if provider == "stripe":
|
||||||
|
return self.stripe_enabled
|
||||||
|
# Add checks for other fiat providers here as needed
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_fiat_providers_for_user(self, user_id: str) -> list[str]:
|
||||||
|
"""
|
||||||
|
Returns a list of fiat payment methods allowed for the user.
|
||||||
|
"""
|
||||||
|
allowed_providers = []
|
||||||
|
if self.stripe_enabled and (
|
||||||
|
not self.stripe_limits.allowed_users
|
||||||
|
or user_id in self.stripe_limits.allowed_users
|
||||||
|
):
|
||||||
|
allowed_providers.append("stripe")
|
||||||
|
|
||||||
|
# Add other fiat providers here as needed
|
||||||
|
return allowed_providers
|
||||||
|
|
||||||
|
def get_fiat_provider_limits(self, provider_name: str) -> FiatProviderLimits | None:
|
||||||
|
"""
|
||||||
|
Returns the limits for a specific fiat provider.
|
||||||
|
"""
|
||||||
|
return getattr(self, provider_name + "_limits", None)
|
||||||
|
|
||||||
|
|
||||||
class WebPushSettings(LNbitsSettings):
|
class WebPushSettings(LNbitsSettings):
|
||||||
lnbits_webpush_pubkey: str | None = Field(default=None)
|
lnbits_webpush_pubkey: str | None = Field(default=None)
|
||||||
lnbits_webpush_privkey: str | None = Field(default=None)
|
lnbits_webpush_privkey: str | None = Field(default=None)
|
||||||
@@ -769,6 +831,7 @@ class EditableSettings(
|
|||||||
SecuritySettings,
|
SecuritySettings,
|
||||||
NotificationsSettings,
|
NotificationsSettings,
|
||||||
FundingSourcesSettings,
|
FundingSourcesSettings,
|
||||||
|
FiatProvidersSettings,
|
||||||
LightningSettings,
|
LightningSettings,
|
||||||
WebPushSettings,
|
WebPushSettings,
|
||||||
NodeUISettings,
|
NodeUISettings,
|
||||||
@@ -963,6 +1026,12 @@ class Settings(EditableSettings, ReadOnlySettings, TransientSettings, BaseSettin
|
|||||||
def is_installed_extension_id(self, ext_id: str) -> bool:
|
def is_installed_extension_id(self, ext_id: str) -> bool:
|
||||||
return ext_id in self.lnbits_installed_extensions_ids
|
return ext_id in self.lnbits_installed_extensions_ids
|
||||||
|
|
||||||
|
def is_ready_to_install_extension_id(self, ext_id: str) -> bool:
|
||||||
|
return (
|
||||||
|
ext_id not in self.lnbits_installed_extensions_ids
|
||||||
|
and ext_id in self.lnbits_all_extensions_ids
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SuperSettings(EditableSettings):
|
class SuperSettings(EditableSettings):
|
||||||
super_user: str
|
super_user: str
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+10
-10
File diff suppressed because one or more lines are too long
+173
-572
@@ -1,617 +1,218 @@
|
|||||||
body[data-theme=classic].body--light {
|
body[data-theme=classic].neon-border .q-card,
|
||||||
background-color: #f4f4f4;
|
body[data-theme=classic].neon-border .q-card.q-card--dark,
|
||||||
|
body[data-theme=classic].neon-border .q-date,
|
||||||
|
body[data-theme=classic].neon-border .q-date--dark {
|
||||||
|
border: 2px solid #673ab7;
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[data-theme=classic].body--light {
|
body[data-theme=bitcoin].neon-border .q-card,
|
||||||
background-color: #f4f4f4;
|
body[data-theme=bitcoin].neon-border .q-card.q-card--dark,
|
||||||
|
body[data-theme=bitcoin].neon-border .q-date,
|
||||||
|
body[data-theme=bitcoin].neon-border .q-date--dark {
|
||||||
|
border: 2px solid #ea611d;
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme=classic] .q-drawer--dark,
|
body[data-theme=freedom].neon-border .q-card,
|
||||||
body[data-theme=classic].body--dark,
|
body[data-theme=freedom].neon-border .q-card.q-card--dark,
|
||||||
[data-theme=classic] .q-menu--dark {
|
body[data-theme=freedom].neon-border .q-date,
|
||||||
background: #1f2234 !important;
|
body[data-theme=freedom].neon-border .q-date--dark {
|
||||||
|
border: 2px solid #e22156;
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* IF WANTING TO SET A DARKER BG COLOR IN THE FUTURE
|
body[data-theme=cyber].neon-border .q-card,
|
||||||
// set a darker body bg for all themes, when in "dark mode"
|
body[data-theme=cyber].neon-border .q-card.q-card--dark,
|
||||||
body[data-theme='classic'].body--dark {
|
body[data-theme=cyber].neon-border .q-date,
|
||||||
background: scale-color($color, $lightness: -60%);
|
body[data-theme=cyber].neon-border .q-date--dark {
|
||||||
}
|
border: 2px solid #7cb342;
|
||||||
*/
|
box-shadow: none;
|
||||||
body[data-theme=classic].body--light {
|
|
||||||
background-color: #f4f4f4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme=classic] .q-card--dark,
|
body[data-theme=mint].neon-border .q-card,
|
||||||
[data-theme=classic] .q-stepper--dark {
|
body[data-theme=mint].neon-border .q-card.q-card--dark,
|
||||||
|
body[data-theme=mint].neon-border .q-date,
|
||||||
|
body[data-theme=mint].neon-border .q-date--dark {
|
||||||
|
border: 2px solid #3ab77d;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme=autumn].neon-border .q-card,
|
||||||
|
body[data-theme=autumn].neon-border .q-card.q-card--dark,
|
||||||
|
body[data-theme=autumn].neon-border .q-date,
|
||||||
|
body[data-theme=autumn].neon-border .q-date--dark {
|
||||||
|
border: 2px solid #b7763a;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme=flamingo].neon-border .q-card,
|
||||||
|
body[data-theme=flamingo].neon-border .q-card.q-card--dark,
|
||||||
|
body[data-theme=flamingo].neon-border .q-date,
|
||||||
|
body[data-theme=flamingo].neon-border .q-date--dark {
|
||||||
|
border: 2px solid #ff00ff;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme=monochrome].neon-border .q-card,
|
||||||
|
body[data-theme=monochrome].neon-border .q-card.q-card--dark,
|
||||||
|
body[data-theme=monochrome].neon-border .q-date,
|
||||||
|
body[data-theme=monochrome].neon-border .q-date--dark {
|
||||||
|
border: 2px solid #494949;
|
||||||
|
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,
|
||||||
|
body.hard-border .q-date--dark {
|
||||||
|
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(255, 255, 255, 0.2784313725);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.retro-border .q-card,
|
||||||
|
body.retro-border .q-card.q-card--dark,
|
||||||
|
body.retro-border .q-date,
|
||||||
|
body.retro-border .q-date--dark {
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 1px 5px rgba(255, 255, 255, 0.2), 0 2px 2px rgba(255, 255, 255, 0.14), 0 3px 1px -2px rgba(255, 255, 255, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.no-border .q-card,
|
||||||
|
body.no-border .q-card.q-card--dark,
|
||||||
|
body.no-border .q-date,
|
||||||
|
body.no-border .q-date--dark {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme=classic] {
|
||||||
|
--q-primary: #673ab7;
|
||||||
|
--q-secondary: #9c27b0;
|
||||||
|
--q-dark-page: #1f2234;
|
||||||
|
}
|
||||||
|
body[data-theme=classic] [data-theme=classic] .q-card--dark,
|
||||||
|
body[data-theme=classic] [data-theme=classic] .q-stepper--dark {
|
||||||
background: #333646 !important;
|
background: #333646 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[data-theme=classic].body--light {
|
body[data-theme=bitcoin] {
|
||||||
background-color: #f4f4f4;
|
--q-primary: #ea611d;
|
||||||
|
--q-secondary: #e56f35;
|
||||||
|
--q-dark-page: #2d293b;
|
||||||
}
|
}
|
||||||
|
body[data-theme=bitcoin] [data-theme=bitcoin] .q-card--dark,
|
||||||
body[data-theme=classic].body--light {
|
body[data-theme=bitcoin] [data-theme=bitcoin] .q-stepper--dark {
|
||||||
background-color: #f4f4f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme=classic] .bg-primary {
|
|
||||||
background: #673ab7 !important;
|
|
||||||
}
|
|
||||||
[data-theme=classic] .text-primary {
|
|
||||||
color: #673ab7 !important;
|
|
||||||
}
|
|
||||||
[data-theme=classic] .bg-secondary {
|
|
||||||
background: #9c27b0 !important;
|
|
||||||
}
|
|
||||||
[data-theme=classic] .text-secondary {
|
|
||||||
color: #9c27b0 !important;
|
|
||||||
}
|
|
||||||
[data-theme=classic] .bg-dark {
|
|
||||||
background: #1f2234 !important;
|
|
||||||
}
|
|
||||||
[data-theme=classic] .text-dark {
|
|
||||||
color: #1f2234 !important;
|
|
||||||
}
|
|
||||||
[data-theme=classic] .bg-info {
|
|
||||||
background: #333646 !important;
|
|
||||||
}
|
|
||||||
[data-theme=classic] .text-info {
|
|
||||||
color: #333646 !important;
|
|
||||||
}
|
|
||||||
[data-theme=classic] .bg-marginal-bg {
|
|
||||||
background: #1f2234 !important;
|
|
||||||
}
|
|
||||||
[data-theme=classic] .text-marginal-bg {
|
|
||||||
color: #1f2234 !important;
|
|
||||||
}
|
|
||||||
[data-theme=classic] .bg-marginal-text {
|
|
||||||
background: #fff !important;
|
|
||||||
}
|
|
||||||
[data-theme=classic] .text-marginal-text {
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body[data-theme=bitcoin].body--light {
|
|
||||||
background-color: #f4f4f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
body[data-theme=bitcoin].body--light {
|
|
||||||
background-color: #f4f4f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme=bitcoin] .q-drawer--dark,
|
|
||||||
body[data-theme=bitcoin].body--dark,
|
|
||||||
[data-theme=bitcoin] .q-menu--dark {
|
|
||||||
background: #2d293b !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* IF WANTING TO SET A DARKER BG COLOR IN THE FUTURE
|
|
||||||
// set a darker body bg for all themes, when in "dark mode"
|
|
||||||
body[data-theme='bitcoin'].body--dark {
|
|
||||||
background: scale-color($color, $lightness: -60%);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
body[data-theme=bitcoin].body--light {
|
|
||||||
background-color: #f4f4f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme=bitcoin] .q-card--dark,
|
|
||||||
[data-theme=bitcoin] .q-stepper--dark {
|
|
||||||
background: #333646 !important;
|
background: #333646 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[data-theme=bitcoin].body--light {
|
body[data-theme=freedom] {
|
||||||
background-color: #f4f4f4;
|
--q-primary: #e22156;
|
||||||
|
--q-secondary: #b91a45;
|
||||||
|
--q-dark-page: #462f36;
|
||||||
|
}
|
||||||
|
body[data-theme=freedom] [data-theme=freedom] .q-card--dark,
|
||||||
|
body[data-theme=freedom] [data-theme=freedom] .q-stepper--dark {
|
||||||
|
background: #47393d !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[data-theme=bitcoin].body--light {
|
body[data-theme=cyber] {
|
||||||
background-color: #f4f4f4;
|
--q-primary: #7cb342;
|
||||||
|
--q-secondary: #558b2f;
|
||||||
|
--q-dark-page: #000;
|
||||||
|
}
|
||||||
|
body[data-theme=cyber] [data-theme=cyber] .q-card--dark,
|
||||||
|
body[data-theme=cyber] [data-theme=cyber] .q-stepper--dark {
|
||||||
|
background: #1f2915 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme=bitcoin] .bg-primary {
|
body[data-theme=mint] {
|
||||||
background: #ea611d !important;
|
--q-primary: #3ab77d;
|
||||||
|
--q-secondary: #27b065;
|
||||||
|
--q-dark-page: #1f342b;
|
||||||
}
|
}
|
||||||
[data-theme=bitcoin] .text-primary {
|
body[data-theme=mint] [data-theme=mint] .q-card--dark,
|
||||||
color: #ea611d !important;
|
body[data-theme=mint] [data-theme=mint] .q-stepper--dark {
|
||||||
}
|
|
||||||
[data-theme=bitcoin] .bg-secondary {
|
|
||||||
background: #e56f35 !important;
|
|
||||||
}
|
|
||||||
[data-theme=bitcoin] .text-secondary {
|
|
||||||
color: #e56f35 !important;
|
|
||||||
}
|
|
||||||
[data-theme=bitcoin] .bg-dark {
|
|
||||||
background: #2d293b !important;
|
|
||||||
}
|
|
||||||
[data-theme=bitcoin] .text-dark {
|
|
||||||
color: #2d293b !important;
|
|
||||||
}
|
|
||||||
[data-theme=bitcoin] .bg-info {
|
|
||||||
background: #333646 !important;
|
|
||||||
}
|
|
||||||
[data-theme=bitcoin] .text-info {
|
|
||||||
color: #333646 !important;
|
|
||||||
}
|
|
||||||
[data-theme=bitcoin] .bg-marginal-bg {
|
|
||||||
background: #000000 !important;
|
|
||||||
}
|
|
||||||
[data-theme=bitcoin] .text-marginal-bg {
|
|
||||||
color: #000000 !important;
|
|
||||||
}
|
|
||||||
[data-theme=bitcoin] .bg-marginal-text {
|
|
||||||
background: #fff !important;
|
|
||||||
}
|
|
||||||
[data-theme=bitcoin] .text-marginal-text {
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body[data-theme=freedom].body--light {
|
|
||||||
background-color: #f4f4f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
body[data-theme=freedom].body--light {
|
|
||||||
background-color: #f4f4f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme=freedom] .q-drawer--dark,
|
|
||||||
body[data-theme=freedom].body--dark,
|
|
||||||
[data-theme=freedom] .q-menu--dark {
|
|
||||||
background: #0a0a0a !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* IF WANTING TO SET A DARKER BG COLOR IN THE FUTURE
|
|
||||||
// set a darker body bg for all themes, when in "dark mode"
|
|
||||||
body[data-theme='freedom'].body--dark {
|
|
||||||
background: scale-color($color, $lightness: -60%);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
body[data-theme=freedom].body--light {
|
|
||||||
background-color: #f4f4f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme=freedom] .q-card--dark,
|
|
||||||
[data-theme=freedom] .q-stepper--dark {
|
|
||||||
background: #1b1b1b !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body[data-theme=freedom].body--light {
|
|
||||||
background-color: #f4f4f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
body[data-theme=freedom].body--light {
|
|
||||||
background-color: #f4f4f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme=freedom] .bg-primary {
|
|
||||||
background: #e22156 !important;
|
|
||||||
}
|
|
||||||
[data-theme=freedom] .text-primary {
|
|
||||||
color: #e22156 !important;
|
|
||||||
}
|
|
||||||
[data-theme=freedom] .bg-secondary {
|
|
||||||
background: #b91a45 !important;
|
|
||||||
}
|
|
||||||
[data-theme=freedom] .text-secondary {
|
|
||||||
color: #b91a45 !important;
|
|
||||||
}
|
|
||||||
[data-theme=freedom] .bg-dark {
|
|
||||||
background: #0a0a0a !important;
|
|
||||||
}
|
|
||||||
[data-theme=freedom] .text-dark {
|
|
||||||
color: #0a0a0a !important;
|
|
||||||
}
|
|
||||||
[data-theme=freedom] .bg-info {
|
|
||||||
background: #1b1b1b !important;
|
|
||||||
}
|
|
||||||
[data-theme=freedom] .text-info {
|
|
||||||
color: #1b1b1b !important;
|
|
||||||
}
|
|
||||||
[data-theme=freedom] .bg-marginal-bg {
|
|
||||||
background: #2d293b !important;
|
|
||||||
}
|
|
||||||
[data-theme=freedom] .text-marginal-bg {
|
|
||||||
color: #2d293b !important;
|
|
||||||
}
|
|
||||||
[data-theme=freedom] .bg-marginal-text {
|
|
||||||
background: #fff !important;
|
|
||||||
}
|
|
||||||
[data-theme=freedom] .text-marginal-text {
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body[data-theme=cyber].body--light {
|
|
||||||
background-color: #f4f4f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
body[data-theme=cyber].body--light {
|
|
||||||
background-color: #f4f4f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme=cyber] .q-drawer--dark,
|
|
||||||
body[data-theme=cyber].body--dark,
|
|
||||||
[data-theme=cyber] .q-menu--dark {
|
|
||||||
background: #0a0a0a !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* IF WANTING TO SET A DARKER BG COLOR IN THE FUTURE
|
|
||||||
// set a darker body bg for all themes, when in "dark mode"
|
|
||||||
body[data-theme='cyber'].body--dark {
|
|
||||||
background: scale-color($color, $lightness: -60%);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
body[data-theme=cyber].body--light {
|
|
||||||
background-color: #f4f4f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme=cyber] .q-card--dark,
|
|
||||||
[data-theme=cyber] .q-stepper--dark {
|
|
||||||
background: #1b1b1b !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body[data-theme=cyber].body--light {
|
|
||||||
background-color: #f4f4f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
body[data-theme=cyber].body--light {
|
|
||||||
background-color: #f4f4f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme=cyber] .bg-primary {
|
|
||||||
background: #7cb342 !important;
|
|
||||||
}
|
|
||||||
[data-theme=cyber] .text-primary {
|
|
||||||
color: #7cb342 !important;
|
|
||||||
}
|
|
||||||
[data-theme=cyber] .bg-secondary {
|
|
||||||
background: #558b2f !important;
|
|
||||||
}
|
|
||||||
[data-theme=cyber] .text-secondary {
|
|
||||||
color: #558b2f !important;
|
|
||||||
}
|
|
||||||
[data-theme=cyber] .bg-dark {
|
|
||||||
background: #0a0a0a !important;
|
|
||||||
}
|
|
||||||
[data-theme=cyber] .text-dark {
|
|
||||||
color: #0a0a0a !important;
|
|
||||||
}
|
|
||||||
[data-theme=cyber] .bg-info {
|
|
||||||
background: #1b1b1b !important;
|
|
||||||
}
|
|
||||||
[data-theme=cyber] .text-info {
|
|
||||||
color: #1b1b1b !important;
|
|
||||||
}
|
|
||||||
[data-theme=cyber] .bg-marginal-bg {
|
|
||||||
background: #2d293b !important;
|
|
||||||
}
|
|
||||||
[data-theme=cyber] .text-marginal-bg {
|
|
||||||
color: #2d293b !important;
|
|
||||||
}
|
|
||||||
[data-theme=cyber] .bg-marginal-text {
|
|
||||||
background: #fff !important;
|
|
||||||
}
|
|
||||||
[data-theme=cyber] .text-marginal-text {
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body[data-theme=mint].body--light {
|
|
||||||
background-color: #f4f4f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
body[data-theme=mint].body--light {
|
|
||||||
background-color: #f4f4f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme=mint] .q-drawer--dark,
|
|
||||||
body[data-theme=mint].body--dark,
|
|
||||||
[data-theme=mint] .q-menu--dark {
|
|
||||||
background: #1f342b !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* IF WANTING TO SET A DARKER BG COLOR IN THE FUTURE
|
|
||||||
// set a darker body bg for all themes, when in "dark mode"
|
|
||||||
body[data-theme='mint'].body--dark {
|
|
||||||
background: scale-color($color, $lightness: -60%);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
body[data-theme=mint].body--light {
|
|
||||||
background-color: #f4f4f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme=mint] .q-card--dark,
|
|
||||||
[data-theme=mint] .q-stepper--dark {
|
|
||||||
background: #334642 !important;
|
background: #334642 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[data-theme=mint].body--light {
|
body[data-theme=autumn] {
|
||||||
background-color: #f4f4f4;
|
--q-primary: #b7763a;
|
||||||
|
--q-secondary: #b07927;
|
||||||
|
--q-dark-page: #34291f;
|
||||||
}
|
}
|
||||||
|
body[data-theme=autumn] [data-theme=autumn] .q-card--dark,
|
||||||
body[data-theme=mint].body--light {
|
body[data-theme=autumn] [data-theme=autumn] .q-stepper--dark {
|
||||||
background-color: #f4f4f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme=mint] .bg-primary {
|
|
||||||
background: #3ab77d !important;
|
|
||||||
}
|
|
||||||
[data-theme=mint] .text-primary {
|
|
||||||
color: #3ab77d !important;
|
|
||||||
}
|
|
||||||
[data-theme=mint] .bg-secondary {
|
|
||||||
background: #27b065 !important;
|
|
||||||
}
|
|
||||||
[data-theme=mint] .text-secondary {
|
|
||||||
color: #27b065 !important;
|
|
||||||
}
|
|
||||||
[data-theme=mint] .bg-dark {
|
|
||||||
background: #1f342b !important;
|
|
||||||
}
|
|
||||||
[data-theme=mint] .text-dark {
|
|
||||||
color: #1f342b !important;
|
|
||||||
}
|
|
||||||
[data-theme=mint] .bg-info {
|
|
||||||
background: #334642 !important;
|
|
||||||
}
|
|
||||||
[data-theme=mint] .text-info {
|
|
||||||
color: #334642 !important;
|
|
||||||
}
|
|
||||||
[data-theme=mint] .bg-marginal-bg {
|
|
||||||
background: #1f342b !important;
|
|
||||||
}
|
|
||||||
[data-theme=mint] .text-marginal-bg {
|
|
||||||
color: #1f342b !important;
|
|
||||||
}
|
|
||||||
[data-theme=mint] .bg-marginal-text {
|
|
||||||
background: #fff !important;
|
|
||||||
}
|
|
||||||
[data-theme=mint] .text-marginal-text {
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body[data-theme=autumn].body--light {
|
|
||||||
background-color: #f4f4f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
body[data-theme=autumn].body--light {
|
|
||||||
background-color: #f4f4f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme=autumn] .q-drawer--dark,
|
|
||||||
body[data-theme=autumn].body--dark,
|
|
||||||
[data-theme=autumn] .q-menu--dark {
|
|
||||||
background: #34291f !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* IF WANTING TO SET A DARKER BG COLOR IN THE FUTURE
|
|
||||||
// set a darker body bg for all themes, when in "dark mode"
|
|
||||||
body[data-theme='autumn'].body--dark {
|
|
||||||
background: scale-color($color, $lightness: -60%);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
body[data-theme=autumn].body--light {
|
|
||||||
background-color: #f4f4f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme=autumn] .q-card--dark,
|
|
||||||
[data-theme=autumn] .q-stepper--dark {
|
|
||||||
background: #463f33 !important;
|
background: #463f33 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[data-theme=autumn].body--light {
|
body[data-theme=flamingo] {
|
||||||
background-color: #f4f4f4;
|
--q-primary: #ff00ff;
|
||||||
|
--q-secondary: #fda3fd;
|
||||||
|
--q-dark-page: #2f032f;
|
||||||
}
|
}
|
||||||
|
body[data-theme=flamingo] [data-theme=flamingo] .q-card--dark,
|
||||||
body[data-theme=autumn].body--light {
|
body[data-theme=flamingo] [data-theme=flamingo] .q-stepper--dark {
|
||||||
background-color: #f4f4f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme=autumn] .bg-primary {
|
|
||||||
background: #b7763a !important;
|
|
||||||
}
|
|
||||||
[data-theme=autumn] .text-primary {
|
|
||||||
color: #b7763a !important;
|
|
||||||
}
|
|
||||||
[data-theme=autumn] .bg-secondary {
|
|
||||||
background: #b07927 !important;
|
|
||||||
}
|
|
||||||
[data-theme=autumn] .text-secondary {
|
|
||||||
color: #b07927 !important;
|
|
||||||
}
|
|
||||||
[data-theme=autumn] .bg-dark {
|
|
||||||
background: #34291f !important;
|
|
||||||
}
|
|
||||||
[data-theme=autumn] .text-dark {
|
|
||||||
color: #34291f !important;
|
|
||||||
}
|
|
||||||
[data-theme=autumn] .bg-info {
|
|
||||||
background: #463f33 !important;
|
|
||||||
}
|
|
||||||
[data-theme=autumn] .text-info {
|
|
||||||
color: #463f33 !important;
|
|
||||||
}
|
|
||||||
[data-theme=autumn] .bg-marginal-bg {
|
|
||||||
background: #342a1f !important;
|
|
||||||
}
|
|
||||||
[data-theme=autumn] .text-marginal-bg {
|
|
||||||
color: #342a1f !important;
|
|
||||||
}
|
|
||||||
[data-theme=autumn] .bg-marginal-text {
|
|
||||||
background: rgb(255, 255, 255) !important;
|
|
||||||
}
|
|
||||||
[data-theme=autumn] .text-marginal-text {
|
|
||||||
color: rgb(255, 255, 255) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body[data-theme=flamingo].body--light {
|
|
||||||
background-color: #f4f4f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
body[data-theme=flamingo].body--light {
|
|
||||||
background-color: #f4f4f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme=flamingo] .q-drawer--dark,
|
|
||||||
body[data-theme=flamingo].body--dark,
|
|
||||||
[data-theme=flamingo] .q-menu--dark {
|
|
||||||
background: #2f032f !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* IF WANTING TO SET A DARKER BG COLOR IN THE FUTURE
|
|
||||||
// set a darker body bg for all themes, when in "dark mode"
|
|
||||||
body[data-theme='flamingo'].body--dark {
|
|
||||||
background: scale-color($color, $lightness: -60%);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
body[data-theme=flamingo].body--light {
|
|
||||||
background-color: #f4f4f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme=flamingo] .q-card--dark,
|
|
||||||
[data-theme=flamingo] .q-stepper--dark {
|
|
||||||
background: #bc23bc !important;
|
background: #bc23bc !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[data-theme=flamingo].body--light {
|
body[data-theme=monochrome] {
|
||||||
background-color: #f4f4f4;
|
--q-primary: #494949;
|
||||||
|
--q-secondary: #6b6b6b;
|
||||||
|
--q-dark-page: #000;
|
||||||
}
|
}
|
||||||
|
body[data-theme=monochrome] [data-theme=monochrome] .q-card--dark,
|
||||||
body[data-theme=flamingo].body--light {
|
body[data-theme=monochrome] [data-theme=monochrome] .q-stepper--dark {
|
||||||
background-color: #f4f4f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme=flamingo] .bg-primary {
|
|
||||||
background: #ff00ff !important;
|
|
||||||
}
|
|
||||||
[data-theme=flamingo] .text-primary {
|
|
||||||
color: #ff00ff !important;
|
|
||||||
}
|
|
||||||
[data-theme=flamingo] .bg-secondary {
|
|
||||||
background: #fda3fd !important;
|
|
||||||
}
|
|
||||||
[data-theme=flamingo] .text-secondary {
|
|
||||||
color: #fda3fd !important;
|
|
||||||
}
|
|
||||||
[data-theme=flamingo] .bg-dark {
|
|
||||||
background: #2f032f !important;
|
|
||||||
}
|
|
||||||
[data-theme=flamingo] .text-dark {
|
|
||||||
color: #2f032f !important;
|
|
||||||
}
|
|
||||||
[data-theme=flamingo] .bg-info {
|
|
||||||
background: #bc23bc !important;
|
|
||||||
}
|
|
||||||
[data-theme=flamingo] .text-info {
|
|
||||||
color: #bc23bc !important;
|
|
||||||
}
|
|
||||||
[data-theme=flamingo] .bg-marginal-bg {
|
|
||||||
background: #311231 !important;
|
|
||||||
}
|
|
||||||
[data-theme=flamingo] .text-marginal-bg {
|
|
||||||
color: #311231 !important;
|
|
||||||
}
|
|
||||||
[data-theme=flamingo] .bg-marginal-text {
|
|
||||||
background: rgb(255, 255, 255) !important;
|
|
||||||
}
|
|
||||||
[data-theme=flamingo] .text-marginal-text {
|
|
||||||
color: rgb(255, 255, 255) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body[data-theme=monochrome].body--light {
|
|
||||||
background-color: #f4f4f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
body[data-theme=monochrome].body--light {
|
|
||||||
background-color: #f4f4f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme=monochrome] .q-drawer--dark,
|
|
||||||
body[data-theme=monochrome].body--dark,
|
|
||||||
[data-theme=monochrome] .q-menu--dark {
|
|
||||||
background: #000 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* IF WANTING TO SET A DARKER BG COLOR IN THE FUTURE
|
|
||||||
// set a darker body bg for all themes, when in "dark mode"
|
|
||||||
body[data-theme='monochrome'].body--dark {
|
|
||||||
background: scale-color($color, $lightness: -60%);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
body[data-theme=monochrome].body--light {
|
|
||||||
background-color: #f4f4f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme=monochrome] .q-card--dark,
|
|
||||||
[data-theme=monochrome] .q-stepper--dark {
|
|
||||||
background: rgb(39, 39, 39) !important;
|
background: rgb(39, 39, 39) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[data-theme=monochrome].body--light {
|
body[data-theme=salvador] {
|
||||||
background-color: #f4f4f4;
|
--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[data-theme=monochrome].body--light {
|
body.gradient-bg {
|
||||||
background-color: #f4f4f4;
|
background-image: linear-gradient(to bottom right, var(--q-dark-page), #0a0a0a);
|
||||||
|
background-attachment: fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme=monochrome] .bg-primary {
|
body.bg-image::before {
|
||||||
background: #494949 !important;
|
content: "";
|
||||||
|
position: fixed;
|
||||||
|
z-index: -1;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
filter: blur(8px);
|
||||||
|
background-image: var(--background);
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
}
|
}
|
||||||
[data-theme=monochrome] .text-primary {
|
body.bg-image .q-page-container {
|
||||||
color: #494949 !important;
|
backdrop-filter: none; /* Ensure the page content is not affected */
|
||||||
}
|
|
||||||
[data-theme=monochrome] .bg-secondary {
|
|
||||||
background: #6b6b6b !important;
|
|
||||||
}
|
|
||||||
[data-theme=monochrome] .text-secondary {
|
|
||||||
color: #6b6b6b !important;
|
|
||||||
}
|
|
||||||
[data-theme=monochrome] .bg-dark {
|
|
||||||
background: #000 !important;
|
|
||||||
}
|
|
||||||
[data-theme=monochrome] .text-dark {
|
|
||||||
color: #000 !important;
|
|
||||||
}
|
|
||||||
[data-theme=monochrome] .bg-info {
|
|
||||||
background: rgb(39, 39, 39) !important;
|
|
||||||
}
|
|
||||||
[data-theme=monochrome] .text-info {
|
|
||||||
color: rgb(39, 39, 39) !important;
|
|
||||||
}
|
|
||||||
[data-theme=monochrome] .bg-marginal-bg {
|
|
||||||
background: #000 !important;
|
|
||||||
}
|
|
||||||
[data-theme=monochrome] .text-marginal-bg {
|
|
||||||
color: #000 !important;
|
|
||||||
}
|
|
||||||
[data-theme=monochrome] .bg-marginal-text {
|
|
||||||
background: rgb(255, 255, 255) !important;
|
|
||||||
}
|
|
||||||
[data-theme=monochrome] .text-marginal-text {
|
|
||||||
color: rgb(255, 255, 255) !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme=freedom] .q-drawer--dark {
|
body.body--dark .q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark),
|
||||||
background: #0a0a0a !important;
|
body.body--dark .q-header,
|
||||||
}
|
body.body--dark .q-drawer {
|
||||||
|
--q-dark: rgba(29, 29, 29, 0.3);
|
||||||
[data-theme=freedom] .q-header {
|
background-color: var(--q-dark);
|
||||||
background: #0a0a0a !important;
|
backdrop-filter: blur(6px);
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme=cyber] .q-drawer--dark {
|
|
||||||
background: #0a0a0a !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme=cyber] .q-header {
|
|
||||||
background: #0a0a0a !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme=salvador] .q-drawer--dark {
|
|
||||||
background: #242424 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme=salvador] .q-header {
|
|
||||||
background: #0f47af !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[v-cloak] {
|
[v-cloak] {
|
||||||
|
|||||||
@@ -338,7 +338,7 @@ window.localisation.br = {
|
|||||||
node_balance: 'Saldo do Nó: {balance} sats',
|
node_balance: 'Saldo do Nó: {balance} sats',
|
||||||
lnbits_balance: 'Saldo do LNbits: {balance} sats',
|
lnbits_balance: 'Saldo do LNbits: {balance} sats',
|
||||||
funding_reserve_percent: 'Reserve Percentual: {percent} %',
|
funding_reserve_percent: 'Reserve Percentual: {percent} %',
|
||||||
node_managment: 'Gerenciamento de Nós',
|
node_management: 'Gerenciamento de Nós',
|
||||||
node_management_not_supported:
|
node_management_not_supported:
|
||||||
'Gerenciamento de nó não suportado pela fonte de financiamento ativa',
|
'Gerenciamento de nó não suportado pela fonte de financiamento ativa',
|
||||||
toggle_node_ui: 'Interface do Nó',
|
toggle_node_ui: 'Interface do Nó',
|
||||||
|
|||||||
@@ -320,7 +320,7 @@ window.localisation.cn = {
|
|||||||
node_balance: '节点余额:{balance} sats',
|
node_balance: '节点余额:{balance} sats',
|
||||||
lnbits_balance: 'LNbits 余额:{balance} sats',
|
lnbits_balance: 'LNbits 余额:{balance} sats',
|
||||||
funding_reserve_percent: '保留百分比: {percent} %',
|
funding_reserve_percent: '保留百分比: {percent} %',
|
||||||
node_managment: '节点管理',
|
node_management: '节点管理',
|
||||||
node_management_not_supported: '活动资金来源不支持节点管理',
|
node_management_not_supported: '活动资金来源不支持节点管理',
|
||||||
toggle_node_ui: '节点用户界面',
|
toggle_node_ui: '节点用户界面',
|
||||||
toggle_public_node_ui: '公共节点用户界面',
|
toggle_public_node_ui: '公共节点用户界面',
|
||||||
|
|||||||
@@ -332,7 +332,7 @@ window.localisation.cs = {
|
|||||||
node_balance: 'Stav uzlu: {balance} sats',
|
node_balance: 'Stav uzlu: {balance} sats',
|
||||||
lnbits_balance: 'Zůstatek LNbits: {balance} sats',
|
lnbits_balance: 'Zůstatek LNbits: {balance} sats',
|
||||||
funding_reserve_percent: 'Rezervovat procento: {percent} %',
|
funding_reserve_percent: 'Rezervovat procento: {percent} %',
|
||||||
node_managment: 'Správa uzlů',
|
node_management: 'Správa uzlů',
|
||||||
node_management_not_supported:
|
node_management_not_supported:
|
||||||
'Správa uzlů není podporována aktivním zdrojem financování',
|
'Správa uzlů není podporována aktivním zdrojem financování',
|
||||||
toggle_node_ui: 'Uživatelské rozhraní uzlu',
|
toggle_node_ui: 'Uživatelské rozhraní uzlu',
|
||||||
|
|||||||
@@ -342,7 +342,7 @@ window.localisation.de = {
|
|||||||
node_balance: 'Kontostand: {balance} Sats',
|
node_balance: 'Kontostand: {balance} Sats',
|
||||||
lnbits_balance: 'LNbits-Guthaben: {balance} Sats',
|
lnbits_balance: 'LNbits-Guthaben: {balance} Sats',
|
||||||
funding_reserve_percent: 'Reservieren Prozent: {percent} %',
|
funding_reserve_percent: 'Reservieren Prozent: {percent} %',
|
||||||
node_managment: 'Knotenverwaltung',
|
node_management: 'Knotenverwaltung',
|
||||||
node_management_not_supported:
|
node_management_not_supported:
|
||||||
'Knotenverwaltung wird von der aktiven Finanzierungsquelle nicht unterstützt',
|
'Knotenverwaltung wird von der aktiven Finanzierungsquelle nicht unterstützt',
|
||||||
toggle_node_ui: 'Node-Benutzeroberfläche',
|
toggle_node_ui: 'Node-Benutzeroberfläche',
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ window.localisation.en = {
|
|||||||
active_channels: 'Active Channels',
|
active_channels: 'Active Channels',
|
||||||
connect_peer: 'Connect Peer',
|
connect_peer: 'Connect Peer',
|
||||||
connect: 'Connect',
|
connect: 'Connect',
|
||||||
|
reconnect: 'Reconnect',
|
||||||
open_channel: 'Open Channel',
|
open_channel: 'Open Channel',
|
||||||
open: 'Open',
|
open: 'Open',
|
||||||
close_channel: 'Close Channel',
|
close_channel: 'Close Channel',
|
||||||
@@ -60,6 +61,7 @@ window.localisation.en = {
|
|||||||
rename_wallet: 'Rename wallet',
|
rename_wallet: 'Rename wallet',
|
||||||
update_name: 'Update name',
|
update_name: 'Update name',
|
||||||
fiat_tracking: 'Fiat tracking',
|
fiat_tracking: 'Fiat tracking',
|
||||||
|
fiat_providers: 'Fiat providers',
|
||||||
currency: 'Currency',
|
currency: 'Currency',
|
||||||
update_currency: 'Update currency',
|
update_currency: 'Update currency',
|
||||||
press_to_claim: 'Press to claim bitcoin',
|
press_to_claim: 'Press to claim bitcoin',
|
||||||
@@ -141,6 +143,7 @@ window.localisation.en = {
|
|||||||
uninstall: 'Uninstall',
|
uninstall: 'Uninstall',
|
||||||
drop_db: 'Remove Data',
|
drop_db: 'Remove Data',
|
||||||
enable: 'Enable',
|
enable: 'Enable',
|
||||||
|
enabled: 'Enabled',
|
||||||
pay_to_enable: 'Pay To Enable',
|
pay_to_enable: 'Pay To Enable',
|
||||||
enable_extension_details: 'Enable extension for current user',
|
enable_extension_details: 'Enable extension for current user',
|
||||||
disable: 'Disable',
|
disable: 'Disable',
|
||||||
@@ -171,12 +174,33 @@ window.localisation.en = {
|
|||||||
payment_hash: 'Payment Hash',
|
payment_hash: 'Payment Hash',
|
||||||
fee: 'Fee',
|
fee: 'Fee',
|
||||||
amount: 'Amount',
|
amount: 'Amount',
|
||||||
|
amount_limits: 'Amount Limits',
|
||||||
amount_sats: 'Amount (sats)',
|
amount_sats: 'Amount (sats)',
|
||||||
|
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: 'Tag',
|
tag: 'Tag',
|
||||||
unit: 'Unit',
|
unit: 'Unit',
|
||||||
description: 'Description',
|
description: 'Description',
|
||||||
expiry: 'Expiry',
|
expiry: 'Expiry',
|
||||||
webhook: 'Webhook',
|
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: 'Payment Proof',
|
payment_proof: 'Payment Proof',
|
||||||
update: 'Update',
|
update: 'Update',
|
||||||
update_available: 'Update {version} available!',
|
update_available: 'Update {version} available!',
|
||||||
@@ -350,12 +374,15 @@ window.localisation.en = {
|
|||||||
update_account: 'Update Account',
|
update_account: 'Update Account',
|
||||||
invalid_username: 'Invalid Username',
|
invalid_username: 'Invalid Username',
|
||||||
auth_provider: 'Auth Provider',
|
auth_provider: 'Auth Provider',
|
||||||
|
external_id: 'External ID',
|
||||||
my_account: 'My Account',
|
my_account: 'My Account',
|
||||||
existing_account_question: 'Already have an account?',
|
existing_account_question: 'Already have an account?',
|
||||||
background_image: 'Background Image',
|
background_image: 'Background Image',
|
||||||
back: 'Back',
|
back: 'Back',
|
||||||
logout: 'Logout',
|
logout: 'Logout',
|
||||||
look_and_feel: 'Look and Feel',
|
look_and_feel: 'Look and Feel',
|
||||||
|
endpoint: 'Endpoint',
|
||||||
|
api: 'API',
|
||||||
api_token: 'API Token',
|
api_token: 'API Token',
|
||||||
api_tokens: 'API Tokens',
|
api_tokens: 'API Tokens',
|
||||||
access_control_list: 'Access Control List',
|
access_control_list: 'Access Control List',
|
||||||
@@ -373,6 +400,8 @@ window.localisation.en = {
|
|||||||
extension_paid_sats: 'You have already paid {paid_sats} sats.',
|
extension_paid_sats: 'You have already paid {paid_sats} sats.',
|
||||||
release_details_error: 'Cannot get the release details.',
|
release_details_error: 'Cannot get the release details.',
|
||||||
pay_from_wallet: 'Pay from Wallet',
|
pay_from_wallet: 'Pay from Wallet',
|
||||||
|
pay_with: 'Pay with {provider}',
|
||||||
|
select_payment_provider: 'Select payment provider',
|
||||||
wallet_required: 'Wallet *',
|
wallet_required: 'Wallet *',
|
||||||
show_qr: 'Show QR',
|
show_qr: 'Show QR',
|
||||||
retry_install: 'Retry Install',
|
retry_install: 'Retry Install',
|
||||||
@@ -385,6 +414,9 @@ window.localisation.en = {
|
|||||||
'The {name} extension requires a payment of minimum {amount} sats to enable.',
|
'The {name} extension requires a payment of minimum {amount} sats to enable.',
|
||||||
hide_empty_wallets: 'Hide empty wallets',
|
hide_empty_wallets: 'Hide empty wallets',
|
||||||
recheck: 'Recheck',
|
recheck: 'Recheck',
|
||||||
|
check: 'Check',
|
||||||
|
check_connection: 'Check Connection',
|
||||||
|
check_webhook: 'Check Webhook',
|
||||||
contributors: 'Contributors',
|
contributors: 'Contributors',
|
||||||
license: 'License',
|
license: 'License',
|
||||||
reset_key: 'Reset Key',
|
reset_key: 'Reset Key',
|
||||||
@@ -446,7 +478,7 @@ window.localisation.en = {
|
|||||||
node_balance: 'Node Balance: {balance} sats',
|
node_balance: 'Node Balance: {balance} sats',
|
||||||
lnbits_balance: 'LNbits Balance: {balance} sats',
|
lnbits_balance: 'LNbits Balance: {balance} sats',
|
||||||
funding_reserve_percent: 'Reserve Percent: {percent} %',
|
funding_reserve_percent: 'Reserve Percent: {percent} %',
|
||||||
node_managment: 'Node Management',
|
node_management: 'Node Management',
|
||||||
node_management_not_supported:
|
node_management_not_supported:
|
||||||
'Node Management not supported by active funding source',
|
'Node Management not supported by active funding source',
|
||||||
toggle_node_ui: 'Node UI',
|
toggle_node_ui: 'Node UI',
|
||||||
@@ -491,7 +523,9 @@ window.localisation.en = {
|
|||||||
allowed_currencies_hint: 'Limit the number of available fiat currencies',
|
allowed_currencies_hint: 'Limit the number of available fiat currencies',
|
||||||
default_account_currency: 'Default Account Currency',
|
default_account_currency: 'Default Account Currency',
|
||||||
default_account_currency_hint: 'Default currency for accounting',
|
default_account_currency_hint: 'Default currency for accounting',
|
||||||
|
min_incoming_payment_amount: 'Min Incoming Payment Amount',
|
||||||
|
min_incoming_payment_amount_desc:
|
||||||
|
'Minimum amount allowed for generating an invoice',
|
||||||
max_incoming_payment_amount: 'Max Incoming Payment Amount',
|
max_incoming_payment_amount: 'Max Incoming Payment Amount',
|
||||||
max_incoming_payment_amount_desc:
|
max_incoming_payment_amount_desc:
|
||||||
'Maximum amount allowed for generating an invoice',
|
'Maximum amount allowed for generating an invoice',
|
||||||
@@ -553,6 +587,7 @@ window.localisation.en = {
|
|||||||
admin_users_label: 'User ID',
|
admin_users_label: 'User ID',
|
||||||
allowed_users: 'Allowed Users',
|
allowed_users: 'Allowed Users',
|
||||||
allowed_users_hint: 'Only these users can use LNbits',
|
allowed_users_hint: 'Only these users can use LNbits',
|
||||||
|
allowed_users_hint_feature: 'Only these users can use {feature}',
|
||||||
allowed_users_label: 'User ID',
|
allowed_users_label: 'User ID',
|
||||||
allow_creation_user: 'Allow creation of new users',
|
allow_creation_user: 'Allow creation of new users',
|
||||||
allow_creation_user_desc: 'Allow creation of new users on the index page',
|
allow_creation_user_desc: 'Allow creation of new users on the index page',
|
||||||
@@ -587,5 +622,12 @@ window.localisation.en = {
|
|||||||
view_column: 'View wallets as rows',
|
view_column: 'View wallets as rows',
|
||||||
filter_payments: 'Filter payments',
|
filter_payments: 'Filter payments',
|
||||||
filter_date: 'Filter by date',
|
filter_date: 'Filter by date',
|
||||||
websocket_example: 'Websocket example'
|
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'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -343,7 +343,7 @@ window.localisation.es = {
|
|||||||
node_balance: 'Balance de Nodo: {balance} sats',
|
node_balance: 'Balance de Nodo: {balance} sats',
|
||||||
lnbits_balance: 'Saldo de LNbits: {balance} sats',
|
lnbits_balance: 'Saldo de LNbits: {balance} sats',
|
||||||
funding_reserve_percent: 'Reserve Porcentaje: {percent} %',
|
funding_reserve_percent: 'Reserve Porcentaje: {percent} %',
|
||||||
node_managment: 'Gestión de nodos',
|
node_management: 'Gestión de nodos',
|
||||||
node_management_not_supported:
|
node_management_not_supported:
|
||||||
'La gestión de nodos no es compatible con la fuente de financiación activa',
|
'La gestión de nodos no es compatible con la fuente de financiación activa',
|
||||||
toggle_node_ui: 'Interfaz de usuario de nodo',
|
toggle_node_ui: 'Interfaz de usuario de nodo',
|
||||||
|
|||||||
@@ -434,7 +434,7 @@ window.localisation.fi = {
|
|||||||
node_balance: 'Solmun saldo: {balance} sats',
|
node_balance: 'Solmun saldo: {balance} sats',
|
||||||
lnbits_balance: 'LNbits-saldo: {balance} sat',
|
lnbits_balance: 'LNbits-saldo: {balance} sat',
|
||||||
funding_reserve_percent: 'Omavaraisuusaste: {percent} %',
|
funding_reserve_percent: 'Omavaraisuusaste: {percent} %',
|
||||||
node_managment: 'Solmun hallinta',
|
node_management: 'Solmun hallinta',
|
||||||
node_management_not_supported:
|
node_management_not_supported:
|
||||||
'Solmun hallinta ei ole mahdollista valitun rahoituslähteen kanssa.',
|
'Solmun hallinta ei ole mahdollista valitun rahoituslähteen kanssa.',
|
||||||
toggle_node_ui: 'Solmun käyttöliittymä',
|
toggle_node_ui: 'Solmun käyttöliittymä',
|
||||||
|
|||||||
@@ -346,7 +346,7 @@ window.localisation.fr = {
|
|||||||
node_balance: 'Solde du nœud : {balance} sats',
|
node_balance: 'Solde du nœud : {balance} sats',
|
||||||
lnbits_balance: 'Solde LNbits : {balance} sats',
|
lnbits_balance: 'Solde LNbits : {balance} sats',
|
||||||
funding_reserve_percent: 'Pourcentage de Réserve : {percent} %',
|
funding_reserve_percent: 'Pourcentage de Réserve : {percent} %',
|
||||||
node_managment: 'Gestion des nœuds',
|
node_management: 'Gestion des nœuds',
|
||||||
node_management_not_supported:
|
node_management_not_supported:
|
||||||
"La gestion des nœuds n'est pas prise en charge par la source de financement active",
|
"La gestion des nœuds n'est pas prise en charge par la source de financement active",
|
||||||
toggle_node_ui: 'Interface utilisateur de nœud',
|
toggle_node_ui: 'Interface utilisateur de nœud',
|
||||||
|
|||||||
@@ -343,7 +343,7 @@ window.localisation.it = {
|
|||||||
node_balance: 'Saldo Nodo: {balance} sats',
|
node_balance: 'Saldo Nodo: {balance} sats',
|
||||||
lnbits_balance: 'Saldo LNbits: {balance} sats',
|
lnbits_balance: 'Saldo LNbits: {balance} sats',
|
||||||
funding_reserve_percent: 'Riserva Percentuale: {percent} %',
|
funding_reserve_percent: 'Riserva Percentuale: {percent} %',
|
||||||
node_managment: 'Gestione dei nodi',
|
node_management: 'Gestione dei nodi',
|
||||||
node_management_not_supported:
|
node_management_not_supported:
|
||||||
'La gestione dei nodi non è supportata dalla fonte di finanziamento attiva.',
|
'La gestione dei nodi non è supportata dalla fonte di finanziamento attiva.',
|
||||||
toggle_node_ui: 'Interfaccia utente del nodo',
|
toggle_node_ui: 'Interfaccia utente del nodo',
|
||||||
|
|||||||
@@ -334,7 +334,7 @@ window.localisation.jp = {
|
|||||||
node_balance: 'ノード残高: {balance} サッツ',
|
node_balance: 'ノード残高: {balance} サッツ',
|
||||||
lnbits_balance: 'LNbits残高: {balance} sats',
|
lnbits_balance: 'LNbits残高: {balance} sats',
|
||||||
funding_reserve_percent: '予約パーセント: {percent} %',
|
funding_reserve_percent: '予約パーセント: {percent} %',
|
||||||
node_managment: 'ノード管理',
|
node_management: 'ノード管理',
|
||||||
node_management_not_supported:
|
node_management_not_supported:
|
||||||
'アクティブな資金源ではノード管理がサポートされていません',
|
'アクティブな資金源ではノード管理がサポートされていません',
|
||||||
toggle_node_ui: 'ノードUI',
|
toggle_node_ui: 'ノードUI',
|
||||||
|
|||||||
@@ -331,7 +331,7 @@ window.localisation.kr = {
|
|||||||
node_balance: '노드 잔액: {balance} 사토시',
|
node_balance: '노드 잔액: {balance} 사토시',
|
||||||
lnbits_balance: 'LNbits 잔액: {balance} sats',
|
lnbits_balance: 'LNbits 잔액: {balance} sats',
|
||||||
funding_reserve_percent: '예약 비율: {percent} %',
|
funding_reserve_percent: '예약 비율: {percent} %',
|
||||||
node_managment: '노드 관리',
|
node_management: '노드 관리',
|
||||||
node_management_not_supported:
|
node_management_not_supported:
|
||||||
'활성화된 자금 출처에 의해 노드 관리는 지원되지 않습니다.',
|
'활성화된 자금 출처에 의해 노드 관리는 지원되지 않습니다.',
|
||||||
toggle_node_ui: '노드 UI',
|
toggle_node_ui: '노드 UI',
|
||||||
|
|||||||
@@ -341,7 +341,7 @@ window.localisation.nl = {
|
|||||||
node_balance: 'Node Balans: {balance} sats',
|
node_balance: 'Node Balans: {balance} sats',
|
||||||
lnbits_balance: 'LNbits Saldo: {balance} sats',
|
lnbits_balance: 'LNbits Saldo: {balance} sats',
|
||||||
funding_reserve_percent: 'Reservepercentage: {percent} %',
|
funding_reserve_percent: 'Reservepercentage: {percent} %',
|
||||||
node_managment: 'Nodebeheer',
|
node_management: 'Nodebeheer',
|
||||||
node_management_not_supported:
|
node_management_not_supported:
|
||||||
'Nodebeheer wordt niet ondersteund door de actieve financieringsbron',
|
'Nodebeheer wordt niet ondersteund door de actieve financieringsbron',
|
||||||
toggle_node_ui: 'Node UI',
|
toggle_node_ui: 'Node UI',
|
||||||
|
|||||||
@@ -337,7 +337,7 @@ window.localisation.pi = {
|
|||||||
node_balance: 'Node Balance: {balance} doubloons',
|
node_balance: 'Node Balance: {balance} doubloons',
|
||||||
lnbits_balance: "LNbits Balance: {balance} pieces o' eight",
|
lnbits_balance: "LNbits Balance: {balance} pieces o' eight",
|
||||||
funding_reserve_percent: 'Reserve Percent: {percent} %',
|
funding_reserve_percent: 'Reserve Percent: {percent} %',
|
||||||
node_managment: 'Node Management',
|
node_management: 'Node Management',
|
||||||
node_management_not_supported:
|
node_management_not_supported:
|
||||||
'Node Management not be supported by active funding source',
|
'Node Management not be supported by active funding source',
|
||||||
toggle_node_ui: 'Node Main Deck',
|
toggle_node_ui: 'Node Main Deck',
|
||||||
|
|||||||
@@ -337,7 +337,7 @@ window.localisation.pl = {
|
|||||||
node_balance: 'Saldo węzła: {balance} sats',
|
node_balance: 'Saldo węzła: {balance} sats',
|
||||||
lnbits_balance: 'Saldo LNbits: {balance} sats',
|
lnbits_balance: 'Saldo LNbits: {balance} sats',
|
||||||
funding_reserve_percent: 'Rezerwa procentowa: {percent} %',
|
funding_reserve_percent: 'Rezerwa procentowa: {percent} %',
|
||||||
node_managment: 'Zarządzanie węzłami',
|
node_management: 'Zarządzanie węzłami',
|
||||||
node_management_not_supported:
|
node_management_not_supported:
|
||||||
'Zarządzanie węzłami nie jest obsługiwane przez aktywne źródło finansowania.',
|
'Zarządzanie węzłami nie jest obsługiwane przez aktywne źródło finansowania.',
|
||||||
toggle_node_ui: 'Interfejs użytkownika węzła',
|
toggle_node_ui: 'Interfejs użytkownika węzła',
|
||||||
|
|||||||
@@ -340,7 +340,7 @@ window.localisation.pt = {
|
|||||||
node_balance: 'Saldo do Nó: {balance} sats',
|
node_balance: 'Saldo do Nó: {balance} sats',
|
||||||
lnbits_balance: 'Saldo do LNbits: {balance} sats',
|
lnbits_balance: 'Saldo do LNbits: {balance} sats',
|
||||||
funding_reserve_percent: 'Reserve Percentagem: {percent} %',
|
funding_reserve_percent: 'Reserve Percentagem: {percent} %',
|
||||||
node_managment: 'Gerenciamento de Nós',
|
node_management: 'Gerenciamento de Nós',
|
||||||
node_management_not_supported:
|
node_management_not_supported:
|
||||||
'Gerenciamento de nós não suportado pela fonte de financiamento ativa',
|
'Gerenciamento de nós não suportado pela fonte de financiamento ativa',
|
||||||
toggle_node_ui: 'Interface do Usuário de Nó',
|
toggle_node_ui: 'Interface do Usuário de Nó',
|
||||||
|
|||||||
@@ -335,7 +335,7 @@ window.localisation.sk = {
|
|||||||
node_balance: 'Stav uzla: {balance} sats',
|
node_balance: 'Stav uzla: {balance} sats',
|
||||||
lnbits_balance: 'Zostatok LNbits: {balance} sats',
|
lnbits_balance: 'Zostatok LNbits: {balance} sats',
|
||||||
funding_reserve_percent: 'Rezervovať percento: {percent} %',
|
funding_reserve_percent: 'Rezervovať percento: {percent} %',
|
||||||
node_managment: 'Správa uzlov',
|
node_management: 'Správa uzlov',
|
||||||
node_management_not_supported:
|
node_management_not_supported:
|
||||||
'Správa uzlov nie je podporovaná aktívnym zdrojom financovania',
|
'Správa uzlov nie je podporovaná aktívnym zdrojom financovania',
|
||||||
toggle_node_ui: 'Používateľské rozhranie uzla',
|
toggle_node_ui: 'Používateľské rozhranie uzla',
|
||||||
|
|||||||
@@ -335,7 +335,7 @@ window.localisation.we = {
|
|||||||
node_balance: 'Cydbwysedd Nôd: {balance} sats',
|
node_balance: 'Cydbwysedd Nôd: {balance} sats',
|
||||||
lnbits_balance: 'Cydbwysedd LNbits: {balance} sats',
|
lnbits_balance: 'Cydbwysedd LNbits: {balance} sats',
|
||||||
funding_reserve_percent: 'Cadw Canran: {percent} %',
|
funding_reserve_percent: 'Cadw Canran: {percent} %',
|
||||||
node_managment: 'Rheoli Nodau',
|
node_management: 'Rheoli Nodau',
|
||||||
node_management_not_supported:
|
node_management_not_supported:
|
||||||
'Nid yw Rheoli Nodau yn cael ei gefnogi gan ffynhonnell ariannu weithredol',
|
'Nid yw Rheoli Nodau yn cael ei gefnogi gan ffynhonnell ariannu weithredol',
|
||||||
toggle_node_ui: 'Node UI',
|
toggle_node_ui: 'Node UI',
|
||||||
|
|||||||
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 7.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -91,35 +91,6 @@ window.AccountPageLogic = {
|
|||||||
window.i18n.global.locale = newValue
|
window.i18n.global.locale = newValue
|
||||||
this.$q.localStorage.set('lnbits.lang', newValue)
|
this.$q.localStorage.set('lnbits.lang', newValue)
|
||||||
},
|
},
|
||||||
toggleGradient() {
|
|
||||||
this.gradientSelection = !this.gradientChoice
|
|
||||||
this.gradientChoice = this.gradientSelection
|
|
||||||
this.$q.localStorage.set('lnbits.backgroundImage', 'none')
|
|
||||||
this.applyGradient()
|
|
||||||
if (!this.gradientChoice) {
|
|
||||||
window.location.reload()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
reactionChoiceFunc() {
|
|
||||||
this.$q.localStorage.set('lnbits.reactions', this.reactionSelection)
|
|
||||||
this.reactionChoice = this.reactionSelection
|
|
||||||
},
|
|
||||||
bgimageChoiceFunc() {
|
|
||||||
this.$q.localStorage.set('lnbits.backgroundImage', this.bgimageSelection)
|
|
||||||
this.bgimageChoice = this.bgimageSelection
|
|
||||||
this.applyBackgroundImage()
|
|
||||||
},
|
|
||||||
themeChoiceFunc(newValue) {
|
|
||||||
this.changeTheme(newValue)
|
|
||||||
this.setColors()
|
|
||||||
if (this.gradientChoice) {
|
|
||||||
this.applyGradient()
|
|
||||||
}
|
|
||||||
if (this.bgimageChoice) {
|
|
||||||
this.applyBackgroundImage()
|
|
||||||
}
|
|
||||||
this.applyBorder()
|
|
||||||
},
|
|
||||||
async updateAccount() {
|
async updateAccount() {
|
||||||
try {
|
try {
|
||||||
const {data} = await LNbits.api.request(
|
const {data} = await LNbits.api.request(
|
||||||
@@ -426,10 +397,6 @@ window.AccountPageLogic = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
this.borderSelection = this.borderChoice
|
|
||||||
this.reactionSelection = this.reactionChoice
|
|
||||||
this.bgimageSelection = this.bgimageChoice
|
|
||||||
this.themeSelection = this.themeChoice
|
|
||||||
try {
|
try {
|
||||||
const {data} = await LNbits.api.getAuthenticatedUser()
|
const {data} = await LNbits.api.getAuthenticatedUser()
|
||||||
this.user = data
|
this.user = data
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ window.AdminPageLogic = {
|
|||||||
chartReady: false,
|
chartReady: false,
|
||||||
formAddAdmin: '',
|
formAddAdmin: '',
|
||||||
formAddUser: '',
|
formAddUser: '',
|
||||||
|
formAddStripeUser: '',
|
||||||
hideInputToggle: true,
|
hideInputToggle: true,
|
||||||
formAddExtensionsManifest: '',
|
formAddExtensionsManifest: '',
|
||||||
nostrNotificationIdentifier: '',
|
nostrNotificationIdentifier: '',
|
||||||
@@ -187,6 +188,23 @@ window.AdminPageLogic = {
|
|||||||
let allowed_users = this.formData.lnbits_allowed_users
|
let allowed_users = this.formData.lnbits_allowed_users
|
||||||
this.formData.lnbits_allowed_users = allowed_users.filter(u => u !== user)
|
this.formData.lnbits_allowed_users = allowed_users.filter(u => u !== user)
|
||||||
},
|
},
|
||||||
|
addStripeAllowedUser() {
|
||||||
|
const addUser = this.formAddStripeUser || ''
|
||||||
|
if (
|
||||||
|
addUser.length &&
|
||||||
|
!this.formData.stripe_limits.allowed_users.includes(addUser)
|
||||||
|
) {
|
||||||
|
this.formData.stripe_limits.allowed_users = [
|
||||||
|
...this.formData.stripe_limits.allowed_users,
|
||||||
|
addUser
|
||||||
|
]
|
||||||
|
this.formAddStripeUser = ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeStripeAllowedUser(user) {
|
||||||
|
this.formData.stripe_limits.allowed_users =
|
||||||
|
this.formData.stripe_limits.allowed_users.filter(u => u !== user)
|
||||||
|
},
|
||||||
addIncludePath() {
|
addIncludePath() {
|
||||||
if (!this.formAddIncludePath) {
|
if (!this.formAddIncludePath) {
|
||||||
return
|
return
|
||||||
@@ -613,6 +631,20 @@ window.AdminPageLogic = {
|
|||||||
.catch(LNbits.utils.notifyApiError)
|
.catch(LNbits.utils.notifyApiError)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
checkFiatProvider(providerName) {
|
||||||
|
LNbits.api
|
||||||
|
.request('PUT', `/api/v1/fiat/check/${providerName}`)
|
||||||
|
.then(response => {
|
||||||
|
response
|
||||||
|
const data = response.data
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: data.success ? 'positive' : 'warning',
|
||||||
|
message: data.message,
|
||||||
|
icon: null
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(LNbits.utils.notifyApiError)
|
||||||
|
},
|
||||||
downloadBackup() {
|
downloadBackup() {
|
||||||
window.open('/admin/api/v1/backup', '_blank')
|
window.open('/admin/api/v1/backup', '_blank')
|
||||||
},
|
},
|
||||||
|
|||||||
+42
-147
@@ -19,14 +19,16 @@ window.LNbits = {
|
|||||||
amount,
|
amount,
|
||||||
memo,
|
memo,
|
||||||
unit = 'sat',
|
unit = 'sat',
|
||||||
lnurlCallback = null
|
lnurlCallback = null,
|
||||||
|
fiatProvider = null
|
||||||
) {
|
) {
|
||||||
return this.request('post', '/api/v1/payments', wallet.inkey, {
|
return this.request('post', '/api/v1/payments', wallet.inkey, {
|
||||||
out: false,
|
out: false,
|
||||||
amount: amount,
|
amount: amount,
|
||||||
memo: memo,
|
memo: memo,
|
||||||
lnurl_callback: lnurlCallback,
|
lnurl_callback: lnurlCallback,
|
||||||
unit: unit
|
unit: unit,
|
||||||
|
fiat_provider: fiatProvider
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
payInvoice(wallet, bolt11) {
|
payInvoice(wallet, bolt11) {
|
||||||
@@ -197,6 +199,7 @@ window.LNbits = {
|
|||||||
email: data.email,
|
email: data.email,
|
||||||
extensions: data.extensions,
|
extensions: data.extensions,
|
||||||
wallets: data.wallets,
|
wallets: data.wallets,
|
||||||
|
fiat_providers: data.fiat_providers || [],
|
||||||
super_user: data.super_user,
|
super_user: data.super_user,
|
||||||
extra: data.extra ?? {}
|
extra: data.extra ?? {}
|
||||||
}
|
}
|
||||||
@@ -434,18 +437,6 @@ window.LNbits = {
|
|||||||
converter.setFlavor('github')
|
converter.setFlavor('github')
|
||||||
converter.setOption('simpleLineBreaks', true)
|
converter.setOption('simpleLineBreaks', true)
|
||||||
return converter.makeHtml(text)
|
return converter.makeHtml(text)
|
||||||
},
|
|
||||||
hexToRgb(hex) {
|
|
||||||
return Quasar.colors.hexToRgb(hex)
|
|
||||||
},
|
|
||||||
hexDarken(hex, percent) {
|
|
||||||
return Quasar.colors.lighten(hex, percent)
|
|
||||||
},
|
|
||||||
hexAlpha(hex, alpha) {
|
|
||||||
return Quasar.colors.changeAlpha(hex, alpha)
|
|
||||||
},
|
|
||||||
getPaletteColor(color) {
|
|
||||||
return Quasar.colors.getPaletteColor(color)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -462,7 +453,6 @@ if (!window.g) {
|
|||||||
fiatTracking: false,
|
fiatTracking: false,
|
||||||
wallets: [],
|
wallets: [],
|
||||||
payments: [],
|
payments: [],
|
||||||
allowedThemes: null,
|
|
||||||
langs: [],
|
langs: [],
|
||||||
walletEventListeners: [],
|
walletEventListeners: [],
|
||||||
updatePayments: false,
|
updatePayments: false,
|
||||||
@@ -479,12 +469,13 @@ window.windowMixin = {
|
|||||||
mobileSimple: true,
|
mobileSimple: true,
|
||||||
walletFlip: true,
|
walletFlip: true,
|
||||||
showAddWalletDialog: {show: false},
|
showAddWalletDialog: {show: false},
|
||||||
borderSelection: null,
|
isUserAuthorized: false,
|
||||||
gradientSelection: null,
|
isSatsDenomination: WINDOW_SETTINGS['LNBITS_DENOMINATION'] == 'sats',
|
||||||
themeSelection: null,
|
allowedThemes: WINDOW_SETTINGS['LNBITS_THEME_OPTIONS'],
|
||||||
reactionSelection: null,
|
walletEventListeners: [],
|
||||||
bgimageSelection: null,
|
darkChoice: this.$q.localStorage.has('lnbits.darkMode')
|
||||||
gradientSelection: false,
|
? this.$q.localStorage.getItem('lnbits.darkMode')
|
||||||
|
: true,
|
||||||
borderChoice: this.$q.localStorage.has('lnbits.border')
|
borderChoice: this.$q.localStorage.has('lnbits.border')
|
||||||
? this.$q.localStorage.getItem('lnbits.border')
|
? this.$q.localStorage.getItem('lnbits.border')
|
||||||
: USE_DEFAULT_BORDER,
|
: USE_DEFAULT_BORDER,
|
||||||
@@ -500,10 +491,6 @@ window.windowMixin = {
|
|||||||
bgimageChoice: this.$q.localStorage.has('lnbits.backgroundImage')
|
bgimageChoice: this.$q.localStorage.has('lnbits.backgroundImage')
|
||||||
? this.$q.localStorage.getItem('lnbits.backgroundImage')
|
? this.$q.localStorage.getItem('lnbits.backgroundImage')
|
||||||
: USE_DEFAULT_BGIMAGE,
|
: USE_DEFAULT_BGIMAGE,
|
||||||
isUserAuthorized: false,
|
|
||||||
isSatsDenomination: WINDOW_SETTINGS['LNBITS_DENOMINATION'] == 'sats',
|
|
||||||
walletEventListeners: [],
|
|
||||||
backgroundImage: '',
|
|
||||||
...WINDOW_SETTINGS
|
...WINDOW_SETTINGS
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -609,138 +596,52 @@ window.windowMixin = {
|
|||||||
},
|
},
|
||||||
changeTheme(newValue) {
|
changeTheme(newValue) {
|
||||||
document.body.setAttribute('data-theme', newValue)
|
document.body.setAttribute('data-theme', newValue)
|
||||||
if (this.themeSelection) {
|
this.$q.localStorage.set('lnbits.theme', newValue)
|
||||||
this.themeChoice = newValue
|
this.themeChoice = newValue
|
||||||
this.$q.localStorage.set('lnbits.theme', newValue)
|
|
||||||
}
|
|
||||||
this.setColors()
|
|
||||||
},
|
},
|
||||||
applyGradient() {
|
applyGradient() {
|
||||||
darkBgColor = this.$q.localStorage.getItem('lnbits.darkBgColor')
|
|
||||||
primaryColor = this.$q.localStorage.getItem('lnbits.primaryColor')
|
|
||||||
if (this.gradientChoice) {
|
if (this.gradientChoice) {
|
||||||
|
document.body.classList.add('gradient-bg')
|
||||||
this.$q.localStorage.set('lnbits.gradientBg', true)
|
this.$q.localStorage.set('lnbits.gradientBg', true)
|
||||||
|
// Ensure dark mode is enabled when gradient background is applied
|
||||||
if (!this.$q.dark.isActive) {
|
if (!this.$q.dark.isActive) {
|
||||||
this.$q.dark.toggle()
|
this.toggleDarkMode()
|
||||||
this.$q.localStorage.set('lnbits.darkMode', true)
|
|
||||||
}
|
}
|
||||||
const gradientStyle = `linear-gradient(to bottom right, ${LNbits.utils.hexDarken(String(primaryColor), -70)}, #0a0a0a)`
|
|
||||||
document.body.style.setProperty(
|
|
||||||
'background-image',
|
|
||||||
gradientStyle,
|
|
||||||
'important'
|
|
||||||
)
|
|
||||||
const gradientStyleCards = `background-color: ${LNbits.utils.hexAlpha(String(darkBgColor), 0.4)} !important; backdrop-filter: blur(6px);`
|
|
||||||
const style = document.createElement('style')
|
|
||||||
style.innerHTML =
|
|
||||||
`body[data-theme="${this.themeChoice}"] .q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark), body.body${this.$q.dark.isActive ? '--dark' : ''} .q-header, body.body${this.$q.dark.isActive ? '--dark' : ''} .q-drawer { ${gradientStyleCards} }` +
|
|
||||||
`body[data-theme="${this.themeChoice}"].body--dark{background: ${LNbits.utils.hexDarken(String(primaryColor), -88)} !important; }` +
|
|
||||||
`[data-theme="${this.themeChoice}"] .q-card--dark{background: ${String(darkBgColor)} !important;} }`
|
|
||||||
document.head.appendChild(style)
|
|
||||||
} else {
|
} else {
|
||||||
|
document.body.classList.remove('gradient-bg')
|
||||||
this.$q.localStorage.set('lnbits.gradientBg', false)
|
this.$q.localStorage.set('lnbits.gradientBg', false)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
toggleDarkMode() {
|
|
||||||
this.$q.dark.toggle()
|
|
||||||
this.$q.localStorage.set('lnbits.darkMode', this.$q.dark.isActive)
|
|
||||||
if (!this.$q.dark.isActive) {
|
|
||||||
this.bgimageSelection = 'null'
|
|
||||||
this.$q.localStorage.set('lnbits.gradientBg', false)
|
|
||||||
this.$q.localStorage.set('lnbits.backgroundImage', 'null')
|
|
||||||
window.location.hash = '#theme'
|
|
||||||
window.location.reload()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
applyBackgroundImage() {
|
applyBackgroundImage() {
|
||||||
if (this.bgimageSelection) {
|
if (this.bgimageChoice == '') {
|
||||||
this.$q.localStorage.set(
|
document.body.classList.remove('bg-image')
|
||||||
'lnbits.backgroundImage',
|
} else {
|
||||||
this.bgimageSelection
|
document.body.classList.add('bg-image')
|
||||||
|
document.body.style.setProperty(
|
||||||
|
'--background',
|
||||||
|
`url(${this.bgimageChoice})`
|
||||||
)
|
)
|
||||||
this.bgimageChoice = this.bgimageSelection
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
this.bgimageChoice &&
|
|
||||||
this.bgimageChoice !== 'null' &&
|
|
||||||
this.bgimageChoice !== 'none' &&
|
|
||||||
this.bgimageChoice !== ''
|
|
||||||
) {
|
|
||||||
this.gradientChoice = true
|
|
||||||
this.applyGradient()
|
|
||||||
const style = document.createElement('style')
|
|
||||||
style.innerHTML = `
|
|
||||||
body[data-theme="${this.themeChoice}"]::before {
|
|
||||||
content: '';
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: url(${this.bgimageChoice});
|
|
||||||
background-size: cover;
|
|
||||||
filter: blur(8px);
|
|
||||||
z-index: -1;
|
|
||||||
background-position: center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: cover;
|
|
||||||
}
|
|
||||||
body[data-theme="${this.themeChoice}"] .q-page-container {
|
|
||||||
backdrop-filter: none; /* Ensure the page content is not affected */
|
|
||||||
}`
|
|
||||||
document.head.appendChild(style)
|
|
||||||
}
|
}
|
||||||
|
this.$q.localStorage.set('lnbits.backgroundImage', this.bgimageChoice)
|
||||||
},
|
},
|
||||||
applyBorder() {
|
applyBorder() {
|
||||||
if (this.borderSelection) {
|
// Remove any existing border classes
|
||||||
this.$q.localStorage.setItem('lnbits.border', this.borderSelection)
|
document.body.classList.forEach(cls => {
|
||||||
this.borderChoice = this.$q.localStorage.getItem('lnbits.border')
|
if (cls.endsWith('-border')) {
|
||||||
}
|
document.body.classList.remove(cls)
|
||||||
let borderStyleCSS
|
|
||||||
if (this.borderChoice == 'hard-border') {
|
|
||||||
borderStyleCSS = `box-shadow: 0 0 0 1px rgba(0,0,0,.12), 0 0 0 1px #ffffff47; border: none;`
|
|
||||||
}
|
|
||||||
if (this.borderChoice == 'neon-border') {
|
|
||||||
borderStyleCSS = `border: 2px solid ${this.$q.localStorage.getItem('lnbits.primaryColor')}; box-shadow: none;`
|
|
||||||
}
|
|
||||||
if (this.borderChoice == 'no-border') {
|
|
||||||
borderStyleCSS = `box-shadow: none; border: none;`
|
|
||||||
}
|
|
||||||
if (this.borderChoice == 'retro-border') {
|
|
||||||
borderStyleCSS = `border: none; border-color: rgba(255, 255, 255, 0.28); box-shadow: 0 1px 5px rgba(255, 255, 255, 0.2), 0 2px 2px rgba(255, 255, 255, 0.14), 0 3px 1px -2px rgba(255, 255, 255, 0.12);`
|
|
||||||
}
|
|
||||||
let style = document.createElement('style')
|
|
||||||
style.innerHTML = `
|
|
||||||
body[data-theme="${this.themeChoice}"] .q-card,
|
|
||||||
body[data-theme="${this.themeChoice}"] .q-card.q-card--dark,
|
|
||||||
body[data-theme="${this.themeChoice}"] .q-date,
|
|
||||||
body[data-theme="${this.themeChoice}"] .q-date--dark {
|
|
||||||
${borderStyleCSS}
|
|
||||||
}
|
}
|
||||||
`
|
})
|
||||||
document.head.appendChild(style)
|
this.$q.localStorage.setItem('lnbits.border', this.borderChoice)
|
||||||
|
document.body.classList.add(this.borderChoice)
|
||||||
},
|
},
|
||||||
setColors() {
|
toggleDarkMode() {
|
||||||
this.$q.localStorage.set(
|
this.$q.dark.toggle()
|
||||||
'lnbits.primaryColor',
|
this.darkChoice = this.$q.dark.isActive
|
||||||
LNbits.utils.getPaletteColor('primary')
|
this.$q.localStorage.set('lnbits.darkMode', this.$q.dark.isActive)
|
||||||
)
|
if (!this.$q.dark.isActive) {
|
||||||
this.$q.localStorage.set(
|
this.gradientChoice = false
|
||||||
'lnbits.secondaryColor',
|
this.applyGradient()
|
||||||
LNbits.utils.getPaletteColor('secondary')
|
}
|
||||||
)
|
|
||||||
this.$q.localStorage.set(
|
|
||||||
'lnbits.darkBgColor',
|
|
||||||
LNbits.utils.getPaletteColor('dark')
|
|
||||||
)
|
|
||||||
document.documentElement.style.setProperty(
|
|
||||||
'--q-primary',
|
|
||||||
LNbits.utils.getPaletteColor('primary')
|
|
||||||
)
|
|
||||||
document.documentElement.style.setProperty(
|
|
||||||
'--q-secondary',
|
|
||||||
LNbits.utils.getPaletteColor('secondary')
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
copyText(text, message, position) {
|
copyText(text, message, position) {
|
||||||
Quasar.copyToClipboard(text).then(() => {
|
Quasar.copyToClipboard(text).then(() => {
|
||||||
@@ -805,10 +706,7 @@ window.windowMixin = {
|
|||||||
const gradient = params.get('gradient')
|
const gradient = params.get('gradient')
|
||||||
const border = params.get('border')
|
const border = params.get('border')
|
||||||
|
|
||||||
if (
|
if (theme && this.allowedThemes.includes(theme.trim().toLowerCase())) {
|
||||||
theme &&
|
|
||||||
this.g.allowedThemes.includes(theme.trim().toLowerCase())
|
|
||||||
) {
|
|
||||||
const normalizedTheme = theme.trim().toLowerCase()
|
const normalizedTheme = theme.trim().toLowerCase()
|
||||||
document.body.setAttribute('data-theme', normalizedTheme)
|
document.body.setAttribute('data-theme', normalizedTheme)
|
||||||
this.$q.localStorage.set('lnbits.theme', normalizedTheme)
|
this.$q.localStorage.set('lnbits.theme', normalizedTheme)
|
||||||
@@ -838,8 +736,6 @@ window.windowMixin = {
|
|||||||
|
|
||||||
window.history.replaceState(null, null, url.pathname)
|
window.history.replaceState(null, null, url.pathname)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setColors()
|
|
||||||
},
|
},
|
||||||
refreshRoute() {
|
refreshRoute() {
|
||||||
const path = window.location.pathname
|
const path = window.location.pathname
|
||||||
@@ -851,7 +747,6 @@ window.windowMixin = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
this.g.allowedThemes = window.allowedThemes ?? ['bitcoin']
|
|
||||||
this.$q.dark.set(
|
this.$q.dark.set(
|
||||||
this.$q.localStorage.has('lnbits.darkMode')
|
this.$q.localStorage.has('lnbits.darkMode')
|
||||||
? this.$q.localStorage.getItem('lnbits.darkMode')
|
? this.$q.localStorage.getItem('lnbits.darkMode')
|
||||||
|
|||||||
+22
-17
@@ -35,6 +35,7 @@ window.WalletPageLogic = {
|
|||||||
lnurl: null,
|
lnurl: null,
|
||||||
units: ['sat'],
|
units: ['sat'],
|
||||||
unit: 'sat',
|
unit: 'sat',
|
||||||
|
fiatProvider: '',
|
||||||
data: {
|
data: {
|
||||||
amount: null,
|
amount: null,
|
||||||
memo: ''
|
memo: ''
|
||||||
@@ -114,8 +115,6 @@ window.WalletPageLogic = {
|
|||||||
isFiatPriority: false,
|
isFiatPriority: false,
|
||||||
formattedFiatAmount: 0,
|
formattedFiatAmount: 0,
|
||||||
formattedExchange: null,
|
formattedExchange: null,
|
||||||
primaryColor: this.$q.localStorage.getItem('lnbits.primaryColor'),
|
|
||||||
secondaryColor: this.$q.localStorage.getItem('lnbits.secondaryColor'),
|
|
||||||
chartData: [],
|
chartData: [],
|
||||||
chartDataPointCount: 0,
|
chartDataPointCount: 0,
|
||||||
chartConfig: {
|
chartConfig: {
|
||||||
@@ -253,12 +252,15 @@ window.WalletPageLogic = {
|
|||||||
this.receive.data.amount,
|
this.receive.data.amount,
|
||||||
this.receive.data.memo,
|
this.receive.data.memo,
|
||||||
this.receive.unit,
|
this.receive.unit,
|
||||||
this.receive.lnurl && this.receive.lnurl.callback
|
this.receive.lnurl && this.receive.lnurl.callback,
|
||||||
|
this.receive.fiatProvider
|
||||||
)
|
)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
this.g.updatePayments = !this.g.updatePayments
|
this.g.updatePayments = !this.g.updatePayments
|
||||||
this.receive.status = 'success'
|
this.receive.status = 'success'
|
||||||
this.receive.paymentReq = response.data.bolt11
|
this.receive.paymentReq = response.data.bolt11
|
||||||
|
this.receive.fiatPaymentReq =
|
||||||
|
response.data.extra?.fiat_payment_request
|
||||||
this.receive.amountMsat = response.data.amount
|
this.receive.amountMsat = response.data.amount
|
||||||
this.receive.paymentHash = response.data.payment_hash
|
this.receive.paymentHash = response.data.payment_hash
|
||||||
if (!this.receive.lnurl) {
|
if (!this.receive.lnurl) {
|
||||||
@@ -820,6 +822,9 @@ window.WalletPageLogic = {
|
|||||||
},
|
},
|
||||||
swapBalancePriority() {
|
swapBalancePriority() {
|
||||||
this.isFiatPriority = !this.isFiatPriority
|
this.isFiatPriority = !this.isFiatPriority
|
||||||
|
this.receive.unit = this.isFiatPriority
|
||||||
|
? this.g.wallet.currency || 'sat'
|
||||||
|
: 'sat'
|
||||||
this.$q.localStorage.setItem('lnbits.isFiatPriority', this.isFiatPriority)
|
this.$q.localStorage.setItem('lnbits.isFiatPriority', this.isFiatPriority)
|
||||||
},
|
},
|
||||||
handleFiatTracking() {
|
handleFiatTracking() {
|
||||||
@@ -963,11 +968,11 @@ window.WalletPageLogic = {
|
|||||||
label: 'Balance',
|
label: 'Balance',
|
||||||
data: data.map(s => s.balance),
|
data: data.map(s => s.balance),
|
||||||
pointStyle: false,
|
pointStyle: false,
|
||||||
backgroundColor: LNbits.utils.hexAlpha(
|
backgroundColor: Quasar.colors.changeAlpha(
|
||||||
this.primaryColor,
|
Quasar.colors.getPaletteColor('primary'),
|
||||||
0.3
|
0.3
|
||||||
),
|
),
|
||||||
borderColor: this.primaryColor,
|
borderColor: Quasar.colors.getPaletteColor('primary'),
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
fill: true,
|
fill: true,
|
||||||
tension: 0.7,
|
tension: 0.7,
|
||||||
@@ -977,11 +982,11 @@ window.WalletPageLogic = {
|
|||||||
label: 'Fees',
|
label: 'Fees',
|
||||||
data: data.map(s => s.fee),
|
data: data.map(s => s.fee),
|
||||||
pointStyle: false,
|
pointStyle: false,
|
||||||
backgroundColor: LNbits.utils.hexAlpha(
|
backgroundColor: Quasar.colors.changeAlpha(
|
||||||
this.secondaryColor,
|
Quasar.colors.getPaletteColor('secondary'),
|
||||||
0.3
|
0.3
|
||||||
),
|
),
|
||||||
borderColor: this.secondaryColor,
|
borderColor: Quasar.colors.getPaletteColor('secondary'),
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
fill: true,
|
fill: true,
|
||||||
tension: 0.7,
|
tension: 0.7,
|
||||||
@@ -1022,8 +1027,8 @@ window.WalletPageLogic = {
|
|||||||
label: 'Balance In',
|
label: 'Balance In',
|
||||||
borderRadius: 5,
|
borderRadius: 5,
|
||||||
data: data.map(s => s.balance_in),
|
data: data.map(s => s.balance_in),
|
||||||
backgroundColor: LNbits.utils.hexAlpha(
|
backgroundColor: Quasar.colors.changeAlpha(
|
||||||
this.primaryColor,
|
Quasar.colors.getPaletteColor('primary'),
|
||||||
0.3
|
0.3
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -1031,8 +1036,8 @@ window.WalletPageLogic = {
|
|||||||
label: 'Balance Out',
|
label: 'Balance Out',
|
||||||
borderRadius: 5,
|
borderRadius: 5,
|
||||||
data: data.map(s => s.balance_out),
|
data: data.map(s => s.balance_out),
|
||||||
backgroundColor: LNbits.utils.hexAlpha(
|
backgroundColor: Quasar.colors.changeAlpha(
|
||||||
this.secondaryColor,
|
Quasar.colors.getPaletteColor('secondary'),
|
||||||
0.3
|
0.3
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1071,16 +1076,16 @@ window.WalletPageLogic = {
|
|||||||
{
|
{
|
||||||
label: 'Payments In',
|
label: 'Payments In',
|
||||||
data: data.map(s => s.count_in),
|
data: data.map(s => s.count_in),
|
||||||
backgroundColor: LNbits.utils.hexAlpha(
|
backgroundColor: Quasar.colors.changeAlpha(
|
||||||
this.primaryColor,
|
Quasar.colors.getPaletteColor('primary'),
|
||||||
0.3
|
0.3
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Payments Out',
|
label: 'Payments Out',
|
||||||
data: data.map(s => -s.count_out),
|
data: data.map(s => -s.count_out),
|
||||||
backgroundColor: LNbits.utils.hexAlpha(
|
backgroundColor: Quasar.colors.changeAlpha(
|
||||||
this.secondaryColor,
|
Quasar.colors.getPaletteColor('secondary'),
|
||||||
0.3
|
0.3
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
@use 'sass:color';
|
||||||
|
|
||||||
|
@each $theme, $colors in $themes {
|
||||||
|
body[data-theme='#{$theme}'] {
|
||||||
|
@each $name, $color in $colors {
|
||||||
|
@if $name== 'dark' {
|
||||||
|
--q-dark-page: #{$color};
|
||||||
|
}
|
||||||
|
@if $name== 'primary' {
|
||||||
|
--q-primary: #{$color};
|
||||||
|
}
|
||||||
|
@if $name== 'secondary' {
|
||||||
|
--q-secondary: #{$color};
|
||||||
|
}
|
||||||
|
@if $name== 'info' {
|
||||||
|
[data-theme='#{$theme}'] .q-card--dark,
|
||||||
|
[data-theme='#{$theme}'] .q-stepper--dark {
|
||||||
|
background: $color !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body.gradient-bg {
|
||||||
|
background-image: linear-gradient(
|
||||||
|
to bottom right,
|
||||||
|
var(--q-dark-page),
|
||||||
|
#0a0a0a
|
||||||
|
);
|
||||||
|
background-attachment: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.bg-image {
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
z-index: -1;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
filter: blur(8px);
|
||||||
|
background-image: var(--background);
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
.q-page-container {
|
||||||
|
backdrop-filter: none; /* Ensure the page content is not affected */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// transparent background for specific elements
|
||||||
|
body.body--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: blur(6px);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,130 +1,6 @@
|
|||||||
$themes: (
|
@import 'themes';
|
||||||
'classic': (
|
@import 'borders';
|
||||||
primary: #673ab7,
|
@import 'background';
|
||||||
secondary: #9c27b0,
|
|
||||||
dark: #1f2234,
|
|
||||||
info: #333646,
|
|
||||||
marginal-bg: #1f2234,
|
|
||||||
marginal-text: #fff
|
|
||||||
),
|
|
||||||
'bitcoin': (
|
|
||||||
primary: #ea611d,
|
|
||||||
secondary: #e56f35,
|
|
||||||
dark: #2d293b,
|
|
||||||
info: #333646,
|
|
||||||
marginal-bg: #000000,
|
|
||||||
marginal-text: #fff
|
|
||||||
),
|
|
||||||
'freedom': (
|
|
||||||
primary: #e22156,
|
|
||||||
secondary: #b91a45,
|
|
||||||
dark: #0a0a0a,
|
|
||||||
info: #1b1b1b,
|
|
||||||
marginal-bg: #2d293b,
|
|
||||||
marginal-text: #fff
|
|
||||||
),
|
|
||||||
'cyber': (
|
|
||||||
primary: #7cb342,
|
|
||||||
secondary: #558b2f,
|
|
||||||
dark: #0a0a0a,
|
|
||||||
info: #1b1b1b,
|
|
||||||
marginal-bg: #2d293b,
|
|
||||||
marginal-text: #fff
|
|
||||||
),
|
|
||||||
'mint': (
|
|
||||||
primary: #3ab77d,
|
|
||||||
secondary: #27b065,
|
|
||||||
dark: #1f342b,
|
|
||||||
info: #334642,
|
|
||||||
marginal-bg: #1f342b,
|
|
||||||
marginal-text: #fff
|
|
||||||
),
|
|
||||||
'autumn': (
|
|
||||||
primary: #b7763a,
|
|
||||||
secondary: #b07927,
|
|
||||||
dark: #34291f,
|
|
||||||
info: #463f33,
|
|
||||||
marginal-bg: #342a1f,
|
|
||||||
marginal-text: rgb(255, 255, 255)
|
|
||||||
),
|
|
||||||
'flamingo': (
|
|
||||||
primary: #ff00ff,
|
|
||||||
secondary: #fda3fd,
|
|
||||||
dark: #2f032f,
|
|
||||||
info: #bc23bc,
|
|
||||||
marginal-bg: #311231,
|
|
||||||
marginal-text: rgb(255, 255, 255)
|
|
||||||
),
|
|
||||||
'monochrome': (
|
|
||||||
primary: #494949,
|
|
||||||
secondary: #6b6b6b,
|
|
||||||
dark: #000,
|
|
||||||
info: rgb(39, 39, 39),
|
|
||||||
marginal-bg: #000,
|
|
||||||
marginal-text: rgb(255, 255, 255)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
@each $theme, $colors in $themes {
|
|
||||||
@each $name, $color in $colors {
|
|
||||||
@if $name== 'dark' {
|
|
||||||
[data-theme='#{$theme}'] .q-drawer--dark,
|
|
||||||
body[data-theme='#{$theme}'].body--dark,
|
|
||||||
[data-theme='#{$theme}'] .q-menu--dark {
|
|
||||||
background: $color !important;
|
|
||||||
}
|
|
||||||
/* IF WANTING TO SET A DARKER BG COLOR IN THE FUTURE
|
|
||||||
// set a darker body bg for all themes, when in "dark mode"
|
|
||||||
body[data-theme='#{$theme}'].body--dark {
|
|
||||||
background: scale-color($color, $lightness: -60%);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
} @else {
|
|
||||||
body[data-theme='#{$theme}'].body--light {
|
|
||||||
background-color: #f4f4f4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@if $name== 'info' {
|
|
||||||
[data-theme='#{$theme}'] .q-card--dark,
|
|
||||||
[data-theme='#{$theme}'] .q-stepper--dark {
|
|
||||||
background: $color !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
[data-theme='#{$theme}'] {
|
|
||||||
@each $name, $color in $colors {
|
|
||||||
.bg-#{$name} {
|
|
||||||
background: $color !important;
|
|
||||||
}
|
|
||||||
.text-#{$name} {
|
|
||||||
color: $color !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme='freedom'] .q-drawer--dark {
|
|
||||||
background: #0a0a0a !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme='freedom'] .q-header {
|
|
||||||
background: #0a0a0a !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme='cyber'] .q-drawer--dark {
|
|
||||||
background: #0a0a0a !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme='cyber'] .q-header {
|
|
||||||
background: #0a0a0a !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme='salvador'] .q-drawer--dark {
|
|
||||||
background: #242424 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme='salvador'] .q-header {
|
|
||||||
background: #0f47af !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
[v-cloak] {
|
[v-cloak] {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
@each $theme, $colors in $themes {
|
||||||
|
@each $name, $color in $colors {
|
||||||
|
@if $name== 'primary' {
|
||||||
|
body[data-theme='#{$theme}'].neon-border {
|
||||||
|
.q-card,
|
||||||
|
.q-card.q-card--dark,
|
||||||
|
.q-date,
|
||||||
|
.q-date--dark {
|
||||||
|
border: 2px solid $color;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body.hard-border {
|
||||||
|
.q-card,
|
||||||
|
.q-card.q-card--dark,
|
||||||
|
.q-date,
|
||||||
|
.q-date--dark {
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px rgba(0, 0, 0, 0.12),
|
||||||
|
0 0 0 1px #ffffff47;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body.retro-border {
|
||||||
|
.q-card,
|
||||||
|
.q-card.q-card--dark,
|
||||||
|
.q-date,
|
||||||
|
.q-date--dark {
|
||||||
|
border: none;
|
||||||
|
box-shadow:
|
||||||
|
0 1px 5px rgba(255, 255, 255, 0.2),
|
||||||
|
0 2px 2px rgba(255, 255, 255, 0.14),
|
||||||
|
0 3px 1px -2px rgba(255, 255, 255, 0.12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body.no-border {
|
||||||
|
.q-card,
|
||||||
|
.q-card.q-card--dark,
|
||||||
|
.q-date,
|
||||||
|
.q-date--dark {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
$themes: (
|
||||||
|
'classic': (
|
||||||
|
primary: #673ab7,
|
||||||
|
secondary: #9c27b0,
|
||||||
|
dark: #1f2234,
|
||||||
|
info: #333646,
|
||||||
|
marginal-bg: #1f2234,
|
||||||
|
marginal-text: #fff
|
||||||
|
),
|
||||||
|
'bitcoin': (
|
||||||
|
primary: #ea611d,
|
||||||
|
secondary: #e56f35,
|
||||||
|
dark: #2d293b,
|
||||||
|
info: #333646,
|
||||||
|
marginal-bg: #000000,
|
||||||
|
marginal-text: #fff
|
||||||
|
),
|
||||||
|
'freedom': (
|
||||||
|
primary: #e22156,
|
||||||
|
secondary: #b91a45,
|
||||||
|
dark: #462f36,
|
||||||
|
info: #47393d,
|
||||||
|
marginal-bg: #2d293b,
|
||||||
|
marginal-text: #fff
|
||||||
|
),
|
||||||
|
'cyber': (
|
||||||
|
primary: #7cb342,
|
||||||
|
secondary: #558b2f,
|
||||||
|
dark: #000,
|
||||||
|
info: #1f2915,
|
||||||
|
marginal-bg: #2d293b,
|
||||||
|
marginal-text: #fff
|
||||||
|
),
|
||||||
|
'mint': (
|
||||||
|
primary: #3ab77d,
|
||||||
|
secondary: #27b065,
|
||||||
|
dark: #1f342b,
|
||||||
|
info: #334642,
|
||||||
|
marginal-bg: #1f342b,
|
||||||
|
marginal-text: #fff
|
||||||
|
),
|
||||||
|
'autumn': (
|
||||||
|
primary: #b7763a,
|
||||||
|
secondary: #b07927,
|
||||||
|
dark: #34291f,
|
||||||
|
info: #463f33,
|
||||||
|
marginal-bg: #342a1f,
|
||||||
|
marginal-text: rgb(255, 255, 255)
|
||||||
|
),
|
||||||
|
'flamingo': (
|
||||||
|
primary: #ff00ff,
|
||||||
|
secondary: #fda3fd,
|
||||||
|
dark: #2f032f,
|
||||||
|
info: #bc23bc,
|
||||||
|
marginal-bg: #311231,
|
||||||
|
marginal-text: rgb(255, 255, 255)
|
||||||
|
),
|
||||||
|
'monochrome': (
|
||||||
|
primary: #494949,
|
||||||
|
secondary: #6b6b6b,
|
||||||
|
dark: #000,
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
);
|
||||||
+32
-18
@@ -2,11 +2,9 @@ import asyncio
|
|||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import uuid
|
import uuid
|
||||||
|
from collections.abc import Coroutine
|
||||||
from typing import (
|
from typing import (
|
||||||
Callable,
|
Callable,
|
||||||
Coroutine,
|
|
||||||
Dict,
|
|
||||||
List,
|
|
||||||
Optional,
|
Optional,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,11 +16,12 @@ from lnbits.core.crud import (
|
|||||||
update_payment,
|
update_payment,
|
||||||
)
|
)
|
||||||
from lnbits.core.models import Payment, PaymentState
|
from lnbits.core.models import Payment, PaymentState
|
||||||
|
from lnbits.core.services.fiat_providers import handle_fiat_payment_confirmation
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.wallets import get_funding_source
|
from lnbits.wallets import get_funding_source
|
||||||
|
|
||||||
tasks: List[asyncio.Task] = []
|
tasks: list[asyncio.Task] = []
|
||||||
unique_tasks: Dict[str, asyncio.Task] = {}
|
unique_tasks: dict[str, asyncio.Task] = {}
|
||||||
|
|
||||||
|
|
||||||
def create_task(coro: Coroutine) -> asyncio.Task:
|
def create_task(coro: Coroutine) -> asyncio.Task:
|
||||||
@@ -82,7 +81,7 @@ async def catch_everything_and_restart(
|
|||||||
return await catch_everything_and_restart(func, name)
|
return await catch_everything_and_restart(func, name)
|
||||||
|
|
||||||
|
|
||||||
invoice_listeners: Dict[str, asyncio.Queue] = {}
|
invoice_listeners: dict[str, asyncio.Queue] = {}
|
||||||
|
|
||||||
|
|
||||||
# TODO: name should not be optional
|
# TODO: name should not be optional
|
||||||
@@ -106,6 +105,13 @@ def register_invoice_listener(send_chan: asyncio.Queue, name: Optional[str] = No
|
|||||||
internal_invoice_queue: asyncio.Queue = asyncio.Queue(0)
|
internal_invoice_queue: asyncio.Queue = asyncio.Queue(0)
|
||||||
|
|
||||||
|
|
||||||
|
async def internal_invoice_queue_put(checking_id: str) -> None:
|
||||||
|
"""
|
||||||
|
A method to call when it wants to notify about an internal invoice payment.
|
||||||
|
"""
|
||||||
|
await internal_invoice_queue.put(checking_id)
|
||||||
|
|
||||||
|
|
||||||
async def internal_invoice_listener() -> None:
|
async def internal_invoice_listener() -> None:
|
||||||
"""
|
"""
|
||||||
internal_invoice_queue will be filled directly in core/services.py
|
internal_invoice_queue will be filled directly in core/services.py
|
||||||
@@ -200,15 +206,23 @@ async def invoice_callback_dispatcher(checking_id: str, is_internal: bool = Fals
|
|||||||
invoice_listeners from core and extensions.
|
invoice_listeners from core and extensions.
|
||||||
"""
|
"""
|
||||||
payment = await get_standalone_payment(checking_id, incoming=True)
|
payment = await get_standalone_payment(checking_id, incoming=True)
|
||||||
if payment and payment.is_in:
|
if not payment:
|
||||||
status = await payment.check_status()
|
logger.warning(f"No payment found for '{checking_id}'.")
|
||||||
payment.fee = status.fee_msat or 0
|
return
|
||||||
# only overwrite preimage if status.preimage provides it
|
if not payment.is_in:
|
||||||
payment.preimage = status.preimage or payment.preimage
|
logger.warning(f"Payment '{checking_id}' is not incoming, skipping.")
|
||||||
payment.status = PaymentState.SUCCESS
|
return
|
||||||
await update_payment(payment)
|
|
||||||
internal = "internal" if is_internal else ""
|
status = await payment.check_status(skip_internal_payment_notifications=True)
|
||||||
logger.success(f"{internal} invoice {checking_id} settled")
|
payment.fee = status.fee_msat or payment.fee
|
||||||
for name, send_chan in invoice_listeners.items():
|
# only overwrite preimage if status.preimage provides it
|
||||||
logger.trace(f"invoice listeners: sending to `{name}`")
|
payment.preimage = status.preimage or payment.preimage
|
||||||
await send_chan.put(payment)
|
payment.status = PaymentState.SUCCESS
|
||||||
|
await update_payment(payment)
|
||||||
|
if payment.fiat_provider:
|
||||||
|
await handle_fiat_payment_confirmation(payment)
|
||||||
|
internal = "internal" if is_internal else ""
|
||||||
|
logger.success(f"{internal} invoice {checking_id} settled")
|
||||||
|
for name, send_chan in invoice_listeners.items():
|
||||||
|
logger.trace(f"invoice listeners: sending to `{name}`")
|
||||||
|
await send_chan.put(payment)
|
||||||
|
|||||||
@@ -423,9 +423,6 @@
|
|||||||
Object.keys(WINDOW_SETTINGS).forEach(key => {
|
Object.keys(WINDOW_SETTINGS).forEach(key => {
|
||||||
window[key] = WINDOW_SETTINGS[key]
|
window[key] = WINDOW_SETTINGS[key]
|
||||||
})
|
})
|
||||||
if (LNBITS_THEME_OPTIONS && LNBITS_THEME_OPTIONS.length) {
|
|
||||||
window.allowedThemes = LNBITS_THEME_OPTIONS.map(str => str.trim())
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
{% include('components.vue') %} {% block vue_templates %}{% endblock %} {%
|
{% include('components.vue') %} {% block vue_templates %}{% endblock %} {%
|
||||||
for url in INCLUDED_JS %}
|
for url in INCLUDED_JS %}
|
||||||
|
|||||||
@@ -998,11 +998,23 @@
|
|||||||
v-if="props.row.isIn && props.row.isPending && props.row.bolt11"
|
v-if="props.row.isIn && props.row.isPending && props.row.bolt11"
|
||||||
class="text-center q-my-lg"
|
class="text-center q-my-lg"
|
||||||
>
|
>
|
||||||
<a :href="'lightning:' + props.row.bolt11">
|
<div v-if="props.row.extra.fiat_payment_request">
|
||||||
<lnbits-qrcode
|
<a
|
||||||
:value="'lightning:' + props.row.bolt11.toUpperCase()"
|
:href="props.row.extra.fiat_payment_request"
|
||||||
></lnbits-qrcode>
|
target="_blank"
|
||||||
</a>
|
>
|
||||||
|
<lnbits-qrcode
|
||||||
|
:value="props.row.extra.fiat_payment_request"
|
||||||
|
></lnbits-qrcode>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<a :href="'lightning:' + props.row.bolt11">
|
||||||
|
<lnbits-qrcode
|
||||||
|
:value="'lightning:' + props.row.bolt11.toUpperCase()"
|
||||||
|
></lnbits-qrcode>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
@@ -1013,7 +1025,11 @@
|
|||||||
"
|
"
|
||||||
outline
|
outline
|
||||||
color="grey"
|
color="grey"
|
||||||
@click="copyText(props.row.bolt11)"
|
@click="
|
||||||
|
copyText(
|
||||||
|
props.row.extra.fiat_payment_request || props.row.bolt11
|
||||||
|
)
|
||||||
|
"
|
||||||
:label="$t('copy_invoice')"
|
:label="$t('copy_invoice')"
|
||||||
></q-btn>
|
></q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
|
|||||||
@@ -49,9 +49,6 @@
|
|||||||
Object.keys(WINDOW_SETTINGS).forEach(key => {
|
Object.keys(WINDOW_SETTINGS).forEach(key => {
|
||||||
window[key] = WINDOW_SETTINGS[key]
|
window[key] = WINDOW_SETTINGS[key]
|
||||||
})
|
})
|
||||||
if (LNBITS_THEME_OPTIONS && LNBITS_THEME_OPTIONS.length) {
|
|
||||||
window.allowedThemes = LNBITS_THEME_OPTIONS.map(str => str.trim())
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
{% include('components.vue') %}{% block vue_templates %}{% endblock %} {%
|
{% include('components.vue') %}{% block vue_templates %}{% endblock %} {%
|
||||||
for url in INCLUDED_JS %}
|
for url in INCLUDED_JS %}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import base64
|
|||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from typing import Dict, Tuple, Union
|
from typing import Union
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import secp256k1
|
import secp256k1
|
||||||
@@ -13,7 +13,7 @@ from Cryptodome.Util.Padding import pad, unpad
|
|||||||
from pynostr.key import PrivateKey
|
from pynostr.key import PrivateKey
|
||||||
|
|
||||||
|
|
||||||
def generate_keypair() -> Tuple[str, str]:
|
def generate_keypair() -> tuple[str, str]:
|
||||||
private_key = PrivateKey()
|
private_key = PrivateKey()
|
||||||
public_key = private_key.public_key
|
public_key = private_key.public_key
|
||||||
return private_key.hex(), public_key.hex()
|
return private_key.hex(), public_key.hex()
|
||||||
@@ -82,7 +82,7 @@ def decrypt_content(
|
|||||||
return decrypted
|
return decrypted
|
||||||
|
|
||||||
|
|
||||||
def verify_event(event: Dict) -> bool:
|
def verify_event(event: dict) -> bool:
|
||||||
"""
|
"""
|
||||||
Verify the event signature
|
Verify the event signature
|
||||||
|
|
||||||
@@ -115,8 +115,8 @@ def verify_event(event: Dict) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def sign_event(
|
def sign_event(
|
||||||
event: Dict, account_public_key_hex: str, account_private_key: secp256k1.PrivateKey
|
event: dict, account_public_key_hex: str, account_private_key: secp256k1.PrivateKey
|
||||||
) -> Dict:
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
Signs the event (in place) with the service secret
|
Signs the event (in place) with the service secret
|
||||||
|
|
||||||
@@ -149,7 +149,7 @@ def sign_event(
|
|||||||
return event
|
return event
|
||||||
|
|
||||||
|
|
||||||
def json_dumps(data: Union[Dict, list]) -> str:
|
def json_dumps(data: Union[dict, list]) -> str:
|
||||||
"""
|
"""
|
||||||
Converts a Python dictionary to a JSON string with compact encoding.
|
Converts a Python dictionary to a JSON string with compact encoding.
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ def json_dumps(data: Union[Dict, list]) -> str:
|
|||||||
Returns:
|
Returns:
|
||||||
str: The compact JSON string.
|
str: The compact JSON string.
|
||||||
"""
|
"""
|
||||||
if isinstance(data, Dict):
|
if isinstance(data, dict):
|
||||||
data = {k: v for k, v in data.items() if v is not None}
|
data = {k: v for k, v in data.items() if v is not None}
|
||||||
return json.dumps(data, separators=(",", ":"), ensure_ascii=False)
|
return json.dumps(data, separators=(",", ":"), ensure_ascii=False)
|
||||||
|
|
||||||
|
|||||||
@@ -58,17 +58,17 @@ __all__ = [
|
|||||||
"BlinkWallet",
|
"BlinkWallet",
|
||||||
"BoltzWallet",
|
"BoltzWallet",
|
||||||
"BreezSdkWallet",
|
"BreezSdkWallet",
|
||||||
"ClicheWallet",
|
|
||||||
"CoreLightningWallet",
|
|
||||||
"CLightningWallet",
|
"CLightningWallet",
|
||||||
|
"ClicheWallet",
|
||||||
"CoreLightningRestWallet",
|
"CoreLightningRestWallet",
|
||||||
|
"CoreLightningWallet",
|
||||||
"EclairWallet",
|
"EclairWallet",
|
||||||
"FakeWallet",
|
"FakeWallet",
|
||||||
"LNbitsWallet",
|
|
||||||
"LndWallet",
|
|
||||||
"LndRestWallet",
|
|
||||||
"LNPayWallet",
|
"LNPayWallet",
|
||||||
|
"LNbitsWallet",
|
||||||
"LnTipsWallet",
|
"LnTipsWallet",
|
||||||
|
"LndRestWallet",
|
||||||
|
"LndWallet",
|
||||||
"NWCWallet",
|
"NWCWallet",
|
||||||
"OpenNodeWallet",
|
"OpenNodeWallet",
|
||||||
"PhoenixdWallet",
|
"PhoenixdWallet",
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
from typing import AsyncGenerator, Optional
|
from collections.abc import AsyncGenerator
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
from lnbits.helpers import normalize_endpoint
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
@@ -27,7 +29,7 @@ class AlbyWallet(Wallet):
|
|||||||
if not settings.alby_access_token:
|
if not settings.alby_access_token:
|
||||||
raise ValueError("cannot initialize AlbyWallet: missing alby_access_token")
|
raise ValueError("cannot initialize AlbyWallet: missing alby_access_token")
|
||||||
|
|
||||||
self.endpoint = self.normalize_endpoint(settings.alby_api_endpoint)
|
self.endpoint = normalize_endpoint(settings.alby_api_endpoint)
|
||||||
self.auth = {
|
self.auth = {
|
||||||
"Authorization": "Bearer " + settings.alby_access_token,
|
"Authorization": "Bearer " + settings.alby_access_token,
|
||||||
"User-Agent": settings.user_agent,
|
"User-Agent": settings.user_agent,
|
||||||
|
|||||||
+2
-15
@@ -2,7 +2,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import TYPE_CHECKING, AsyncGenerator, Coroutine, NamedTuple
|
from collections.abc import AsyncGenerator, Coroutine
|
||||||
|
from typing import TYPE_CHECKING, NamedTuple
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
@@ -150,17 +151,3 @@ class Wallet(ABC):
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error(f"could not get status of invoice {invoice}: '{exc}' ")
|
logger.error(f"could not get status of invoice {invoice}: '{exc}' ")
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
def normalize_endpoint(self, endpoint: str, add_proto=True) -> str:
|
|
||||||
endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
|
|
||||||
if add_proto:
|
|
||||||
if endpoint.startswith("ws://") or endpoint.startswith("wss://"):
|
|
||||||
return endpoint
|
|
||||||
endpoint = (
|
|
||||||
f"https://{endpoint}" if not endpoint.startswith("http") else endpoint
|
|
||||||
)
|
|
||||||
return endpoint
|
|
||||||
|
|
||||||
|
|
||||||
class UnsupportedError(Exception):
|
|
||||||
pass
|
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
from typing import AsyncGenerator, Optional
|
from collections.abc import AsyncGenerator
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from websockets.client import WebSocketClientProtocol, connect
|
from websockets.legacy.client import WebSocketClientProtocol, connect
|
||||||
from websockets.typing import Subprotocol
|
from websockets.typing import Subprotocol
|
||||||
|
|
||||||
from lnbits import bolt11
|
from lnbits import bolt11
|
||||||
|
from lnbits.helpers import normalize_endpoint
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
@@ -34,13 +36,13 @@ class BlinkWallet(Wallet):
|
|||||||
if not settings.blink_token:
|
if not settings.blink_token:
|
||||||
raise ValueError("cannot initialize BlinkWallet: missing blink_token")
|
raise ValueError("cannot initialize BlinkWallet: missing blink_token")
|
||||||
|
|
||||||
self.endpoint = self.normalize_endpoint(settings.blink_api_endpoint)
|
self.endpoint = normalize_endpoint(settings.blink_api_endpoint)
|
||||||
|
|
||||||
self.auth = {
|
self.auth = {
|
||||||
"X-API-KEY": settings.blink_token,
|
"X-API-KEY": settings.blink_token,
|
||||||
"User-Agent": settings.user_agent,
|
"User-Agent": settings.user_agent,
|
||||||
}
|
}
|
||||||
self.ws_endpoint = self.normalize_endpoint(settings.blink_ws_endpoint)
|
self.ws_endpoint = normalize_endpoint(settings.blink_ws_endpoint)
|
||||||
self.ws_auth = {
|
self.ws_auth = {
|
||||||
"type": "connection_init",
|
"type": "connection_init",
|
||||||
"payload": {"X-API-KEY": settings.blink_token},
|
"payload": {"X-API-KEY": settings.blink_token},
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from typing import AsyncGenerator, Optional
|
from collections.abc import AsyncGenerator
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from bolt11.decode import decode
|
from bolt11.decode import decode
|
||||||
from grpc.aio import AioRpcError
|
from grpc.aio import AioRpcError
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
from lnbits.helpers import normalize_endpoint
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.wallets.boltz_grpc_files import boltzrpc_pb2, boltzrpc_pb2_grpc
|
from lnbits.wallets.boltz_grpc_files import boltzrpc_pb2, boltzrpc_pb2_grpc
|
||||||
from lnbits.wallets.lnd_grpc_files.lightning_pb2_grpc import grpc
|
from lnbits.wallets.lnd_grpc_files.lightning_pb2_grpc import grpc
|
||||||
@@ -42,7 +44,7 @@ class BoltzWallet(Wallet):
|
|||||||
"cannot initialize BoltzWallet: missing boltz_client_wallet"
|
"cannot initialize BoltzWallet: missing boltz_client_wallet"
|
||||||
)
|
)
|
||||||
|
|
||||||
self.endpoint = self.normalize_endpoint(
|
self.endpoint = normalize_endpoint(
|
||||||
settings.boltz_client_endpoint, add_proto=True
|
settings.boltz_client_endpoint, add_proto=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import base64
|
import base64
|
||||||
|
|
||||||
|
from lnbits.exceptions import UnsupportedError
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import breez_sdk # type: ignore
|
import breez_sdk # type: ignore
|
||||||
|
|
||||||
@@ -18,8 +20,9 @@ if not BREEZ_SDK_INSTALLED:
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from collections.abc import AsyncGenerator
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import AsyncGenerator, Optional
|
from typing import Optional
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
@@ -34,7 +37,6 @@ else:
|
|||||||
PaymentStatus,
|
PaymentStatus,
|
||||||
PaymentSuccessStatus,
|
PaymentSuccessStatus,
|
||||||
StatusResponse,
|
StatusResponse,
|
||||||
UnsupportedError,
|
|
||||||
Wallet,
|
Wallet,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
from typing import AsyncGenerator, Optional
|
from collections.abc import AsyncGenerator
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from websocket import create_connection
|
from websocket import create_connection
|
||||||
|
|
||||||
|
from lnbits.helpers import normalize_endpoint
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
@@ -25,7 +27,7 @@ class ClicheWallet(Wallet):
|
|||||||
if not settings.cliche_endpoint:
|
if not settings.cliche_endpoint:
|
||||||
raise ValueError("cannot initialize ClicheWallet: missing cliche_endpoint")
|
raise ValueError("cannot initialize ClicheWallet: missing cliche_endpoint")
|
||||||
|
|
||||||
self.endpoint = self.normalize_endpoint(settings.cliche_endpoint)
|
self.endpoint = normalize_endpoint(settings.cliche_endpoint)
|
||||||
|
|
||||||
async def cleanup(self):
|
async def cleanup(self):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from bolt11.exceptions import Bolt11Exception
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pyln.client import LightningRpc, RpcError
|
from pyln.client import LightningRpc, RpcError
|
||||||
|
|
||||||
|
from lnbits.exceptions import UnsupportedError
|
||||||
from lnbits.nodes.cln import CoreLightningNode
|
from lnbits.nodes.cln import CoreLightningNode
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.utils.crypto import random_secret_and_hash
|
from lnbits.utils.crypto import random_secret_and_hash
|
||||||
@@ -20,7 +21,6 @@ from .base import (
|
|||||||
PaymentStatus,
|
PaymentStatus,
|
||||||
PaymentSuccessStatus,
|
PaymentSuccessStatus,
|
||||||
StatusResponse,
|
StatusResponse,
|
||||||
UnsupportedError,
|
|
||||||
Wallet,
|
Wallet,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ from bolt11 import Bolt11Exception
|
|||||||
from bolt11.decode import decode
|
from bolt11.decode import decode
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
from lnbits.exceptions import UnsupportedError
|
||||||
|
from lnbits.helpers import normalize_endpoint
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.utils.crypto import random_secret_and_hash
|
from lnbits.utils.crypto import random_secret_and_hash
|
||||||
|
|
||||||
@@ -18,7 +20,6 @@ from .base import (
|
|||||||
PaymentResponse,
|
PaymentResponse,
|
||||||
PaymentStatus,
|
PaymentStatus,
|
||||||
StatusResponse,
|
StatusResponse,
|
||||||
UnsupportedError,
|
|
||||||
Wallet,
|
Wallet,
|
||||||
)
|
)
|
||||||
from .macaroon import load_macaroon
|
from .macaroon import load_macaroon
|
||||||
@@ -43,7 +44,7 @@ class CoreLightningRestWallet(Wallet):
|
|||||||
"invalid corelightning_rest_macaroon provided"
|
"invalid corelightning_rest_macaroon provided"
|
||||||
)
|
)
|
||||||
|
|
||||||
self.url = self.normalize_endpoint(settings.corelightning_rest_url)
|
self.url = normalize_endpoint(settings.corelightning_rest_url)
|
||||||
headers = {
|
headers = {
|
||||||
"macaroon": macaroon,
|
"macaroon": macaroon,
|
||||||
"encodingtype": "hex",
|
"encodingtype": "hex",
|
||||||
@@ -217,7 +218,7 @@ class CoreLightningRestWallet(Wallet):
|
|||||||
data = exc.response.json()
|
data = exc.response.json()
|
||||||
error_code = int(data["error"]["code"])
|
error_code = int(data["error"]["code"])
|
||||||
if error_code in self.pay_failure_error_codes:
|
if error_code in self.pay_failure_error_codes:
|
||||||
error_message = f"Payment failed: {data['error']['message']}"
|
error_message = data["error"]["message"]
|
||||||
return PaymentResponse(ok=False, error_message=error_message)
|
return PaymentResponse(ok=False, error_message=error_message)
|
||||||
error_message = f"REST failed with {data['error']['message']}."
|
error_message = f"REST failed with {data['error']['message']}."
|
||||||
return PaymentResponse(error_message=error_message)
|
return PaymentResponse(error_message=error_message)
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ from typing import Any, Optional
|
|||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from websockets.client import connect
|
from websockets.legacy.client import connect
|
||||||
|
|
||||||
|
from lnbits.helpers import normalize_endpoint
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.utils.crypto import random_secret_and_hash
|
from lnbits.utils.crypto import random_secret_and_hash
|
||||||
|
|
||||||
@@ -39,7 +40,7 @@ class EclairWallet(Wallet):
|
|||||||
if not settings.eclair_pass:
|
if not settings.eclair_pass:
|
||||||
raise ValueError("cannot initialize EclairWallet: missing eclair_pass")
|
raise ValueError("cannot initialize EclairWallet: missing eclair_pass")
|
||||||
|
|
||||||
self.url = self.normalize_endpoint(settings.eclair_url)
|
self.url = normalize_endpoint(settings.eclair_url)
|
||||||
self.ws_url = f"ws://{urllib.parse.urlsplit(self.url).netloc}/ws"
|
self.ws_url = f"ws://{urllib.parse.urlsplit(self.url).netloc}/ws"
|
||||||
|
|
||||||
password = settings.eclair_pass
|
password = settings.eclair_pass
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
from collections.abc import AsyncGenerator
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from os import urandom
|
from os import urandom
|
||||||
from typing import AsyncGenerator, Optional
|
from typing import Optional
|
||||||
|
|
||||||
from bolt11 import (
|
from bolt11 import (
|
||||||
Bolt11,
|
Bolt11,
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
from typing import AsyncGenerator, Dict, Optional
|
from collections.abc import AsyncGenerator
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from websockets.client import connect
|
from websockets.legacy.client import connect
|
||||||
|
|
||||||
|
from lnbits.helpers import normalize_endpoint
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
@@ -36,7 +38,7 @@ class LNbitsWallet(Wallet):
|
|||||||
"cannot initialize LNbitsWallet: "
|
"cannot initialize LNbitsWallet: "
|
||||||
"missing lnbits_key or lnbits_admin_key or lnbits_invoice_key"
|
"missing lnbits_key or lnbits_admin_key or lnbits_invoice_key"
|
||||||
)
|
)
|
||||||
self.endpoint = self.normalize_endpoint(settings.lnbits_endpoint)
|
self.endpoint = normalize_endpoint(settings.lnbits_endpoint)
|
||||||
self.ws_url = f"{self.endpoint.replace('http', 'ws', 1)}/api/v1/ws/{key}"
|
self.ws_url = f"{self.endpoint.replace('http', 'ws', 1)}/api/v1/ws/{key}"
|
||||||
self.headers = {"X-Api-Key": key, "User-Agent": settings.user_agent}
|
self.headers = {"X-Api-Key": key, "User-Agent": settings.user_agent}
|
||||||
self.client = httpx.AsyncClient(base_url=self.endpoint, headers=self.headers)
|
self.client = httpx.AsyncClient(base_url=self.endpoint, headers=self.headers)
|
||||||
@@ -74,7 +76,7 @@ class LNbitsWallet(Wallet):
|
|||||||
unhashed_description: Optional[bytes] = None,
|
unhashed_description: Optional[bytes] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
data: Dict = {"out": False, "amount": amount, "memo": memo or ""}
|
data: dict = {"out": False, "amount": amount, "memo": memo or ""}
|
||||||
if kwargs.get("expiry"):
|
if kwargs.get("expiry"):
|
||||||
data["expiry"] = kwargs["expiry"]
|
data["expiry"] = kwargs["expiry"]
|
||||||
if description_hash:
|
if description_hash:
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import base64
|
import base64
|
||||||
|
from collections.abc import AsyncGenerator
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from os import environ
|
from os import environ
|
||||||
from typing import AsyncGenerator, Dict, Optional
|
from typing import Optional
|
||||||
|
|
||||||
import grpc
|
import grpc
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -11,6 +12,7 @@ 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.lightning_pb2_grpc as lnrpc
|
||||||
import lnbits.wallets.lnd_grpc_files.router_pb2 as router
|
import lnbits.wallets.lnd_grpc_files.router_pb2 as router
|
||||||
import lnbits.wallets.lnd_grpc_files.router_pb2_grpc as routerrpc
|
import lnbits.wallets.lnd_grpc_files.router_pb2_grpc as routerrpc
|
||||||
|
from lnbits.helpers import normalize_endpoint
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.utils.crypto import random_secret_and_hash
|
from lnbits.utils.crypto import random_secret_and_hash
|
||||||
|
|
||||||
@@ -72,9 +74,7 @@ class LndWallet(Wallet):
|
|||||||
"cannot initialize LndWallet: missing lnd_grpc_cert or lnd_cert"
|
"cannot initialize LndWallet: missing lnd_grpc_cert or lnd_cert"
|
||||||
)
|
)
|
||||||
|
|
||||||
self.endpoint = self.normalize_endpoint(
|
self.endpoint = normalize_endpoint(settings.lnd_grpc_endpoint, add_proto=False)
|
||||||
settings.lnd_grpc_endpoint, add_proto=False
|
|
||||||
)
|
|
||||||
self.port = int(settings.lnd_grpc_port)
|
self.port = int(settings.lnd_grpc_port)
|
||||||
|
|
||||||
macaroon = (
|
macaroon = (
|
||||||
@@ -122,7 +122,7 @@ class LndWallet(Wallet):
|
|||||||
unhashed_description: Optional[bytes] = None,
|
unhashed_description: Optional[bytes] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
data: Dict = {
|
data: dict = {
|
||||||
"description_hash": b"",
|
"description_hash": b"",
|
||||||
"value": amount,
|
"value": amount,
|
||||||
"private": True,
|
"private": True,
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ import asyncio
|
|||||||
import base64
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
from typing import Any, AsyncGenerator, Dict, Optional
|
from collections.abc import AsyncGenerator
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
from lnbits.helpers import normalize_endpoint
|
||||||
from lnbits.nodes.lndrest import LndRestNode
|
from lnbits.nodes.lndrest import LndRestNode
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.utils.crypto import random_secret_and_hash
|
from lnbits.utils.crypto import random_secret_and_hash
|
||||||
@@ -41,7 +43,7 @@ class LndRestWallet(Wallet):
|
|||||||
"This only works if you have a publicly issued certificate."
|
"This only works if you have a publicly issued certificate."
|
||||||
)
|
)
|
||||||
|
|
||||||
self.endpoint = self.normalize_endpoint(settings.lnd_rest_endpoint)
|
self.endpoint = normalize_endpoint(settings.lnd_rest_endpoint)
|
||||||
|
|
||||||
# if no cert provided it should be public so we set verify to True
|
# if no cert provided it should be public so we set verify to True
|
||||||
# and it will still check for validity of certificate and fail if its not valid
|
# and it will still check for validity of certificate and fail if its not valid
|
||||||
@@ -104,7 +106,7 @@ class LndRestWallet(Wallet):
|
|||||||
unhashed_description: Optional[bytes] = None,
|
unhashed_description: Optional[bytes] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
_data: Dict = {
|
_data: dict = {
|
||||||
"value": amount,
|
"value": amount,
|
||||||
"private": settings.lnd_rest_route_hints,
|
"private": settings.lnd_rest_route_hints,
|
||||||
"memo": memo or "",
|
"memo": memo or "",
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import hashlib
|
import hashlib
|
||||||
from typing import AsyncGenerator, Dict, Optional
|
from collections.abc import AsyncGenerator
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
from lnbits.helpers import normalize_endpoint
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
@@ -36,7 +38,7 @@ class LNPayWallet(Wallet):
|
|||||||
"missing lnpay_wallet_key or lnpay_admin_key"
|
"missing lnpay_wallet_key or lnpay_admin_key"
|
||||||
)
|
)
|
||||||
self.wallet_key = wallet_key
|
self.wallet_key = wallet_key
|
||||||
self.endpoint = self.normalize_endpoint(settings.lnpay_api_endpoint)
|
self.endpoint = normalize_endpoint(settings.lnpay_api_endpoint)
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
"X-Api-Key": settings.lnpay_api_key,
|
"X-Api-Key": settings.lnpay_api_key,
|
||||||
@@ -78,7 +80,7 @@ class LNPayWallet(Wallet):
|
|||||||
unhashed_description: Optional[bytes] = None,
|
unhashed_description: Optional[bytes] = None,
|
||||||
**_,
|
**_,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
data: Dict = {"num_satoshis": f"{amount}"}
|
data: dict = {"num_satoshis": f"{amount}"}
|
||||||
if description_hash:
|
if description_hash:
|
||||||
data["description_hash"] = description_hash.hex()
|
data["description_hash"] = description_hash.hex()
|
||||||
elif unhashed_description:
|
elif unhashed_description:
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ import asyncio
|
|||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
from typing import AsyncGenerator, Dict, Optional
|
from collections.abc import AsyncGenerator
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
from lnbits.helpers import normalize_endpoint
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
@@ -36,7 +38,7 @@ class LnTipsWallet(Wallet):
|
|||||||
"missing lntips_api_key or lntips_admin_key or lntips_invoice_key"
|
"missing lntips_api_key or lntips_admin_key or lntips_invoice_key"
|
||||||
)
|
)
|
||||||
|
|
||||||
self.endpoint = self.normalize_endpoint(settings.lntips_api_endpoint)
|
self.endpoint = normalize_endpoint(settings.lntips_api_endpoint)
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"Basic {key}",
|
"Authorization": f"Basic {key}",
|
||||||
@@ -72,7 +74,7 @@ class LnTipsWallet(Wallet):
|
|||||||
unhashed_description: Optional[bytes] = None,
|
unhashed_description: Optional[bytes] = None,
|
||||||
**_,
|
**_,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
data: Dict = {"amount": amount, "description_hash": "", "memo": memo or ""}
|
data: dict = {"amount": amount, "description_hash": "", "memo": memo or ""}
|
||||||
if description_hash:
|
if description_hash:
|
||||||
data["description_hash"] = description_hash.hex()
|
data["description_hash"] = description_hash.hex()
|
||||||
elif unhashed_description:
|
elif unhashed_description:
|
||||||
|
|||||||
+13
-12
@@ -3,13 +3,14 @@ import hashlib
|
|||||||
import json
|
import json
|
||||||
import random
|
import random
|
||||||
import time
|
import time
|
||||||
from typing import AsyncGenerator, Dict, List, Optional, Union, cast
|
from collections.abc import AsyncGenerator
|
||||||
|
from typing import Optional, Union, cast
|
||||||
from urllib.parse import parse_qs, unquote, urlparse
|
from urllib.parse import parse_qs, unquote, urlparse
|
||||||
|
|
||||||
import secp256k1
|
import secp256k1
|
||||||
from bolt11 import decode as bolt11_decode
|
from bolt11 import decode as bolt11_decode
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from websockets.client import connect as ws_connect
|
from websockets.legacy.client import connect as ws_connect
|
||||||
|
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.utils.nostr import (
|
from lnbits.utils.nostr import (
|
||||||
@@ -358,7 +359,7 @@ class NWCConnection:
|
|||||||
"""
|
"""
|
||||||
return self.shutdown or not settings.lnbits_running
|
return self.shutdown or not settings.lnbits_running
|
||||||
|
|
||||||
async def _send(self, data: List[Union[str, Dict]]):
|
async def _send(self, data: list[Union[str, dict]]):
|
||||||
"""
|
"""
|
||||||
Sends data to the NWC relay.
|
Sends data to the NWC relay.
|
||||||
|
|
||||||
@@ -394,7 +395,7 @@ class NWCConnection:
|
|||||||
|
|
||||||
async def _close_subscription_by_subid(
|
async def _close_subscription_by_subid(
|
||||||
self, sub_id: str, send_event: bool = True
|
self, sub_id: str, send_event: bool = True
|
||||||
) -> Optional[Dict]:
|
) -> Optional[dict]:
|
||||||
"""
|
"""
|
||||||
Closes a subscription by its sub_id.
|
Closes a subscription by its sub_id.
|
||||||
|
|
||||||
@@ -425,7 +426,7 @@ class NWCConnection:
|
|||||||
|
|
||||||
async def _close_subscription_by_eventid(
|
async def _close_subscription_by_eventid(
|
||||||
self, event_id, send_event=True
|
self, event_id, send_event=True
|
||||||
) -> Optional[Dict]:
|
) -> Optional[dict]:
|
||||||
"""
|
"""
|
||||||
Closes a subscription associated to an event_id.
|
Closes a subscription associated to an event_id.
|
||||||
|
|
||||||
@@ -498,7 +499,7 @@ class NWCConnection:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Error handling subscription timeout: " + str(e))
|
logger.error("Error handling subscription timeout: " + str(e))
|
||||||
|
|
||||||
async def _on_ok_message(self, msg: List[str]):
|
async def _on_ok_message(self, msg: list[str]):
|
||||||
"""
|
"""
|
||||||
Handles OK messages from the relay.
|
Handles OK messages from the relay.
|
||||||
"""
|
"""
|
||||||
@@ -512,12 +513,12 @@ class NWCConnection:
|
|||||||
if subscription: # Check if the subscription exists first
|
if subscription: # Check if the subscription exists first
|
||||||
subscription["future"].set_exception(Exception(info))
|
subscription["future"].set_exception(Exception(info))
|
||||||
|
|
||||||
async def _on_event_message(self, msg: List[Union[str, Dict]]):
|
async def _on_event_message(self, msg: list[Union[str, dict]]):
|
||||||
"""
|
"""
|
||||||
Handles EVENT messages from the relay.
|
Handles EVENT messages from the relay.
|
||||||
"""
|
"""
|
||||||
sub_id = cast(str, msg[1])
|
sub_id = cast(str, msg[1])
|
||||||
event = cast(Dict, msg[2])
|
event = cast(dict, msg[2])
|
||||||
if not verify_event(event): # Ensure the event is valid (do not trust relays)
|
if not verify_event(event): # Ensure the event is valid (do not trust relays)
|
||||||
raise Exception("Invalid event signature")
|
raise Exception("Invalid event signature")
|
||||||
tags = event["tags"]
|
tags = event["tags"]
|
||||||
@@ -571,7 +572,7 @@ class NWCConnection:
|
|||||||
else:
|
else:
|
||||||
subscription["future"].set_result(result)
|
subscription["future"].set_result(result)
|
||||||
|
|
||||||
async def _on_closed_message(self, msg: List[str]):
|
async def _on_closed_message(self, msg: list[str]):
|
||||||
"""
|
"""
|
||||||
Handles CLOSED messages from the relay.
|
Handles CLOSED messages from the relay.
|
||||||
"""
|
"""
|
||||||
@@ -646,7 +647,7 @@ class NWCConnection:
|
|||||||
logger.debug("Reconnecting to NWC relay in 5 seconds...")
|
logger.debug("Reconnecting to NWC relay in 5 seconds...")
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
async def call(self, method: str, params: Dict) -> Dict:
|
async def call(self, method: str, params: dict) -> dict:
|
||||||
"""
|
"""
|
||||||
Call a NWC method.
|
Call a NWC method.
|
||||||
|
|
||||||
@@ -708,7 +709,7 @@ class NWCConnection:
|
|||||||
# Wait for the response
|
# Wait for the response
|
||||||
return await future
|
return await future
|
||||||
|
|
||||||
async def get_info(self) -> Dict:
|
async def get_info(self) -> dict:
|
||||||
"""
|
"""
|
||||||
Get the info about the service provider and cache it.
|
Get the info about the service provider and cache it.
|
||||||
|
|
||||||
@@ -793,7 +794,7 @@ class NWCConnection:
|
|||||||
logger.warning("Error closing connection: " + str(e))
|
logger.warning("Error closing connection: " + str(e))
|
||||||
|
|
||||||
|
|
||||||
def parse_nwc(nwc) -> Dict:
|
def parse_nwc(nwc) -> dict:
|
||||||
"""
|
"""
|
||||||
Parses a NWC URL (nostr+walletconnect://...) and extracts relevant information.
|
Parses a NWC URL (nostr+walletconnect://...) and extracts relevant information.
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from typing import AsyncGenerator, Optional
|
from collections.abc import AsyncGenerator
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
from lnbits.exceptions import UnsupportedError
|
||||||
|
from lnbits.helpers import normalize_endpoint
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
@@ -12,7 +15,6 @@ from .base import (
|
|||||||
PaymentResponse,
|
PaymentResponse,
|
||||||
PaymentStatus,
|
PaymentStatus,
|
||||||
StatusResponse,
|
StatusResponse,
|
||||||
UnsupportedError,
|
|
||||||
Wallet,
|
Wallet,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -38,7 +40,7 @@ class OpenNodeWallet(Wallet):
|
|||||||
)
|
)
|
||||||
self.key = key
|
self.key = key
|
||||||
|
|
||||||
self.endpoint = self.normalize_endpoint(settings.opennode_api_endpoint)
|
self.endpoint = normalize_endpoint(settings.opennode_api_endpoint)
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": self.key,
|
"Authorization": self.key,
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import base64
|
|||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
from typing import Any, AsyncGenerator, Dict, Optional
|
from collections.abc import AsyncGenerator
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from websockets.client import connect
|
from websockets.legacy.client import connect
|
||||||
|
|
||||||
|
from lnbits.helpers import normalize_endpoint
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
@@ -35,7 +37,7 @@ class PhoenixdWallet(Wallet):
|
|||||||
"cannot initialize PhoenixdWallet: missing phoenixd_api_password"
|
"cannot initialize PhoenixdWallet: missing phoenixd_api_password"
|
||||||
)
|
)
|
||||||
|
|
||||||
self.endpoint = self.normalize_endpoint(settings.phoenixd_api_endpoint)
|
self.endpoint = normalize_endpoint(settings.phoenixd_api_endpoint)
|
||||||
parsed_url = urllib.parse.urlparse(settings.phoenixd_api_endpoint)
|
parsed_url = urllib.parse.urlparse(settings.phoenixd_api_endpoint)
|
||||||
|
|
||||||
if parsed_url.scheme == "http":
|
if parsed_url.scheme == "http":
|
||||||
@@ -100,7 +102,7 @@ class PhoenixdWallet(Wallet):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
msats_amount = amount
|
msats_amount = amount
|
||||||
data: Dict[str, Any] = {
|
data: dict[str, Any] = {
|
||||||
"amountSat": f"{msats_amount}",
|
"amountSat": f"{msats_amount}",
|
||||||
"externalId": "",
|
"externalId": "",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ import asyncio
|
|||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import random
|
import random
|
||||||
from typing import AsyncGenerator, Optional
|
from collections.abc import AsyncGenerator
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
from lnbits.helpers import normalize_endpoint
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
@@ -36,7 +38,7 @@ class SparkWallet(Wallet):
|
|||||||
if not settings.spark_token:
|
if not settings.spark_token:
|
||||||
raise ValueError("cannot initialize SparkWallet: missing spark_token")
|
raise ValueError("cannot initialize SparkWallet: missing spark_token")
|
||||||
|
|
||||||
url = self.normalize_endpoint(settings.spark_url)
|
url = normalize_endpoint(settings.spark_url)
|
||||||
url = url.replace("/rpc", "")
|
url = url.replace("/rpc", "")
|
||||||
self.token = settings.spark_token
|
self.token = settings.spark_token
|
||||||
|
|
||||||
|
|||||||
+13
-11
@@ -1,11 +1,13 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import time
|
import time
|
||||||
|
from collections.abc import AsyncGenerator
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Any, AsyncGenerator, Dict, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
from lnbits.helpers import normalize_endpoint
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
@@ -94,7 +96,7 @@ class StrikeWallet(Wallet):
|
|||||||
self._general_limiter = TokenBucket(1000, 600)
|
self._general_limiter = TokenBucket(1000, 600)
|
||||||
|
|
||||||
self.client = httpx.AsyncClient(
|
self.client = httpx.AsyncClient(
|
||||||
base_url=self.normalize_endpoint(settings.strike_api_endpoint),
|
base_url=normalize_endpoint(settings.strike_api_endpoint),
|
||||||
headers={
|
headers={
|
||||||
"Authorization": f"Bearer {settings.strike_api_key}",
|
"Authorization": f"Bearer {settings.strike_api_key}",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -110,8 +112,8 @@ class StrikeWallet(Wallet):
|
|||||||
|
|
||||||
# runtime state
|
# runtime state
|
||||||
self.pending_invoices: list[str] = [] # Keep it as a list
|
self.pending_invoices: list[str] = [] # Keep it as a list
|
||||||
self.pending_payments: Dict[str, str] = {}
|
self.pending_payments: dict[str, str] = {}
|
||||||
self.failed_payments: Dict[str, str] = {}
|
self.failed_payments: dict[str, str] = {}
|
||||||
|
|
||||||
# balance cache
|
# balance cache
|
||||||
self._cached_balance: Optional[int] = None
|
self._cached_balance: Optional[int] = None
|
||||||
@@ -182,7 +184,7 @@ class StrikeWallet(Wallet):
|
|||||||
if btc and "available" in btc:
|
if btc and "available" in btc:
|
||||||
available_btc = Decimal(btc["available"]) # Get available BTC amount.
|
available_btc = Decimal(btc["available"]) # Get available BTC amount.
|
||||||
msats = int(
|
msats = int(
|
||||||
available_btc * Decimal(1e11)
|
available_btc * Decimal("1e11")
|
||||||
) # Convert BTC to millisatoshis.
|
) # Convert BTC to millisatoshis.
|
||||||
self._cached_balance = msats
|
self._cached_balance = msats
|
||||||
self._cached_balance_ts = now
|
self._cached_balance_ts = now
|
||||||
@@ -203,10 +205,10 @@ class StrikeWallet(Wallet):
|
|||||||
**kwargs,
|
**kwargs,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
try:
|
try:
|
||||||
btc_amt = (Decimal(amount) / Decimal(1e8)).quantize(
|
btc_amt = (Decimal(amount) / Decimal("1e8")).quantize(
|
||||||
Decimal("0.00000001")
|
Decimal("0.00000001")
|
||||||
) # Convert amount from millisatoshis to BTC.
|
) # Convert amount from millisatoshis to BTC.
|
||||||
payload: Dict[str, Any] = {
|
payload: dict[str, Any] = {
|
||||||
"bolt11": {
|
"bolt11": {
|
||||||
"amount": {
|
"amount": {
|
||||||
"currency": "BTC",
|
"currency": "BTC",
|
||||||
@@ -269,7 +271,7 @@ class StrikeWallet(Wallet):
|
|||||||
# Network fee → msat.
|
# Network fee → msat.
|
||||||
fee_obj = data.get("lightningNetworkFee") or data.get("totalFee") or {}
|
fee_obj = data.get("lightningNetworkFee") or data.get("totalFee") or {}
|
||||||
fee_btc = Decimal(fee_obj.get("amount", "0"))
|
fee_btc = Decimal(fee_obj.get("amount", "0"))
|
||||||
fee_msat = int(fee_btc * Decimal(1e11)) # millisatoshis.
|
fee_msat = int(fee_btc * Decimal("1e11")) # millisatoshis.
|
||||||
|
|
||||||
if state in {"SUCCEEDED", "COMPLETED"}:
|
if state in {"SUCCEEDED", "COMPLETED"}:
|
||||||
preimage = data.get("preimage") or data.get("preImage")
|
preimage = data.get("preimage") or data.get("preImage")
|
||||||
@@ -426,9 +428,9 @@ class StrikeWallet(Wallet):
|
|||||||
orderby: Optional[str] = None,
|
orderby: Optional[str] = None,
|
||||||
skip: Optional[int] = None,
|
skip: Optional[int] = None,
|
||||||
top: Optional[int] = None,
|
top: Optional[int] = None,
|
||||||
) -> Dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
try:
|
try:
|
||||||
params: Dict[str, Any] = {}
|
params: dict[str, Any] = {}
|
||||||
if filters:
|
if filters:
|
||||||
params["$filter"] = filters
|
params["$filter"] = filters
|
||||||
if orderby:
|
if orderby:
|
||||||
@@ -464,7 +466,7 @@ class StrikeWallet(Wallet):
|
|||||||
try:
|
try:
|
||||||
if currency_str == "BTC":
|
if currency_str == "BTC":
|
||||||
fee_btc_decimal = Decimal(amount_str)
|
fee_btc_decimal = Decimal(amount_str)
|
||||||
fee_msat = int(fee_btc_decimal * Decimal(1e11))
|
fee_msat = int(fee_btc_decimal * Decimal("1e11"))
|
||||||
elif currency_str == "SAT":
|
elif currency_str == "SAT":
|
||||||
fee_sat_decimal = Decimal(amount_str)
|
fee_sat_decimal = Decimal(amount_str)
|
||||||
fee_msat = int(fee_sat_decimal * 1000)
|
fee_msat = int(fee_sat_decimal * 1000)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import hashlib
|
import hashlib
|
||||||
from typing import AsyncGenerator, Dict, Optional
|
from collections.abc import AsyncGenerator
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from bolt11 import decode as bolt11_decode
|
from bolt11 import decode as bolt11_decode
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
from lnbits.helpers import normalize_endpoint
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
@@ -27,7 +29,7 @@ class ZBDWallet(Wallet):
|
|||||||
if not settings.zbd_api_key:
|
if not settings.zbd_api_key:
|
||||||
raise ValueError("cannot initialize ZBDWallet: missing zbd_api_key")
|
raise ValueError("cannot initialize ZBDWallet: missing zbd_api_key")
|
||||||
|
|
||||||
self.endpoint = self.normalize_endpoint(settings.zbd_api_endpoint)
|
self.endpoint = normalize_endpoint(settings.zbd_api_endpoint)
|
||||||
headers = {
|
headers = {
|
||||||
"apikey": settings.zbd_api_key,
|
"apikey": settings.zbd_api_key,
|
||||||
"User-Agent": settings.user_agent,
|
"User-Agent": settings.user_agent,
|
||||||
@@ -66,7 +68,7 @@ class ZBDWallet(Wallet):
|
|||||||
# https://api.zebedee.io/v0/charges
|
# https://api.zebedee.io/v0/charges
|
||||||
|
|
||||||
msats_amount = amount * 1000
|
msats_amount = amount * 1000
|
||||||
data: Dict = {
|
data: dict = {
|
||||||
"amount": f"{msats_amount}",
|
"amount": f"{msats_amount}",
|
||||||
"expiresIn": 3600,
|
"expiresIn": 3600,
|
||||||
"callbackUrl": "",
|
"callbackUrl": "",
|
||||||
|
|||||||
Generated
+873
-264
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user