diff --git a/lnbits/core/crud/payments.py b/lnbits/core/crud/payments.py
index a13eb173..f9d93924 100644
--- a/lnbits/core/crud/payments.py
+++ b/lnbits/core/crud/payments.py
@@ -39,7 +39,6 @@ async def get_standalone_payment(
) -> Payment | None:
clause: str = "checking_id = :checking_id OR payment_hash = :hash"
values = {
- "wallet_id": wallet_id,
"checking_id": checking_id_or_hash,
"hash": checking_id_or_hash,
}
@@ -47,6 +46,10 @@ async def get_standalone_payment(
clause = f"({clause}) AND amount > 0"
if wallet_id:
+ wallet = await get_wallet(wallet_id)
+ if not wallet or not wallet.can_view_payments:
+ return None
+ values["wallet_id"] = wallet.source_wallet_id
clause = f"({clause}) AND wallet_id = :wallet_id"
row = await (conn or db).fetchone(
@@ -66,13 +69,16 @@ async def get_standalone_payment(
async def get_wallet_payment(
wallet_id: str, payment_hash: str, conn: Connection | None = None
) -> Payment | None:
+ wallet = await get_wallet(wallet_id)
+ if not wallet or not wallet.can_view_payments:
+ return None
payment = await (conn or db).fetchone(
"""
SELECT *
FROM apipayments
WHERE wallet_id = :wallet AND payment_hash = :hash
""",
- {"wallet": wallet_id, "hash": payment_hash},
+ {"wallet": wallet.source_wallet_id, "hash": payment_hash},
Payment,
)
return payment
@@ -128,7 +134,11 @@ async def get_payments_paginated( # noqa: C901
clause.append(f"time > {db.timestamp_placeholder('time')}")
if wallet_id:
- values["wallet_id"] = wallet_id
+ wallet = await get_wallet(wallet_id)
+ if not wallet or not wallet.can_view_payments:
+ return Page(data=[], total=0)
+
+ values["wallet_id"] = wallet.source_wallet_id
clause.append("wallet_id = :wallet_id")
elif user_id:
only_user_wallets = await _only_user_wallets_statement(user_id, conn=conn)
@@ -320,7 +330,7 @@ async def get_payments_history(
date_trunc = db.datetime_grouping(group)
- values = {
+ values: dict[str, Any] = {
"wallet_id": wallet_id,
}
# count outgoing payments if they are still pending
@@ -350,10 +360,10 @@ async def get_payments_history(
)
if wallet_id:
wallet = await get_wallet(wallet_id)
- if wallet:
- balance = wallet.balance_msat
- else:
- raise ValueError("Unknown wallet")
+ if not wallet or not wallet.can_view_payments:
+ return []
+ balance = wallet.balance_msat
+ values["wallet_id"] = wallet.source_wallet_id
else:
balance = await get_total_balance()
diff --git a/lnbits/core/crud/wallets.py b/lnbits/core/crud/wallets.py
index 771f6c04..67d63eaa 100644
--- a/lnbits/core/crud/wallets.py
+++ b/lnbits/core/crud/wallets.py
@@ -3,7 +3,7 @@ from time import time
from uuid import uuid4
from lnbits.core.db import db
-from lnbits.core.models.wallets import WalletsFilters
+from lnbits.core.models.wallets import WalletsFilters, WalletType
from lnbits.db import Connection, Filters, Page
from lnbits.settings import settings
@@ -14,17 +14,22 @@ async def create_wallet(
*,
user_id: str,
wallet_name: str | None = None,
+ wallet_type: WalletType = WalletType.LIGHTNING,
+ shared_wallet_id: str | None = None,
conn: Connection | None = None,
) -> Wallet:
wallet_id = uuid4().hex
wallet = Wallet(
id=wallet_id,
name=wallet_name or settings.lnbits_default_wallet_name,
+ wallet_type=wallet_type.value,
+ shared_wallet_id=shared_wallet_id,
user=user_id,
adminkey=uuid4().hex,
inkey=uuid4().hex,
currency=settings.lnbits_default_accounting_currency or "USD",
)
+
await (conn or db).insert("wallets", wallet)
return wallet
@@ -103,7 +108,7 @@ async def delete_unused_wallets(
)
-async def get_wallet(
+async def get_standalone_wallet(
wallet_id: str, deleted: bool | None = False, conn: Connection | None = None
) -> Wallet | None:
query = """
@@ -121,8 +126,23 @@ async def get_wallet(
)
+async def get_wallet(
+ wallet_id: str, deleted: bool | None = False, conn: Connection | None = None
+) -> Wallet | None:
+ wallet = await get_standalone_wallet(wallet_id, deleted, conn)
+ if not wallet:
+ return None
+ if wallet.is_lightning_shared_wallet:
+ return await get_source_wallet(wallet, conn)
+
+ return wallet
+
+
async def get_wallets(
- user_id: str, deleted: bool | None = False, conn: Connection | None = None
+ user_id: str,
+ deleted: bool | None = False,
+ wallet_type: WalletType | None = None,
+ conn: Connection | None = None,
) -> list[Wallet]:
query = """
SELECT *, COALESCE((
@@ -132,12 +152,20 @@ async def get_wallets(
"""
if deleted is not None:
query += " AND deleted = :deleted "
- return await (conn or db).fetchall(
+ if wallet_type is not None:
+ query += " AND wallet_type = :wallet_type "
+ wallets = await (conn or db).fetchall(
query,
- {"user": user_id, "deleted": deleted},
+ {
+ "user": user_id,
+ "deleted": deleted,
+ "wallet_type": wallet_type.value if wallet_type else None,
+ },
Wallet,
)
+ return await get_source_wallets(wallets, conn)
+
async def get_wallets_paginated(
user_id: str,
@@ -149,7 +177,7 @@ async def get_wallets_paginated(
deleted = False
where: list[str] = [""" "user" = :user AND deleted = :deleted """]
- return await (conn or db).fetch_page(
+ wallets = await (conn or db).fetch_page(
"""
SELECT *, COALESCE((
SELECT balance FROM balances WHERE wallet_id = wallets.id
@@ -161,18 +189,24 @@ async def get_wallets_paginated(
model=Wallet,
)
+ wallets.data = await get_source_wallets(wallets.data, conn)
+ return wallets
+
async def get_wallets_ids(
user_id: str, deleted: bool | None = False, conn: Connection | None = None
) -> list[str]:
- query = """SELECT id FROM wallets WHERE "user" = :user"""
+ query = """SELECT * FROM wallets WHERE "user" = :user"""
if deleted is not None:
- query += " AND deleted = :deleted"
- result: list[dict] = await (conn or db).fetchall(
+ query += " AND deleted = :deleted "
+ wallets = await (conn or db).fetchall(
query,
{"user": user_id, "deleted": deleted},
+ Wallet,
)
- return [row["id"] for row in result]
+
+ wallets = await get_source_wallets(wallets, conn)
+ return [w.source_wallet_id for w in wallets if w.can_view_payments]
async def get_wallets_count():
@@ -185,7 +219,7 @@ async def get_wallet_for_key(
key: str,
conn: Connection | None = None,
) -> Wallet | None:
- return await (conn or db).fetchone(
+ wallet = await (conn or db).fetchone(
"""
SELECT *, COALESCE((
SELECT balance FROM balances WHERE wallet_id = wallets.id
@@ -196,6 +230,39 @@ async def get_wallet_for_key(
{"key": key},
Wallet,
)
+ if not wallet:
+ return None
+
+ if wallet.is_lightning_shared_wallet:
+ mw = await get_source_wallet(wallet, conn)
+ return mw
+ return wallet
+
+
+async def get_source_wallet(
+ wallet: Wallet, conn: Connection | None = None
+) -> Wallet | None:
+ if not wallet.is_lightning_shared_wallet:
+ return wallet
+ if not wallet.shared_wallet_id:
+ return None
+
+ shared_wallet = await get_standalone_wallet(wallet.shared_wallet_id, False, conn)
+ if not shared_wallet:
+ return None
+ wallet.mirror_shared_wallet(shared_wallet)
+ return wallet
+
+
+async def get_source_wallets(
+ wallet: list[Wallet], conn: Connection | None = None
+) -> list[Wallet]:
+ source_wallets = []
+ for w in wallet:
+ source_wallet = await get_source_wallet(w, conn)
+ if source_wallet:
+ source_wallets.append(source_wallet)
+ return source_wallets
async def get_total_balance(conn: Connection | None = None):
diff --git a/lnbits/core/migrations.py b/lnbits/core/migrations.py
index 52d337a7..2a312df9 100644
--- a/lnbits/core/migrations.py
+++ b/lnbits/core/migrations.py
@@ -743,3 +743,19 @@ async def m034_add_stored_paylinks_to_wallet(db: Connection):
ALTER TABLE wallets ADD COLUMN stored_paylinks TEXT
"""
)
+
+
+async def m035_add_wallet_type_column(db: Connection):
+ await db.execute(
+ """
+ ALTER TABLE wallets ADD COLUMN wallet_type TEXT DEFAULT 'lightning'
+ """
+ )
+
+
+async def m036_add_shared_wallet_column(db: Connection):
+ await db.execute(
+ """
+ ALTER TABLE wallets ADD COLUMN shared_wallet_id TEXT
+ """
+ )
diff --git a/lnbits/core/models/users.py b/lnbits/core/models/users.py
index 0ad0988a..0bccb5b5 100644
--- a/lnbits/core/models/users.py
+++ b/lnbits/core/models/users.py
@@ -29,6 +29,13 @@ class UserNotifications(BaseModel):
incoming_payments_sats: int = 0
+class WalletInviteRequest(BaseModel):
+ request_id: str
+ from_user_name: str | None = None
+ to_wallet_id: str
+ to_wallet_name: str
+
+
class UserExtra(BaseModel):
email_verified: bool | None = False
first_name: str | None = None
@@ -46,6 +53,41 @@ class UserExtra(BaseModel):
notifications: UserNotifications = UserNotifications()
+ wallet_invite_requests: list[WalletInviteRequest] = []
+
+ def add_wallet_invite_request(
+ self,
+ request_id: str,
+ to_wallet_id: str,
+ to_wallet_name: str,
+ from_user_name: str | None = None,
+ ) -> WalletInviteRequest:
+ self.remove_wallet_invite_request(request_id)
+ invite = WalletInviteRequest(
+ request_id=request_id,
+ from_user_name=from_user_name,
+ to_wallet_id=to_wallet_id,
+ to_wallet_name=to_wallet_name,
+ )
+ self.wallet_invite_requests.append(invite)
+ return invite
+
+ def find_wallet_invite_request(self, request_id: str) -> WalletInviteRequest | None:
+ for invite in self.wallet_invite_requests:
+ if invite.request_id == request_id:
+ return invite
+ return None
+
+ def remove_wallet_invite_request(
+ self,
+ request_id: str,
+ ):
+ self.wallet_invite_requests = [
+ invite
+ for invite in self.wallet_invite_requests
+ if invite.request_id != request_id
+ ]
+
class EndpointAccess(BaseModel):
path: str
diff --git a/lnbits/core/models/wallets.py b/lnbits/core/models/wallets.py
index 217f7ad6..96fb56b8 100644
--- a/lnbits/core/models/wallets.py
+++ b/lnbits/core/models/wallets.py
@@ -21,10 +21,95 @@ class BaseWallet(BaseModel):
balance_msat: int
+class WalletType(Enum):
+ LIGHTNING = "lightning"
+ LIGHTNING_SHARED = "lightning-shared"
+
+
+class WalletPermission(Enum):
+ VIEW_PAYMENTS = "view-payments"
+ RECEIVE_PAYMENTS = "receive-payments"
+ SEND_PAYMENTS = "send-payments"
+
+ def __str__(self):
+ return self.value
+
+
+class WalletShareStatus(Enum):
+ INVITE_SENT = "invite_sent"
+ APPROVED = "approved"
+
+
+class WalletSharePermission(BaseModel):
+ # unique identifier for this share request
+ request_id: str | None = None
+ # username of the invited user
+ username: str
+ # ID of the wallet being shared with
+ shared_with_wallet_id: str | None = None
+ # permissions being granted
+ permissions: list[WalletPermission] = []
+ # status of the share request
+ status: WalletShareStatus
+ comment: str | None = None
+
+ def approve(
+ self,
+ permissions: list[WalletPermission] | None = None,
+ shared_with_wallet_id: str | None = None,
+ ):
+ self.status = WalletShareStatus.APPROVED
+ if permissions is not None:
+ self.permissions = permissions
+ if shared_with_wallet_id is not None:
+ self.shared_with_wallet_id = shared_with_wallet_id
+
+ @property
+ def is_approved(self) -> bool:
+ return self.status == WalletShareStatus.APPROVED
+
+
class WalletExtra(BaseModel):
icon: str = "flash_on"
color: str = "primary"
pinned: bool = False
+ # What permissions this wallet grants when it's shared with other users
+ shared_with: list[WalletSharePermission] = []
+
+ def invite_user_to_shared_wallet(
+ self,
+ request_id: str,
+ request_type: WalletShareStatus,
+ username: str,
+ permissions: list[WalletPermission] | None = None,
+ ) -> WalletSharePermission:
+ share = WalletSharePermission(
+ request_id=request_id,
+ username=username,
+ status=request_type,
+ permissions=permissions or [],
+ )
+ self.shared_with.append(share)
+ return share
+
+ def find_share_by_id(self, request_id: str) -> WalletSharePermission | None:
+ for share in self.shared_with:
+ if share.request_id == request_id:
+ return share
+ return None
+
+ def find_share_for_wallet(
+ self, shared_with_wallet_id: str
+ ) -> WalletSharePermission | None:
+ for share in self.shared_with:
+ if share.shared_with_wallet_id == shared_with_wallet_id:
+ return share
+ return None
+
+ def remove_share_by_id(self, request_id: str):
+ self.shared_with = [
+ share for share in self.shared_with if share.request_id != request_id
+ ]
class Wallet(BaseModel):
@@ -33,6 +118,9 @@ class Wallet(BaseModel):
name: str
adminkey: str
inkey: str
+ wallet_type: str = WalletType.LIGHTNING.value
+ # Must be set only for shared wallets
+ shared_wallet_id: str | None = None
deleted: bool = False
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
@@ -40,6 +128,65 @@ class Wallet(BaseModel):
balance_msat: int = Field(default=0, no_database=True)
extra: WalletExtra = WalletExtra()
stored_paylinks: StoredPayLinks = StoredPayLinks()
+ # What permission this wallet has when it's a shared wallet
+ share_permissions: list[WalletPermission] = Field(default=[], no_database=True)
+
+ def __init__(self, **data):
+ super().__init__(**data)
+ self._validate_data()
+
+ def mirror_shared_wallet(
+ self,
+ shared_wallet: Wallet,
+ ):
+ if not shared_wallet.is_lightning_wallet:
+ return None
+
+ self.wallet_type = WalletType.LIGHTNING_SHARED.value
+ self.shared_wallet_id = shared_wallet.id
+ self.name = shared_wallet.name
+ self.share_permissions = shared_wallet.get_share_permissions(self.id)
+
+ if len(self.share_permissions):
+ self.currency = shared_wallet.currency
+ self.balance_msat = shared_wallet.balance_msat
+
+ self.stored_paylinks = shared_wallet.stored_paylinks
+ self.extra.icon = shared_wallet.extra.icon
+ self.extra.color = shared_wallet.extra.color
+
+ def get_share_permissions(self, wallet_id: str) -> list[WalletPermission]:
+ for share in self.extra.shared_with:
+ if share.shared_with_wallet_id == wallet_id and share.is_approved:
+ return share.permissions
+ return []
+
+ def has_permission(self, permission: WalletPermission) -> bool:
+ if self.is_lightning_wallet:
+ return True
+ if self.is_lightning_shared_wallet:
+ return permission in self.share_permissions
+
+ return False
+
+ @property
+ def source_wallet_id(self) -> str:
+ """For shared wallets return the original wallet ID, else return own ID."""
+ if self.is_lightning_shared_wallet and len(self.share_permissions):
+ return self.shared_wallet_id or self.id
+ return self.id
+
+ @property
+ def can_receive_payments(self) -> bool:
+ return self.has_permission(WalletPermission.RECEIVE_PAYMENTS)
+
+ @property
+ def can_send_payments(self) -> bool:
+ return self.has_permission(WalletPermission.SEND_PAYMENTS)
+
+ @property
+ def can_view_payments(self) -> bool:
+ return self.has_permission(WalletPermission.VIEW_PAYMENTS)
@property
def balance(self) -> int:
@@ -57,9 +204,24 @@ class Wallet(BaseModel):
except Exception:
return ""
+ @property
+ def is_lightning_wallet(self) -> bool:
+ return self.wallet_type == WalletType.LIGHTNING.value
+
+ @property
+ def is_lightning_shared_wallet(self) -> bool:
+ return self.wallet_type == WalletType.LIGHTNING_SHARED.value
+
+ def _validate_data(self):
+ if self.is_lightning_shared_wallet:
+ if not self.shared_wallet_id:
+ raise ValueError("Shared wallet ID must be set for shared wallets.")
+
class CreateWallet(BaseModel):
name: str | None = None
+ wallet_type: WalletType = WalletType.LIGHTNING
+ shared_wallet_id: str | None = None
class KeyType(Enum):
diff --git a/lnbits/core/services/notifications.py b/lnbits/core/services/notifications.py
index 02c4a5e1..41c25ed3 100644
--- a/lnbits/core/services/notifications.py
+++ b/lnbits/core/services/notifications.py
@@ -16,6 +16,7 @@ from lnbits.core.crud import (
mark_webhook_sent,
)
from lnbits.core.crud.users import get_user
+from lnbits.core.crud.wallets import get_wallet
from lnbits.core.models import Payment, Wallet
from lnbits.core.models.notifications import (
NOTIFICATION_TEMPLATES,
@@ -257,6 +258,12 @@ async def dispatch_webhook(payment: Payment):
async def send_payment_notification(wallet: Wallet, payment: Payment):
try:
await send_ws_payment_notification(wallet, payment)
+ for shared in wallet.extra.shared_with:
+ if not shared.shared_with_wallet_id:
+ continue
+ shared_wallet = await get_wallet(shared.shared_with_wallet_id)
+ if shared_wallet and shared_wallet.can_view_payments:
+ await send_ws_payment_notification(shared_wallet, payment)
except Exception as e:
logger.error(f"Error sending websocket payment notification {e!s}")
try:
diff --git a/lnbits/core/services/payments.py b/lnbits/core/services/payments.py
index 7fdfcfff..d1a188f3 100644
--- a/lnbits/core/services/payments.py
+++ b/lnbits/core/services/payments.py
@@ -72,6 +72,11 @@ async def pay_invoice(
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
amount_msat = invoice.amount_msat
wallet = await _check_wallet_for_payment(wallet_id, tag, amount_msat, new_conn)
+ if not wallet.can_send_payments:
+ raise PaymentError(
+ "Wallet does not have permission to pay invoices.",
+ status="failed",
+ )
if await is_internal_status_success(invoice.payment_hash, new_conn):
raise PaymentError("Internal invoice already paid.", status="failed")
@@ -79,7 +84,7 @@ async def pay_invoice(
_, extra = await calculate_fiat_amounts(amount_msat / 1000, wallet, extra=extra)
create_payment_model = CreatePayment(
- wallet_id=wallet_id,
+ wallet_id=wallet.source_wallet_id,
bolt11=payment_request,
payment_hash=invoice.payment_hash,
amount_msat=-amount_msat,
@@ -88,7 +93,7 @@ async def pay_invoice(
extra=extra,
)
- payment = await _pay_invoice(wallet.id, create_payment_model, conn)
+ payment = await _pay_invoice(wallet.source_wallet_id, create_payment_model, conn)
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
await _credit_service_fee_wallet(wallet, payment, new_conn)
@@ -250,6 +255,12 @@ async def create_invoice(
if not user_wallet:
raise InvoiceError(f"Could not fetch wallet '{wallet_id}'.", status="failed")
+ if not user_wallet.can_receive_payments:
+ raise InvoiceError(
+ "Wallet does not have permission to create invoices.",
+ status="failed",
+ )
+
invoice_memo = None if description_hash else memo[:640]
# use the fake wallet if the invoice is for internal use only
@@ -308,7 +319,7 @@ async def create_invoice(
invoice = bolt11_decode(invoice_response.payment_request)
create_payment_model = CreatePayment(
- wallet_id=wallet_id,
+ wallet_id=user_wallet.source_wallet_id,
bolt11=invoice_response.payment_request,
payment_hash=invoice.payment_hash,
preimage=invoice_response.preimage,
@@ -456,7 +467,7 @@ async def update_wallet_balance(
await create_payment(
checking_id=f"internal_{payment_hash}",
data=CreatePayment(
- wallet_id=wallet.id,
+ wallet_id=wallet.source_wallet_id,
bolt11=bolt11,
payment_hash=payment_hash,
amount_msat=amount * 1000,
@@ -475,7 +486,7 @@ async def update_wallet_balance(
raise ValueError("Balance change failed, amount exceeds maximum balance.")
async with db.reuse_conn(conn) if conn else db.connect() as conn:
payment = await create_invoice(
- wallet_id=wallet.id,
+ wallet_id=wallet.source_wallet_id,
amount=amount,
memo="Admin credit",
internal=True,
@@ -910,7 +921,7 @@ async def _credit_service_fee_wallet(
memo = f"""
Service fee for payment of {abs(payment.sat)} sats.
- Wallet: '{wallet.name}' ({wallet.id})."""
+ Wallet: '{wallet.name}' ({wallet.source_wallet_id})."""
create_payment_model = CreatePayment(
wallet_id=settings.lnbits_service_fee_wallet,
diff --git a/lnbits/core/services/wallets.py b/lnbits/core/services/wallets.py
new file mode 100644
index 00000000..2b787203
--- /dev/null
+++ b/lnbits/core/services/wallets.py
@@ -0,0 +1,202 @@
+from lnbits.core.crud.users import (
+ get_account,
+ get_account_by_username_or_email,
+ update_account,
+)
+from lnbits.core.crud.wallets import (
+ create_wallet,
+ force_delete_wallet,
+ get_standalone_wallet,
+ get_wallet,
+ get_wallets,
+ update_wallet,
+)
+from lnbits.core.models.misc import SimpleStatus
+from lnbits.core.models.users import Account
+from lnbits.core.models.wallets import (
+ Wallet,
+ WalletSharePermission,
+ WalletShareStatus,
+ WalletType,
+)
+from lnbits.db import Connection
+from lnbits.helpers import sha256s
+
+
+async def invite_to_wallet(
+ source_wallet: Wallet, data: WalletSharePermission
+) -> WalletSharePermission:
+ if not source_wallet.is_lightning_wallet:
+ raise ValueError("Only lightning wallets can be shared.")
+ if not data.username:
+ raise ValueError("Username or email missing.")
+ invited_user = await get_account_by_username_or_email(data.username)
+ if not invited_user:
+ raise ValueError("Invited user not found.")
+
+ request_id = sha256s(invited_user.id + source_wallet.id)
+ share = source_wallet.extra.find_share_by_id(request_id)
+ if share:
+ raise ValueError("User already invited to this wallet.")
+
+ invite_request = source_wallet.extra.invite_user_to_shared_wallet(
+ request_id=request_id,
+ request_type=WalletShareStatus.INVITE_SENT,
+ username=data.username,
+ permissions=data.permissions,
+ )
+ await update_wallet(source_wallet)
+
+ wallet_owner = await get_account(source_wallet.user)
+ if not wallet_owner:
+ raise ValueError("Cannot find wallet owner.")
+ invited_user.extra.add_wallet_invite_request(
+ request_id=request_id,
+ from_user_name=wallet_owner.username or wallet_owner.email,
+ to_wallet_id=source_wallet.id,
+ to_wallet_name=source_wallet.name,
+ )
+ await update_account(invited_user)
+
+ return invite_request
+
+
+async def reject_wallet_invitation(invited_user_id: str, share_request_id: str):
+ invited_user = await get_account(invited_user_id)
+ if not invited_user:
+ raise ValueError("Invited user not found.")
+
+ existing_request = invited_user.extra.find_wallet_invite_request(share_request_id)
+ if not existing_request:
+ raise ValueError("Invitation not found.")
+
+ invited_user.extra.remove_wallet_invite_request(share_request_id)
+ await update_account(invited_user)
+
+
+async def update_wallet_share_permissions(
+ source_wallet: Wallet, data: WalletSharePermission
+) -> WalletSharePermission:
+ if not source_wallet.is_lightning_wallet:
+ raise ValueError("Only lightning wallets can be shared.")
+ if not data.shared_with_wallet_id:
+ raise ValueError("Wallet ID missing.")
+
+ share = source_wallet.extra.find_share_for_wallet(data.shared_with_wallet_id)
+ if not share:
+ raise ValueError("Share not found")
+
+ if not share.shared_with_wallet_id:
+ raise ValueError("Share does not have a mirror wallet ID.")
+
+ mirror_wallet = await get_wallet(share.shared_with_wallet_id)
+ if not mirror_wallet:
+ raise ValueError("Target wallet not found")
+ if not mirror_wallet.is_lightning_shared_wallet:
+ raise ValueError("Target wallet is not a shared wallet.")
+ if mirror_wallet.shared_wallet_id != source_wallet.id:
+ raise ValueError("Not the owner of the shared wallet.")
+
+ share.approve(permissions=data.permissions)
+ await update_wallet(source_wallet)
+ return share
+
+
+async def delete_wallet_share(source_wallet: Wallet, request_id: str) -> SimpleStatus:
+ if not source_wallet.is_lightning_wallet:
+ raise ValueError("Source wallet is not a lightning wallet.")
+
+ share = source_wallet.extra.find_share_by_id(request_id)
+ if not share:
+ raise ValueError("Wallet share not found.")
+ source_wallet.extra.remove_share_by_id(request_id)
+
+ invited_user = await get_account_by_username_or_email(share.username)
+ if not invited_user:
+ await update_wallet(source_wallet)
+ return SimpleStatus(
+ success=True, message="Permission removed. Invited user not found."
+ )
+ if invited_user.extra.find_wallet_invite_request(request_id):
+ invited_user.extra.remove_wallet_invite_request(request_id)
+ await update_account(invited_user)
+
+ mirror_wallets = await get_wallets(
+ invited_user.id, wallet_type=WalletType.LIGHTNING_SHARED
+ )
+ mirror_wallet = next(
+ (w for w in mirror_wallets if w.shared_wallet_id == source_wallet.id), None
+ )
+
+ if not mirror_wallet:
+ await update_wallet(source_wallet)
+ return SimpleStatus(
+ success=True, message="Permission removed. Target wallet not found."
+ )
+
+ if not mirror_wallet.is_lightning_shared_wallet:
+ raise ValueError("Target wallet is not a shared lightning wallet.")
+
+ if mirror_wallet.shared_wallet_id != source_wallet.id:
+ raise ValueError("Not the owner of the shared wallet.")
+
+ await force_delete_wallet(mirror_wallet.id)
+
+ await update_wallet(source_wallet)
+ return SimpleStatus(success=True, message="Permission removed.")
+
+
+async def create_lightning_shared_wallet(
+ user_id: str,
+ source_wallet_id: str,
+ conn: Connection | None = None,
+) -> Wallet:
+ source_wallet = await get_standalone_wallet(source_wallet_id, conn=conn)
+ if not source_wallet:
+ raise ValueError("Shared wallet does not exist.")
+
+ if not source_wallet.is_lightning_wallet:
+ raise ValueError("Shared wallet is not a lightning wallet.")
+
+ if source_wallet.user == user_id:
+ raise ValueError("Cannot mirror your own wallet.")
+
+ invited_user = await get_account(user_id, conn=conn)
+ if not invited_user:
+ raise ValueError("Cannot find invited user.")
+
+ return await _accept_invitation_to_shared_wallet(
+ invited_user, source_wallet, conn=conn
+ )
+
+
+async def _accept_invitation_to_shared_wallet(
+ invited_user: Account,
+ source_wallet: Wallet,
+ conn: Connection | None = None,
+) -> Wallet:
+ request_id = sha256s(invited_user.id + source_wallet.id)
+ existing_request = source_wallet.extra.find_share_by_id(request_id)
+ if not existing_request:
+ raise ValueError("No invitation found for this invited user.")
+ if existing_request.status == WalletShareStatus.APPROVED:
+ raise ValueError("This wallet is already shared with you.")
+ if existing_request.status != WalletShareStatus.INVITE_SENT:
+ raise ValueError("Unknown request type.")
+
+ invited_user.extra.remove_wallet_invite_request(request_id)
+ await update_account(invited_user)
+
+ # todo: double check if user already has a mirror wallet for this source wallet
+
+ mirror_wallet = await create_wallet(
+ user_id=invited_user.id,
+ wallet_name=source_wallet.name,
+ wallet_type=WalletType.LIGHTNING_SHARED,
+ shared_wallet_id=source_wallet.id,
+ conn=conn,
+ )
+ existing_request.approve(shared_with_wallet_id=mirror_wallet.id)
+ await update_wallet(source_wallet, conn=conn)
+ mirror_wallet.mirror_shared_wallet(source_wallet)
+ return mirror_wallet
diff --git a/lnbits/core/templates/core/_wallet_share.html b/lnbits/core/templates/core/_wallet_share.html
new file mode 100644
index 00000000..a32f49b3
--- /dev/null
+++ b/lnbits/core/templates/core/_wallet_share.html
@@ -0,0 +1,251 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ You can invite other users to have access to this wallet.
+
+ The access is limitted by the permission you grant.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This wallet is not shared with anyone.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No pending invites.
+
+
+
+
+
+
+
+
+
+
+ This wallet does not belong to you. It is a shared Lightning wallet.
+
+ The owner can revoke the permissions at any moment.
+
+
+
+
+
+ Shared Wallet ID:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Permissions:
+
+
+
+
+
+
+
+
+
+
+
+ Unknown wallet type:
+
+
+
+
diff --git a/lnbits/core/templates/core/wallet.html b/lnbits/core/templates/core/wallet.html
index 418e5629..9339155a 100644
--- a/lnbits/core/templates/core/wallet.html
+++ b/lnbits/core/templates/core/wallet.html
@@ -159,6 +159,7 @@
color="primary"
class="q-mr-md"
@click="showParseDialog"
+ :disable="!this.g.wallet.canSendPayments"
:label="$t('paste_request')"
>
+ {% include "core/_wallet_share.html" %}
+
diff --git a/lnbits/core/views/wallet_api.py b/lnbits/core/views/wallet_api.py
index 73560b47..ce6d81b6 100644
--- a/lnbits/core/views/wallet_api.py
+++ b/lnbits/core/views/wallet_api.py
@@ -8,10 +8,25 @@ from fastapi import (
HTTPException,
)
-from lnbits.core.crud.wallets import get_wallets_paginated
+from lnbits.core.crud.wallets import (
+ create_wallet,
+ get_wallets_paginated,
+)
from lnbits.core.models import CreateWallet, KeyType, User, Wallet, WalletTypeInfo
from lnbits.core.models.lnurl import StoredPayLink, StoredPayLinks
-from lnbits.core.models.wallets import WalletsFilters
+from lnbits.core.models.misc import SimpleStatus
+from lnbits.core.models.wallets import (
+ WalletsFilters,
+ WalletSharePermission,
+ WalletType,
+)
+from lnbits.core.services.wallets import (
+ create_lightning_shared_wallet,
+ delete_wallet_share,
+ invite_to_wallet,
+ reject_wallet_invitation,
+ update_wallet_share_permissions,
+)
from lnbits.db import Filters, Page
from lnbits.decorators import (
check_user_exists,
@@ -22,7 +37,6 @@ from lnbits.decorators import (
from lnbits.helpers import generate_filter_params_openapi
from ..crud import (
- create_wallet,
delete_wallet,
get_wallet,
update_wallet,
@@ -62,6 +76,35 @@ async def api_wallets_paginated(
return page
+@wallet_router.put("/share/invite")
+async def api_invite_wallet_share(
+ data: WalletSharePermission, key_info: WalletTypeInfo = Depends(require_admin_key)
+) -> WalletSharePermission:
+ return await invite_to_wallet(key_info.wallet, data)
+
+
+@wallet_router.delete("/share/invite/{share_request_id}")
+async def api_reject_wallet_invitation(
+ share_request_id: str, invited_user: User = Depends(check_user_exists)
+) -> SimpleStatus:
+ await reject_wallet_invitation(invited_user.id, share_request_id)
+ return SimpleStatus(success=True, message="Invitation rejected.")
+
+
+@wallet_router.put("/share")
+async def api_accept_wallet_share_request(
+ data: WalletSharePermission, key_info: WalletTypeInfo = Depends(require_admin_key)
+) -> WalletSharePermission:
+ return await update_wallet_share_permissions(key_info.wallet, data)
+
+
+@wallet_router.delete("/share/{share_request_id}")
+async def api_delete_wallet_share_permissions(
+ share_request_id: str, key_info: WalletTypeInfo = Depends(require_admin_key)
+) -> SimpleStatus:
+ return await delete_wallet_share(key_info.wallet, share_request_id)
+
+
@wallet_router.put("/{new_name}")
async def api_update_wallet_name(
new_name: str, key_info: WalletTypeInfo = Depends(require_admin_key)
@@ -70,6 +113,7 @@ async def api_update_wallet_name(
if not wallet:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found")
wallet.name = new_name
+
await update_wallet(wallet)
return {
"id": wallet.id,
@@ -124,6 +168,7 @@ async def api_update_wallet(
wallet.extra.color = color or wallet.extra.color
wallet.extra.pinned = pinned if pinned is not None else wallet.extra.pinned
wallet.currency = currency if currency is not None else wallet.currency
+
await update_wallet(wallet)
return wallet
@@ -147,4 +192,21 @@ async def api_create_wallet(
data: CreateWallet,
key_info: WalletTypeInfo = Depends(require_admin_key),
) -> Wallet:
- return await create_wallet(user_id=key_info.wallet.user, wallet_name=data.name)
+ if data.wallet_type == WalletType.LIGHTNING:
+ return await create_wallet(user_id=key_info.wallet.user, wallet_name=data.name)
+
+ if data.wallet_type == WalletType.LIGHTNING_SHARED:
+ if not data.shared_wallet_id:
+ raise HTTPException(
+ HTTPStatus.BAD_REQUEST,
+ "Shared wallet ID is required for shared wallets.",
+ )
+ return await create_lightning_shared_wallet(
+ user_id=key_info.wallet.user,
+ source_wallet_id=data.shared_wallet_id,
+ )
+
+ raise HTTPException(
+ HTTPStatus.BAD_REQUEST,
+ f"Unknown wallet type: {data.wallet_type}.",
+ )
diff --git a/lnbits/db.py b/lnbits/db.py
index 480ad10b..f6261543 100644
--- a/lnbits/db.py
+++ b/lnbits/db.py
@@ -594,6 +594,13 @@ class Filters(BaseModel, Generic[TFilterModel]):
return values
+class DbJsonEncoder(json.JSONEncoder):
+ def default(self, o):
+ if isinstance(o, Enum):
+ return o.value
+ return super().default(o)
+
+
def insert_query(table_name: str, model: BaseModel) -> str:
"""
Generate an insert query with placeholders for a given table and model
@@ -648,7 +655,7 @@ def model_to_dict(model: BaseModel) -> dict:
or type_ is dict
or get_origin(outertype_) is list
):
- _dict[key] = json.dumps(value)
+ _dict[key] = json.dumps(value, cls=DbJsonEncoder)
continue
_dict[key] = value
diff --git a/lnbits/helpers.py b/lnbits/helpers.py
index 167b695b..c54c5567 100644
--- a/lnbits/helpers.py
+++ b/lnbits/helpers.py
@@ -399,3 +399,11 @@ def is_snake_case(v: str) -> bool:
def lowercase_first_letter(s: str) -> str:
return s[:1].lower() + s[1:] if s else s
+
+
+def sha256s(value: str) -> str:
+ """
+ SHA256 applied on a string value.
+ Returns the hex as a string.
+ """
+ return hashlib.sha256(value.encode("utf-8")).hexdigest()
diff --git a/lnbits/static/bundle-components.min.js b/lnbits/static/bundle-components.min.js
index 421e9897..9188c5ab 100644
--- a/lnbits/static/bundle-components.min.js
+++ b/lnbits/static/bundle-components.min.js
@@ -1 +1 @@
-window.PagePayments={template:"#page-payments",mixins:[window.windowMixin],data:()=>({payments:[],dailyChartData:[],searchDate:{from:null,to:null},searchData:{wallet_id:null,payment_hash:null,memo:null,internal_memo:null},statusFilters:{success:!0,pending:!0,failed:!0,incoming:!0,outgoing:!0},chartData:{showPaymentStatus:!0,showPaymentTags:!0,showBalance:!0,showWalletsSize:!1,showBalanceInOut:!1,showPaymentCountInOut:!1},searchOptions:{status:[]},paymentsTable:{columns:[{name:"status",align:"left",label:"Status",field:"status",sortable:!1},{name:"created_at",align:"left",label:"Created At",field:"created_at",sortable:!0},{name:"amount",align:"right",label:"Amount",field:"amount",sortable:!0},{name:"amountFiat",align:"right",label:"Fiat",field:"amountFiat",sortable:!1},{name:"fee_sats",align:"left",label:"Fee",field:"fee_sats",sortable:!0},{name:"tag",align:"left",label:"Tag",field:"tag",sortable:!1},{name:"memo",align:"left",label:"Memo",field:"memo",sortable:!1,max_length:20},{name:"internal_memo",align:"left",label:"Internal Memo",field:"internal_memo",sortable:!1,max_length:20},{name:"wallet_id",align:"left",label:"Wallet (ID)",field:"wallet_id",sortable:!1},{name:"payment_hash",align:"left",label:"Payment Hash",field:"payment_hash",sortable:!1}],pagination:{sortBy:"created_at",rowsPerPage:25,page:1,descending:!0,rowsNumber:10},search:null,hideEmpty:!0,loading:!1},chartsReady:!1,showDetails:!1,paymentDetails:null,lnbitsBalance:0}),async mounted(){this.chartsReady=!0,await this.$nextTick(),this.initCharts(),await this.fetchPayments()},computed:{},methods:{async fetchPayments(t){const e=Object.entries(this.searchData).reduce(((t,[e,a])=>a?(t[e]=a,t):t),{});delete e["time[ge]"],delete e["time[le]"],this.searchDate.from&&(e["time[ge]"]=this.searchDate.from+"T00:00:00"),this.searchDate.to&&(e["time[le]"]=this.searchDate.to+"T23:59:59"),this.paymentsTable.filter=e;try{const e=LNbits.utils.prepareFilterQuery(this.paymentsTable,t),{data:a}=await LNbits.api.request("GET",`/api/v1/payments/all/paginated?${e}`);this.paymentsTable.pagination.rowsNumber=a.total,this.payments=a.data.map((t=>(t.extra&&t.extra.tag&&(t.tag=t.extra.tag),t.timeFrom=moment.utc(t.created_at).local().fromNow(),t.outgoing=t.amount<0,t.amount=new Intl.NumberFormat(window.LOCALE).format(t.amount/1e3)+" sats",t.extra?.wallet_fiat_amount&&(t.amountFiat=this.formatCurrency(t.extra.wallet_fiat_amount,t.extra.wallet_fiat_currency)),t.extra?.internal_memo&&(t.internal_memo=t.extra.internal_memo),t.fee_sats=new Intl.NumberFormat(window.LOCALE).format(t.fee/1e3)+" sats",t)))}catch(t){console.error(t),LNbits.utils.notifyApiError(t)}finally{this.updateCharts(t)}},async searchPaymentsBy(t,e){t&&(this.searchData[t]=e),await this.fetchPayments()},clearDateSeach(){this.searchDate={from:null,to:null},delete this.paymentsTable.filter["time[ge]"],delete this.paymentsTable.filter["time[le]"],this.fetchPayments()},searchByDate(){"string"==typeof this.searchDate&&(this.searchDate={from:this.searchDate,to:this.searchDate}),this.searchDate.from&&(this.paymentsTable.filter["time[ge]"]=this.searchDate.from+"T00:00:00"),this.searchDate.to&&(this.paymentsTable.filter["time[le]"]=this.searchDate.to+"T23:59:59"),this.fetchPayments()},handleFilterChanged(){const{success:t,pending:e,failed:a,incoming:s,outgoing:i}=this.statusFilters;delete this.searchData["status[ne]"],delete this.searchData["status[eq]"],t&&e&&a||(t&&e?this.searchData["status[ne]"]="failed":t&&a?this.searchData["status[ne]"]="pending":a&&e?this.searchData["status[ne]"]="success":t?this.searchData["status[eq]"]="success":e?this.searchData["status[eq]"]="pending":a&&(this.searchData["status[eq]"]="failed")),delete this.searchData["amount[ge]"],delete this.searchData["amount[le]"],s&&i||(s?this.searchData["amount[ge]"]="0":i&&(this.searchData["amount[le]"]="0")),this.fetchPayments()},showDetailsToggle(t){return this.paymentDetails=t,this.showDetails=!this.showDetails},formatDate:t=>LNbits.utils.formatDateString(t),formatCurrency(t,e){try{return LNbits.utils.formatCurrency(t,e)}catch(e){return console.error(e),`${t} ???`}},shortify:(t,e=10)=>(valueLength=(t||"").length,valueLength<=e?t:`${t.substring(0,5)}...${t.substring(valueLength-5,valueLength)}`),async updateCharts(t){let e=LNbits.utils.prepareFilterQuery(this.paymentsTable,t);try{const{data:t}=await LNbits.api.request("GET",`/api/v1/payments/stats/count?${e}&count_by=status`);t.sort(((t,e)=>t.field-e.field)).reverse(),this.searchOptions.status=t.map((t=>t.field)),this.paymentsStatusChart.data.datasets[0].data=t.map((t=>t.total)),this.paymentsStatusChart.data.labels=[...this.searchOptions.status],this.paymentsStatusChart.update()}catch(t){console.warn(t),LNbits.utils.notifyApiError(t)}try{const{data:t}=await LNbits.api.request("GET",`/api/v1/payments/stats/wallets?${e}`),a=t.map((t=>t.balance/t.payments_count)),s=Math.min(...a),i=Math.max(...a),n=t=>Math.floor(3+22*(t-s)/(i-s)),o=this.randomColors(20),l=t.map(((t,e)=>({data:[{x:t.payments_count,y:t.balance,r:n(Math.max(t.balance/t.payments_count,5))}],label:t.wallet_name,wallet_id:t.wallet_id,backgroundColor:o[e%100],hoverOffset:4})));this.paymentsWalletsChart.data.datasets=l,this.paymentsWalletsChart.update()}catch(t){console.warn(t),LNbits.utils.notifyApiError(t)}try{const{data:t}=await LNbits.api.request("GET",`/api/v1/payments/stats/count?${e}&count_by=tag`);this.searchOptions.tag=t.map((t=>t.field)),this.searchOptions.status.sort(),this.paymentsTagsChart.data.datasets[0].data=t.map((t=>t.total)),this.paymentsTagsChart.data.labels=t.map((t=>t.field||"core")),this.paymentsTagsChart.update()}catch(t){console.warn(t),LNbits.utils.notifyApiError(t)}try{const e=Object.entries(this.searchData).reduce(((t,[e,a])=>a?(t[e]=a,t):t),{}),a={...this.paymentsTable,filter:e},s=LNbits.utils.prepareFilterQuery(a,t);let{data:i}=await LNbits.api.request("GET",`/api/v1/payments/stats/daily?${s}`);const n=this.searchDate.from+"T00:00:00",o=this.searchDate.to+"T23:59:59";this.lnbitsBalance=i.length?i[i.length-1].balance:0,i=i.filter((t=>this.searchDate.from&&this.searchDate.to?t.date>=n&&t.date<=o:this.searchDate.from?t.date>=n:!this.searchDate.to||t.date<=o)),this.paymentsDailyChart.data.datasets=[{label:"Balance",data:i.map((t=>t.balance)),pointStyle:!1,borderWidth:2,tension:.7,fill:1},{label:"Fees",data:i.map((t=>t.fee)),pointStyle:!1,borderWidth:1,tension:.4,fill:1}],this.paymentsDailyChart.data.labels=i.map((t=>t.date.substring(0,10))),this.paymentsDailyChart.update(),this.paymentsBalanceInOutChart.data.datasets=[{label:"Incoming Payments Balance",data:i.map((t=>t.balance_in))},{label:"Outgoing Payments Balance",data:i.map((t=>t.balance_out))}],this.paymentsBalanceInOutChart.data.labels=i.map((t=>t.date.substring(0,10))),this.paymentsBalanceInOutChart.update(),this.paymentsCountInOutChart.data.datasets=[{label:"Incoming Payments Count",data:i.map((t=>t.count_in))},{label:"Outgoing Payments Count",data:i.map((t=>-t.count_out))}],this.paymentsCountInOutChart.data.labels=i.map((t=>t.date.substring(0,10))),this.paymentsCountInOutChart.update()}catch(t){console.warn(t),LNbits.utils.notifyApiError(t)}},async initCharts(){const t=this.$q.localStorage.getItem("lnbits.payments.chartData")||{};this.chartData={...this.chartData,...t},this.chartsReady?(this.paymentsStatusChart=new Chart(this.$refs.paymentsStatusChart.getContext("2d"),{type:"doughnut",options:{responsive:!0,maintainAspectRatio:!1,plugins:{title:{display:!1}},onClick:(t,e,a)=>{if(e[0]){const t=e[0].index;this.searchPaymentsBy("status",a.data.labels[t])}}},data:{datasets:[{label:"",data:[],backgroundColor:["rgb(0, 205, 86)","rgb(64, 72, 78)","rgb(255, 99, 132)"],hoverOffset:4}]}}),this.paymentsWalletsChart=new Chart(this.$refs.paymentsWalletsChart.getContext("2d"),{type:"bubble",options:{responsive:!0,maintainAspectRatio:!1,plugins:{legend:{display:!1},title:{display:!1}},onClick:(t,e,a)=>{if(e[0]){const t=e[0].datasetIndex;this.searchPaymentsBy("wallet_id",a.data.datasets[t].wallet_id)}}},data:{datasets:[{label:"",data:[],backgroundColor:this.randomColors(20),hoverOffset:4}]}}),this.paymentsTagsChart=new Chart(this.$refs.paymentsTagsChart.getContext("2d"),{type:"pie",options:{responsive:!0,maintainAspectRatio:!1,plugins:{title:{display:!1},legend:{display:!1,title:{display:!1,text:"Tags"}}},onClick:(t,e,a)=>{if(e[0]){const t=e[0].index;this.searchPaymentsBy("tag",a.data.labels[t])}}},data:{datasets:[{label:"",data:[],backgroundColor:this.randomColors(10),hoverOffset:4}]}}),this.paymentsDailyChart=new Chart(this.$refs.paymentsDailyChart.getContext("2d"),{type:"line",options:{responsive:!0,maintainAspectRatio:!1,plugins:{title:{display:!1},legend:{display:!0,title:{display:!1,text:"Tags"}}}},data:{datasets:[{label:"",data:[],backgroundColor:this.randomColors(10),hoverOffset:4}]}}),this.paymentsBalanceInOutChart=new Chart(this.$refs.paymentsBalanceInOutChart.getContext("2d"),{type:"bar",options:{responsive:!0,maintainAspectRatio:!1,plugins:{title:{display:!1},legend:{display:!0,title:{display:!1,text:"Tags"}}},scales:{x:{stacked:!0},y:{stacked:!0}}},data:{datasets:[{label:"",data:[],backgroundColor:this.randomColors(50),hoverOffset:4}]}}),this.paymentsCountInOutChart=new Chart(this.$refs.paymentsCountInOutChart.getContext("2d"),{type:"bar",options:{responsive:!0,maintainAspectRatio:!1,plugins:{title:{display:!1},legend:{display:!0,title:{display:!1,text:""}}},scales:{x:{stacked:!0},y:{stacked:!0}}},data:{datasets:[{label:"",data:[],backgroundColor:this.randomColors(80),hoverOffset:4}]}})):console.warn("Charts are not ready yet. Initialization delayed.")},saveChartsPreferences(){this.$q.localStorage.set("lnbits.payments.chartData",this.chartData)},randomColors(t=1){const e=[];for(let a=1;a<=10;a++)for(let s=1;s<=10;s++)e.push(`rgb(${s*t*33%200}, ${71*(a+s+t)%255}, ${(a+30*t)%255})`);return e}}},window.PageNode={mixins:[window.windowMixin],template:"#page-node",config:{globalProperties:{LNbits:LNbits,msg:"hello"}},data(){return{isSuperUser:!1,wallet:{},tab:"dashboard",payments:1e3,info:{},channel_stats:{},channels:{data:[],filter:""},activeBalance:{},ranks:{},peers:{data:[],filter:""},connectPeerDialog:{show:!1,data:{}},setFeeDialog:{show:!1,data:{fee_ppm:0,fee_base_msat:0}},openChannelDialog:{show:!1,data:{}},closeChannelDialog:{show:!1,data:{}},nodeInfoDialog:{show:!1,data:{}},transactionDetailsDialog:{show:!1,data:{}},states:[{label:"Active",value:"active",color:"green"},{label:"Pending",value:"pending",color:"orange"},{label:"Inactive",value:"inactive",color:"grey"},{label:"Closed",value:"closed",color:"red"}],stateFilters:[{label:"Active",value:"active"},{label:"Pending",value:"pending"}],paymentsTable:{data:[],columns:[{name:"pending",label:""},{name:"date",align:"left",label:this.$t("date"),field:"date",sortable:!0},{name:"sat",align:"right",label:this.$t("amount")+" ("+LNBITS_DENOMINATION+")",field:t=>this.formatMsat(t.amount),sortable:!0},{name:"fee",align:"right",label:this.$t("fee")+" (m"+LNBITS_DENOMINATION+")",field:"fee"},{name:"destination",align:"right",label:"Destination",field:"destination"},{name:"memo",align:"left",label:this.$t("memo"),field:"memo"}],pagination:{rowsPerPage:10,page:1,rowsNumber:10},filter:null},invoiceTable:{data:[],columns:[{name:"pending",label:""},{name:"paid_at",field:"paid_at",align:"left",label:"Paid at",sortable:!0},{name:"expiry",label:this.$t("expiry"),field:"expiry",align:"left",sortable:!0},{name:"amount",label:this.$t("amount")+" ("+LNBITS_DENOMINATION+")",field:t=>this.formatMsat(t.amount),sortable:!0},{name:"memo",align:"left",label:this.$t("memo"),field:"memo"}],pagination:{rowsPerPage:10,page:1,rowsNumber:10},filter:null}}},created(){this.getInfo(),this.get1MLStats()},watch:{tab(t){"transactions"!==t||this.paymentsTable.data.length?"channels"!==t||this.channels.data.length||(this.getChannels(),this.getPeers()):(this.getPayments(),this.getInvoices())}},computed:{checkChanges(){return!_.isEqual(this.settings,this.formData)},filteredChannels(){return this.stateFilters?this.channels.data.filter((t=>this.stateFilters.find((({value:e})=>e==t.state)))):this.channels.data},totalBalance(){return this.filteredChannels.reduce(((t,e)=>(t.local_msat+=e.balance.local_msat,t.remote_msat+=e.balance.remote_msat,t.total_msat+=e.balance.total_msat,t)),{local_msat:0,remote_msat:0,total_msat:0})}},methods:{formatMsat:t=>LNbits.utils.formatMsat(t),api(t,e,a){const s=new URLSearchParams(a?.query);return LNbits.api.request(t,`/node/api/v1${e}?${s}`,{},a?.data).catch((t=>{LNbits.utils.notifyApiError(t)}))},getChannel(t){return this.api("GET",`/channels/${t}`).then((t=>{this.setFeeDialog.data.fee_ppm=t.data.fee_ppm,this.setFeeDialog.data.fee_base_msat=t.data.fee_base_msat}))},getChannels(){return this.api("GET","/channels").then((t=>{this.channels.data=t.data}))},getInfo(){return this.api("GET","/info").then((t=>{this.info=t.data,this.channel_stats=t.data.channel_stats})).catch((()=>{this.info={},this.channel_stats={}}))},get1MLStats(){return this.api("GET","/rank").then((t=>{this.ranks=t.data})).catch((()=>{this.ranks={}}))},getPayments(t){t&&(this.paymentsTable.pagination=t.pagination);let e=this.paymentsTable.pagination;const a={limit:e.rowsPerPage,offset:(e.page-1)*e.rowsPerPage??0};return this.api("GET","/payments",{query:a}).then((t=>{this.paymentsTable.data=t.data.data,this.paymentsTable.pagination.rowsNumber=t.data.total}))},getInvoices(t){t&&(this.invoiceTable.pagination=t.pagination);let e=this.invoiceTable.pagination;const a={limit:e.rowsPerPage,offset:(e.page-1)*e.rowsPerPage??0};return this.api("GET","/invoices",{query:a}).then((t=>{this.invoiceTable.data=t.data.data,this.invoiceTable.pagination.rowsNumber=t.data.total}))},getPeers(){return this.api("GET","/peers").then((t=>{this.peers.data=t.data}))},connectPeer(){this.api("POST","/peers",{data:this.connectPeerDialog.data}).then((()=>{this.connectPeerDialog.show=!1,this.getPeers()}))},disconnectPeer(t){LNbits.utils.confirmDialog("Do you really wanna disconnect this peer?").onOk((()=>{this.api("DELETE",`/peers/${t}`).then((t=>{Quasar.Notify.create({message:"Disconnected",icon:null}),this.needsRestart=!0,this.getPeers()}))}))},setChannelFee(t){this.api("PUT",`/channels/${t}`,{data:this.setFeeDialog.data}).then((t=>{this.setFeeDialog.show=!1,this.getChannels()})).catch(LNbits.utils.notifyApiError)},openChannel(){this.api("POST","/channels",{data:this.openChannelDialog.data}).then((t=>{this.openChannelDialog.show=!1,this.getChannels()})).catch((t=>{console.log(t)}))},showCloseChannelDialog(t){this.closeChannelDialog.show=!0,this.closeChannelDialog.data={force:!1,short_id:t.short_id,...t.point}},closeChannel(){this.api("DELETE","/channels",{query:this.closeChannelDialog.data}).then((t=>{this.closeChannelDialog.show=!1,this.getChannels()}))},showSetFeeDialog(t){this.setFeeDialog.show=!0,this.setFeeDialog.channel_id=t,this.getChannel(t)},showOpenChannelDialog(t){this.openChannelDialog.show=!0,this.openChannelDialog.data={peer_id:t,funding_amount:0}},showNodeInfoDialog(t){this.nodeInfoDialog.show=!0,this.nodeInfoDialog.data=t},showTransactionDetailsDialog(t){this.transactionDetailsDialog.show=!0,this.transactionDetailsDialog.data=t},shortenNodeId:t=>t?t.substring(0,5)+"..."+t.substring(t.length-5):"..."}},window.PageNodePublic={template:"#page-node-public",mixins:[window.windowMixin],data:()=>({enabled:!1,isSuperUser:!1,wallet:{},tab:"dashboard",payments:1e3,info:{},channel_stats:{},channels:[],activeBalance:{},ranks:{},peers:[],connectPeerDialog:{show:!1,data:{}},openChannelDialog:{show:!1,data:{}},closeChannelDialog:{show:!1,data:{}},nodeInfoDialog:{show:!1,data:{}},states:[{label:"Active",value:"active",color:"green"},{label:"Pending",value:"pending",color:"orange"},{label:"Inactive",value:"inactive",color:"grey"},{label:"Closed",value:"closed",color:"red"}]}),created(){this.getInfo(),this.get1MLStats()},methods:{formatMsat:t=>LNbits.utils.formatMsat(t),api:(t,e,a)=>LNbits.api.request(t,"/node/public/api/v1"+e,{},a),getInfo(){this.api("GET","/info",{}).then((t=>{this.info=t.data,this.channel_stats=t.data.channel_stats,this.enabled=!0})).catch((()=>{this.info={},this.channel_stats={}}))},get1MLStats(){this.api("GET","/rank",{}).then((t=>{this.ranks=t.data})).catch((()=>{this.ranks={}}))}}},window.PageAudit={template:"#page-audit",mixins:[window.windowMixin],data:()=>({chartsReady:!1,auditEntries:[],searchData:{user_id:"",ip_address:"",request_type:"",component:"",request_method:"",response_code:"",path:""},searchOptions:{component:[],request_method:[],response_code:[]},auditTable:{columns:[{name:"created_at",align:"center",label:"Date",field:"created_at",sortable:!0},{name:"duration",align:"left",label:"Duration (sec)",field:"duration",sortable:!0},{name:"component",align:"left",label:"Component",field:"component",sortable:!1},{name:"request_method",align:"left",label:"Method",field:"request_method",sortable:!1},{name:"response_code",align:"left",label:"Code",field:"response_code",sortable:!1},{name:"user_id",align:"left",label:"User Id",field:"user_id",sortable:!1},{name:"ip_address",align:"left",label:"IP Address",field:"ip_address",sortable:!1},{name:"path",align:"left",label:"Path",field:"path",sortable:!1}],pagination:{sortBy:"created_at",rowsPerPage:10,page:1,descending:!0,rowsNumber:10},search:null,hideEmpty:!0,loading:!1},auditDetailsDialog:{data:null,show:!1}}),async created(){},async mounted(){this.chartsReady=!0,await this.$nextTick(),this.initCharts(),await this.fetchAudit()},methods:{formatDate:t=>LNbits.utils.formatDateString(t),async fetchAudit(t){try{const e=LNbits.utils.prepareFilterQuery(this.auditTable,t),{data:a}=await LNbits.api.request("GET",`/audit/api/v1?${e}`);this.auditTable.pagination.rowsNumber=a.total,this.auditEntries=a.data,await this.fetchAuditStats(t)}catch(t){console.warn(t),LNbits.utils.notifyApiError(t)}finally{this.auditTable.loading=!1}},async fetchAuditStats(t){try{const e=LNbits.utils.prepareFilterQuery(this.auditTable,t),{data:a}=await LNbits.api.request("GET",`/audit/api/v1/stats?${e}`),s=a.request_method.map((t=>t.field));this.searchOptions.request_method=[...new Set(this.searchOptions.request_method.concat(s))],this.requestMethodChart.data.labels=s,this.requestMethodChart.data.datasets[0].data=a.request_method.map((t=>t.total)),this.requestMethodChart.update();const i=a.response_code.map((t=>t.field));this.searchOptions.response_code=[...new Set(this.searchOptions.response_code.concat(i))],this.responseCodeChart.data.labels=i,this.responseCodeChart.data.datasets[0].data=a.response_code.map((t=>t.total)),this.responseCodeChart.update();const n=a.component.map((t=>t.field));this.searchOptions.component=[...new Set(this.searchOptions.component.concat(n))],this.componentUseChart.data.labels=n,this.componentUseChart.data.datasets[0].data=a.component.map((t=>t.total)),this.componentUseChart.update(),this.longDurationChart.data.labels=a.long_duration.map((t=>t.field)),this.longDurationChart.data.datasets[0].data=a.long_duration.map((t=>t.total)),this.longDurationChart.update()}catch(t){console.warn(t),LNbits.utils.notifyApiError(t)}},async searchAuditBy(t,e){t&&(this.searchData[t]=e),this.auditTable.filter=Object.entries(this.searchData).reduce(((t,[e,a])=>a?(t[e]=a,t):t),{}),await this.fetchAudit()},showDetailsDialog(t){const e=JSON.parse(t?.request_details||"");try{e.body&&(e.body=JSON.parse(e.body))}catch(t){}this.auditDetailsDialog.data=JSON.stringify(e,null,4),this.auditDetailsDialog.show=!0},shortify:t=>(valueLength=(t||"").length,valueLength<=10?t:`${t.substring(0,5)}...${t.substring(valueLength-5,valueLength)}`),async initCharts(){this.chartsReady?(this.responseCodeChart=new Chart(this.$refs.responseCodeChart.getContext("2d"),{type:"doughnut",options:{responsive:!0,plugins:{legend:{position:"bottom"},title:{display:!1,text:"HTTP Response Codes"}},onClick:(t,e,a)=>{if(e[0]){const t=e[0].index;this.searchAuditBy("response_code",a.data.labels[t])}}},data:{datasets:[{label:"",data:[20,10],backgroundColor:["rgb(100, 99, 200)","rgb(54, 162, 235)","rgb(255, 205, 86)","rgb(255, 5, 86)","rgb(25, 205, 86)","rgb(255, 205, 250)"]}],labels:[]}}),this.requestMethodChart=new Chart(this.$refs.requestMethodChart.getContext("2d"),{type:"bar",options:{responsive:!0,maintainAspectRatio:!1,plugins:{title:{display:!1}},onClick:(t,e,a)=>{if(e[0]){const t=e[0].index;this.searchAuditBy("request_method",a.data.labels[t])}}},data:{datasets:[{label:"",data:[],backgroundColor:["rgb(255, 99, 132)","rgb(54, 162, 235)","rgb(255, 205, 86)","rgb(255, 5, 86)","rgb(25, 205, 86)","rgb(255, 205, 250)"],hoverOffset:4}]}}),this.componentUseChart=new Chart(this.$refs.componentUseChart.getContext("2d"),{type:"pie",options:{responsive:!0,plugins:{legend:{position:"xxx"},title:{display:!1,text:"Components"}},onClick:(t,e,a)=>{if(e[0]){const t=e[0].index;this.searchAuditBy("component",a.data.labels[t])}}},data:{datasets:[{data:[],backgroundColor:["rgb(255, 99, 132)","rgb(54, 162, 235)","rgb(255, 205, 86)","rgb(255, 5, 86)","rgb(25, 205, 86)","rgb(255, 205, 250)","rgb(100, 205, 250)","rgb(120, 205, 250)","rgb(140, 205, 250)","rgb(160, 205, 250)"],hoverOffset:4}]}}),this.longDurationChart=new Chart(this.$refs.longDurationChart.getContext("2d"),{type:"bar",options:{responsive:!0,indexAxis:"y",maintainAspectRatio:!1,plugins:{legend:{title:{display:!1,text:"Long Duration"}}},onClick:(t,e,a)=>{if(e[0]){const t=e[0].index;this.searchAuditBy("path",a.data.labels[t])}}},data:{datasets:[{label:"",data:[],backgroundColor:["rgb(255, 99, 132)","rgb(54, 162, 235)","rgb(255, 205, 86)","rgb(255, 5, 86)","rgb(25, 205, 86)","rgb(255, 205, 250)","rgb(100, 205, 250)","rgb(120, 205, 250)","rgb(140, 205, 250)","rgb(160, 205, 250)"],hoverOffset:4}]}})):console.warn("Charts are not ready yet. Initialization delayed.")}}},window.PageWallets={template:"#page-wallets",mixins:[window.windowMixin],data:()=>({user:null,tab:"wallets",wallets:[],showAddWalletDialog:{show:!1},walletsTable:{columns:[{name:"name",align:"left",label:"Name",field:"name",sortable:!0},{name:"currency",align:"center",label:"Currency",field:"currency",sortable:!0},{name:"updated_at",align:"right",label:"Last Updated",field:"updated_at",sortable:!0}],pagination:{sortBy:"updated_at",rowsPerPage:12,page:1,descending:!0,rowsNumber:10},search:"",hideEmpty:!0,loading:!1}}),watch:{"walletsTable.search":{handler(){const t={};this.walletsTable.search&&(t.search=this.walletsTable.search),this.getUserWallets()}}},methods:{async getUserWallets(t){try{this.walletsTable.loading=!0;const e=LNbits.utils.prepareFilterQuery(this.walletsTable,t),{data:a}=await LNbits.api.request("GET",`/api/v1/wallet/paginated?${e}`,null);this.wallets=a.data,this.walletsTable.pagination.rowsNumber=a.total}catch(t){LNbits.utils.notifyApiError(t)}finally{this.walletsTable.loading=!1}},goToWallet(t){window.location=`/wallet?wal=${t}`},formattedFiatAmount:(t,e)=>LNbits.utils.formatCurrency(Number(t).toFixed(2),e),formattedSatAmount:t=>LNbits.utils.formatMsat(t)+" sat"},async created(){await this.getUserWallets()}},window.PageUsers={template:"#page-users",mixins:[window.windowMixin],data:()=>({paymentsWallet:{},cancel:{},users:[],wallets:[],searchData:{user:"",username:"",email:"",pubkey:""},paymentPage:{show:!1},activeWallet:{userId:null,show:!1},activeUser:{data:null,showUserId:!1,show:!1},createWalletDialog:{data:{},show:!1},walletTable:{columns:[{name:"name",align:"left",label:"Name",field:"name"},{name:"id",align:"left",label:"Wallet Id",field:"id"},{name:"currency",align:"left",label:"Currency",field:"currency"},{name:"balance_msat",align:"left",label:"Balance",field:"balance_msat"}],pagination:{sortBy:"name",rowsPerPage:10,page:1,descending:!0,rowsNumber:10},search:null,hideEmpty:!0,loading:!1},usersTable:{columns:[{name:"admin",align:"left",label:"Admin",field:"admin",sortable:!1},{name:"wallet_id",align:"left",label:"Wallets",field:"wallet_id",sortable:!1},{name:"user",align:"left",label:"User Id",field:"user",sortable:!1},{name:"username",align:"left",label:"Username",field:"username",sortable:!1},{name:"email",align:"left",label:"Email",field:"email",sortable:!1},{name:"pubkey",align:"left",label:"Public Key",field:"pubkey",sortable:!1},{name:"balance_msat",align:"left",label:"Balance",field:"balance_msat",sortable:!0},{name:"transaction_count",align:"left",label:"Payments",field:"transaction_count",sortable:!0},{name:"last_payment",align:"left",label:"Last Payment",field:"last_payment",sortable:!0}],pagination:{sortBy:"balance_msat",rowsPerPage:10,page:1,descending:!0,rowsNumber:10},search:null,hideEmpty:!0,loading:!1}}),watch:{"usersTable.hideEmpty":function(t,e){this.usersTable.filter=t?{"transaction_count[gt]":0}:{},this.fetchUsers()}},created(){this.fetchUsers()},methods:{formatDate:t=>LNbits.utils.formatDateString(t),formatSat:t=>LNbits.utils.formatSat(Math.floor(t/1e3)),backToUsersPage(){this.activeUser.show=!1,this.paymentPage.show=!1,this.activeWallet.show=!1,this.fetchUsers()},handleBalanceUpdate(){this.fetchWallets(this.activeWallet.userId)},resetPassword(t){return LNbits.api.request("PUT",`/users/api/v1/user/${t}/reset_password`).then((t=>{LNbits.utils.confirmDialog(this.$t("reset_key_generated")+" "+this.$t("reset_key_copy")).onOk((()=>{const e=window.location.origin+"?reset_key="+t.data;this.copyText(e)}))})).catch(LNbits.utils.notifyApiError)},createUser(){LNbits.api.request("POST","/users/api/v1/user",null,this.activeUser.data).then((t=>{Quasar.Notify.create({type:"positive",message:"User created!",icon:null}),this.activeUser.setPassword=!0,this.activeUser.data=t.data,this.fetchUsers()})).catch(LNbits.utils.notifyApiError)},updateUser(){LNbits.api.request("PUT",`/users/api/v1/user/${this.activeUser.data.id}`,null,this.activeUser.data).then((()=>{Quasar.Notify.create({type:"positive",message:"User updated!",icon:null}),this.activeUser.data=null,this.activeUser.show=!1,this.fetchUsers()})).catch(LNbits.utils.notifyApiError)},createWallet(){const t=this.activeWallet.userId;t?LNbits.api.request("POST",`/users/api/v1/user/${t}/wallet`,null,this.createWalletDialog.data).then((()=>{this.fetchWallets(t),Quasar.Notify.create({type:"positive",message:"Wallet created!"})})).catch(LNbits.utils.notifyApiError):Quasar.Notify.create({type:"warning",message:"No user selected!",icon:null})},deleteUser(t){LNbits.utils.confirmDialog("Are you sure you want to delete this user?").onOk((()=>{LNbits.api.request("DELETE",`/users/api/v1/user/${t}`).then((()=>{this.fetchUsers(),Quasar.Notify.create({type:"positive",message:"User deleted!",icon:null}),this.activeUser.data=null,this.activeUser.show=!1})).catch(LNbits.utils.notifyApiError)}))},undeleteUserWallet(t,e){LNbits.api.request("PUT",`/users/api/v1/user/${t}/wallet/${e}/undelete`).then((()=>{this.fetchWallets(t),Quasar.Notify.create({type:"positive",message:"Undeleted user wallet!",icon:null})})).catch(LNbits.utils.notifyApiError)},deleteUserWallet(t,e,a){const s=a?"Wallet is already deleted, are you sure you want to permanently delete this user wallet?":"Are you sure you want to delete this user wallet?";LNbits.utils.confirmDialog(s).onOk((()=>{LNbits.api.request("DELETE",`/users/api/v1/user/${t}/wallet/${e}`).then((()=>{this.fetchWallets(t),Quasar.Notify.create({type:"positive",message:"User wallet deleted!",icon:null})})).catch(LNbits.utils.notifyApiError)}))},deleteAllUserWallets(t){LNbits.utils.confirmDialog(this.$t("confirm_delete_all_wallets")).onOk((()=>{LNbits.api.request("DELETE",`/users/api/v1/user/${t}/wallets`).then((e=>{Quasar.Notify.create({type:"positive",message:e.data.message,icon:null}),this.fetchWallets(t)})).catch(LNbits.utils.notifyApiError)}))},copyWalletLink(t){const e=`${window.location.origin}/wallet?usr=${this.activeWallet.userId}&wal=${t}`;this.copyText(e)},fetchUsers(t){this.relaxFilterForFields(["username","email"]);const e=LNbits.utils.prepareFilterQuery(this.usersTable,t);LNbits.api.request("GET",`/users/api/v1/user?${e}`).then((t=>{this.usersTable.loading=!1,this.usersTable.pagination.rowsNumber=t.data.total,this.users=t.data.data})).catch(LNbits.utils.notifyApiError)},fetchWallets(t){return LNbits.api.request("GET",`/users/api/v1/user/${t}/wallet`).then((e=>{this.wallets=e.data,this.activeWallet.userId=t,this.activeWallet.show=!0})).catch(LNbits.utils.notifyApiError)},relaxFilterForFields(t=[]){t.forEach((t=>{const e=this.usersTable?.filter?.[t];e&&this.usersTable.filter[t]&&(this.usersTable.filter[`${t}[like]`]=e,delete this.usersTable.filter[t])}))},updateWallet(t){LNbits.api.request("PATCH","/api/v1/wallet",t.adminkey,{name:t.name}).then((()=>{t.editable=!1,Quasar.Notify.create({message:"Wallet name updated.",type:"positive",timeout:3500})})).catch((t=>{LNbits.utils.notifyApiError(t)}))},toggleAdmin(t){LNbits.api.request("GET",`/users/api/v1/user/${t}/admin`).then((()=>{this.fetchUsers(),Quasar.Notify.create({type:"positive",message:"Toggled admin!",icon:null})})).catch(LNbits.utils.notifyApiError)},async showAccountPage(t){if(this.activeUser.showPassword=!1,this.activeUser.showUserId=!1,this.activeUser.setPassword=!1,!t)return this.activeUser.data={extra:{}},void(this.activeUser.show=!0);try{const{data:e}=await LNbits.api.request("GET",`/users/api/v1/user/${t}`);this.activeUser.data=e,this.activeUser.show=!0}catch(t){console.warn(t),Quasar.Notify.create({type:"warning",message:"Failed to get user!"}),this.activeUser.show=!1}},async showWalletPayments(t){this.activeUser.show=!1,await this.fetchWallets(this.users[0].id),await this.showPayments(t)},showPayments(t){this.paymentsWallet=this.wallets.find((e=>e.id===t)),this.paymentPage.show=!0},searchUserBy(t){const e=this.searchData[t];this.usersTable.filter={},e&&(this.usersTable.filter[t]=e),this.fetchUsers()},shortify:t=>(valueLength=(t||"").length,valueLength<=10?t:`${t.substring(0,5)}...${t.substring(valueLength-5,valueLength)}`)}},window.PageAccount={template:"#page-account",mixins:[window.windowMixin],data(){return{user:null,hasUsername:!1,showUserId:!1,reactionOptions:["None","confettiBothSides","confettiFireworks","confettiStars","confettiTop"],borderOptions:["retro-border","hard-border","neon-border","no-border"],tab:"user",credentialsData:{show:!1,oldPassword:null,newPassword:null,newPasswordRepeat:null,username:null,pubkey:null},apiAcl:{showNewAclDialog:!1,showPasswordDialog:!1,showNewTokenDialog:!1,data:[],passwordGuardedFunction:null,newAclName:"",newTokenName:"",password:"",apiToken:null,selectedTokenId:null,columns:[{name:"Name",align:"left",label:this.$t("Name"),field:"Name",sortable:!1},{name:"path",align:"left",label:this.$t("path"),field:"path",sortable:!1},{name:"read",align:"left",label:this.$t("read"),field:"read",sortable:!1},{name:"write",align:"left",label:this.$t("write"),field:"write",sortable:!1}],pagination:{rowsPerPage:100,page:1}},selectedApiAcl:{id:null,name:null,endpoints:[],token_id_list:[],allRead:!1,allWrite:!1},notifications:{nostr:{identifier:""}}}},methods:{activeLanguage:t=>window.i18n.global.locale===t,changeLanguage(t){window.i18n.global.locale=t,this.$q.localStorage.set("lnbits.lang",t)},async updateAccount(){try{const{data:t}=await LNbits.api.request("PUT","/api/v1/auth/update",null,{user_id:this.user.id,username:this.user.username,email:this.user.email,extra:this.user.extra});this.user=t,this.hasUsername=!!t.username,Quasar.Notify.create({type:"positive",message:"Account updated."})}catch(t){LNbits.utils.notifyApiError(t)}},disableUpdatePassword(){return!this.credentialsData.newPassword||!this.credentialsData.newPasswordRepeat||this.credentialsData.newPassword!==this.credentialsData.newPasswordRepeat},async updatePassword(){if(this.credentialsData.username)try{const{data:t}=await LNbits.api.request("PUT","/api/v1/auth/password",null,{user_id:this.user.id,username:this.credentialsData.username,password_old:this.credentialsData.oldPassword,password:this.credentialsData.newPassword,password_repeat:this.credentialsData.newPasswordRepeat});this.user=t,this.hasUsername=!!t.username,this.credentialsData.show=!1,Quasar.Notify.create({type:"positive",message:"Password updated."})}catch(t){LNbits.utils.notifyApiError(t)}else Quasar.Notify.create({type:"warning",message:"Please set a username."})},async updatePubkey(){try{const{data:t}=await LNbits.api.request("PUT","/api/v1/auth/pubkey",null,{user_id:this.user.id,pubkey:this.credentialsData.pubkey});this.user=t,this.hasUsername=!!t.username,this.credentialsData.show=!1,this.$q.notify({type:"positive",message:"Public key updated."})}catch(t){LNbits.utils.notifyApiError(t)}},showUpdateCredentials(){this.credentialsData={show:!0,oldPassword:null,username:this.user.username,pubkey:this.user.pubkey,newPassword:null,newPasswordRepeat:null}},newApiAclDialog(){this.apiAcl.newAclName=null,this.apiAcl.showNewAclDialog=!0},newTokenAclDialog(){this.apiAcl.newTokenName=null,this.apiAcl.newTokenExpiry=null,this.apiAcl.showNewTokenDialog=!0},handleApiACLSelected(t){this.selectedApiAcl={id:null,name:null,endpoints:[],token_id_list:[]},this.apiAcl.selectedTokenId=null,t&&setTimeout((()=>{const e=this.apiAcl.data.find((e=>e.id===t));this.selectedApiAcl&&(this.selectedApiAcl={...e},this.selectedApiAcl.allRead=this.selectedApiAcl.endpoints.every((t=>t.read)),this.selectedApiAcl.allWrite=this.selectedApiAcl.endpoints.every((t=>t.write)))}))},handleAllEndpointsReadAccess(){this.selectedApiAcl.endpoints.forEach((t=>t.read=this.selectedApiAcl.allRead))},handleAllEndpointsWriteAccess(){this.selectedApiAcl.endpoints.forEach((t=>t.write=this.selectedApiAcl.allWrite))},async getApiACLs(){try{const{data:t}=await LNbits.api.request("GET","/api/v1/auth/acl",null);this.apiAcl.data=t.access_control_list}catch(t){LNbits.utils.notifyApiError(t)}},askPasswordAndRunFunction(t){this.apiAcl.passwordGuardedFunction=t,this.apiAcl.showPasswordDialog=!0},runPasswordGuardedFunction(){this.apiAcl.showPasswordDialog=!1;const t=this.apiAcl.passwordGuardedFunction;t&&this[t]()},async addApiACL(){if(this.apiAcl.newAclName){try{const{data:t}=await LNbits.api.request("PUT","/api/v1/auth/acl",null,{id:this.apiAcl.newAclName,name:this.apiAcl.newAclName,password:this.apiAcl.password});this.apiAcl.data=t.access_control_list;const e=this.apiAcl.data.find((t=>t.name===this.apiAcl.newAclName));this.handleApiACLSelected(e.id),this.apiAcl.showNewAclDialog=!1,this.$q.notify({type:"positive",message:"Access Control List created."})}catch(t){LNbits.utils.notifyApiError(t)}finally{this.apiAcl.name="",this.apiAcl.password=""}this.apiAcl.showNewAclDialog=!1}else this.$q.notify({type:"warning",message:"Name is required."})},async updateApiACLs(){try{const{data:t}=await LNbits.api.request("PUT","/api/v1/auth/acl",null,{id:this.user.id,password:this.apiAcl.password,...this.selectedApiAcl});this.apiAcl.data=t.access_control_list}catch(t){LNbits.utils.notifyApiError(t)}finally{this.apiAcl.password=""}},async deleteApiACL(){if(this.selectedApiAcl.id){try{await LNbits.api.request("DELETE","/api/v1/auth/acl",null,{id:this.selectedApiAcl.id,password:this.apiAcl.password}),this.$q.notify({type:"positive",message:"Access Control List deleted."})}catch(t){LNbits.utils.notifyApiError(t)}finally{this.apiAcl.password=""}this.apiAcl.data=this.apiAcl.data.filter((t=>t.id!==this.selectedApiAcl.id)),this.handleApiACLSelected(this.apiAcl.data[0]?.id)}},async generateApiToken(){if(!this.selectedApiAcl.id)return;const t=new Date(this.apiAcl.newTokenExpiry)-new Date;try{const{data:e}=await LNbits.api.request("POST","/api/v1/auth/acl/token",null,{acl_id:this.selectedApiAcl.id,token_name:this.apiAcl.newTokenName,password:this.apiAcl.password,expiration_time_minutes:Math.trunc(t/6e4)});this.apiAcl.apiToken=e.api_token,this.apiAcl.selectedTokenId=e.id,Quasar.Notify.create({type:"positive",message:"Token Generated."}),await this.getApiACLs(),this.handleApiACLSelected(this.selectedApiAcl.id),this.apiAcl.showNewTokenDialog=!1}catch(t){LNbits.utils.notifyApiError(t)}finally{this.apiAcl.password=""}},async deleteToken(){if(this.apiAcl.selectedTokenId)try{await LNbits.api.request("DELETE","/api/v1/auth/acl/token",null,{id:this.apiAcl.selectedTokenId,acl_id:this.selectedApiAcl.id,password:this.apiAcl.password}),this.$q.notify({type:"positive",message:"Token deleted."}),this.selectedApiAcl.token_id_list=this.selectedApiAcl.token_id_list.filter((t=>t.id!==this.apiAcl.selectedTokenId)),this.apiAcl.selectedTokenId=null}catch(t){LNbits.utils.notifyApiError(t)}finally{this.apiAcl.password=""}}},async created(){try{const{data:t}=await LNbits.api.getAuthenticatedUser();this.user=t,this.hasUsername=!!t.username,this.user.extra||(this.user.extra={})}catch(t){LNbits.utils.notifyApiError(t)}const t=window.location.hash.replace("#","");t&&(this.tab=t),await this.getApiACLs()}},window.PageAdmin={template:"#page-admin",mixins:[windowMixin],data:()=>({tab:"funding",settings:{},formData:{lnbits_exchange_rate_providers:[],lnbits_audit_exclude_paths:[],lnbits_audit_include_paths:[],lnbits_audit_http_response_codes:[]},isSuperUser:!1,needsRestart:!1}),async created(){await this.getSettings();const t=window.location.hash.replace("#","");t&&(this.tab=t)},computed:{checkChanges(){return!_.isEqual(this.settings,this.formData)}},methods:{getDefaultSetting(t){LNbits.api.request("GET",`/admin/api/v1/settings/default?field_name=${t}`).then((e=>{this.formData[t]=e.data.default_value})).catch((function(t){LNbits.utils.notifyApiError(t)}))},restartServer(){LNbits.api.request("GET","/admin/api/v1/restart/").then((t=>{this.$q.notify({type:"positive",message:"Success! Restarted Server",icon:null}),this.needsRestart=!1})).catch(LNbits.utils.notifyApiError)},async getSettings(){await LNbits.api.request("GET","/admin/api/v1/settings",this.g.user.wallets[0].adminkey).then((t=>{this.isSuperUser=t.data.is_super_user||!1,this.settings=t.data,this.formData={...this.settings}})).catch(LNbits.utils.notifyApiError)},updateSettings(){const t=_.omit(this.formData,["is_super_user","lnbits_allowed_funding_sources","touch"]);LNbits.api.request("PUT","/admin/api/v1/settings",this.g.user.wallets[0].adminkey,t).then((t=>{this.needsRestart=this.settings.lnbits_backend_wallet_class!==this.formData.lnbits_backend_wallet_class,this.settings=this.formData,this.formData=_.clone(this.settings),Quasar.Notify.create({type:"positive",message:"Success! Settings changed! "+(this.needsRestart?"Restart required!":""),icon:null})})).catch(LNbits.utils.notifyApiError)},deleteSettings(){LNbits.utils.confirmDialog("Are you sure you want to restore settings to default?").onOk((()=>{LNbits.api.request("DELETE","/admin/api/v1/settings").then((t=>{Quasar.Notify.create({type:"positive",message:"Success! Restored settings to defaults. Restarting...",icon:null}),this.$q.localStorage.clear()})).catch(LNbits.utils.notifyApiError)}))},downloadBackup(){window.open("/admin/api/v1/backup","_blank")}}},window.app.component("lnbits-admin-funding",{props:["is-super-user","form-data","settings"],template:"#lnbits-admin-funding",mixins:[window.windowMixin],data:()=>({auditData:[]}),created(){this.getAudit()},methods:{getAudit(){LNbits.api.request("GET","/admin/api/v1/audit",this.g.user.wallets[0].adminkey).then((t=>{this.auditData=t.data})).catch(LNbits.utils.notifyApiError)}}}),window.app.component("lnbits-admin-funding-sources",{template:"#lnbits-admin-funding-sources",mixins:[window.windowMixin],props:["form-data","allowed-funding-sources"],methods:{getFundingSourceLabel(t){const e=this.rawFundingSources.find((e=>e[0]===t));return e?e[1]:t},showQRValue(t){this.qrValue=t,this.showQRDialog=!0}},computed:{fundingSources(){let t=[];for(const[e,a,s]of this.rawFundingSources){const a={};if(null!==s)for(let[t,e]of Object.entries(s))a[t]="string"==typeof e?{label:e,value:null}:e||{};t.push([e,a])}return new Map(t)},sortedAllowedFundingSources(){return this.allowedFundingSources.sort()}},data:()=>({hideInput:!0,showQRDialog:!1,qrValue:"",rawFundingSources:[["VoidWallet","Void Wallet",null],["FakeWallet","Fake Wallet",{fake_wallet_secret:"Secret",lnbits_denomination:'"sats" or 3 Letter Custom Denomination'}],["CLNRestWallet","Core Lightning Rest (plugin)",{clnrest_url:"Endpoint",clnrest_ca:"ca.pem",clnrest_cert:"server.pem",clnrest_readonly_rune:"Rune used for readonly requests",clnrest_invoice_rune:"Rune used for creating invoices",clnrest_pay_rune:"Rune used for paying invoices using pay",clnrest_renepay_rune:"Rune used for paying invoices using renepay",clnrest_last_pay_index:"Ignores any invoices paid prior to or including this index. 0 is equivalent to not specifying and negative value is invalid.",clnrest_nodeid:"Node id"}],["CoreLightningWallet","Core Lightning",{corelightning_rpc:"Endpoint",corelightning_pay_command:"Custom Pay Command"}],["CoreLightningRestWallet","Core Lightning Rest (legacy)",{corelightning_rest_url:"Endpoint",corelightning_rest_cert:"Certificate",corelightning_rest_macaroon:"Macaroon"}],["LndRestWallet","Lightning Network Daemon (LND Rest)",{lnd_rest_endpoint:"Endpoint",lnd_rest_cert:"Certificate",lnd_rest_macaroon:"Macaroon",lnd_rest_macaroon_encrypted:"Encrypted Macaroon",lnd_rest_route_hints:"Enable Route Hints",lnd_rest_allow_self_payment:"Allow Self Payment"}],["LndWallet","Lightning Network Daemon (LND)",{lnd_grpc_endpoint:"Endpoint",lnd_grpc_cert:"Certificate",lnd_grpc_port:"Port",lnd_grpc_macaroon:"GRPC Macaroon",lnd_grpc_invoice_macaroon:"GRPC Invoice Macaroon",lnd_grpc_admin_macaroon:"GRPC Admin Macaroon",lnd_grpc_macaroon_encrypted:"Encrypted Macaroon"}],["LnTipsWallet","LN.Tips",{lntips_api_endpoint:"Endpoint",lntips_api_key:"API Key"}],["LNPayWallet","LN Pay",{lnpay_api_endpoint:"Endpoint",lnpay_api_key:"API Key",lnpay_wallet_key:"Wallet Key"}],["EclairWallet","Eclair (ACINQ)",{eclair_url:"URL",eclair_pass:"Password"}],["LNbitsWallet","LNbits",{lnbits_endpoint:"Endpoint",lnbits_key:"Admin Key"}],["BlinkWallet","Blink",{blink_api_endpoint:"Endpoint",blink_ws_endpoint:"WebSocket",blink_token:"Key"}],["AlbyWallet","Alby",{alby_api_endpoint:"Endpoint",alby_access_token:"Key"}],["BoltzWallet","Boltz",{boltz_client_endpoint:"Endpoint",boltz_client_macaroon:"Admin Macaroon path or hex",boltz_client_cert:"Certificate path or hex",boltz_client_wallet:"Wallet Name",boltz_client_password:"Wallet Password (can be empty)",boltz_mnemonic:{label:"Liquid mnemonic (copy into greenwallet)",readonly:!0,copy:!0,qrcode:!0}}],["ZBDWallet","ZBD",{zbd_api_endpoint:"Endpoint",zbd_api_key:"Key"}],["PhoenixdWallet","Phoenixd",{phoenixd_api_endpoint:"Endpoint",phoenixd_api_password:"Key"}],["OpenNodeWallet","OpenNode",{opennode_api_endpoint:"Endpoint",opennode_key:"Key"}],["ClicheWallet","Cliche (NBD)",{cliche_endpoint:"Endpoint"}],["SparkWallet","Spark",{spark_url:"Endpoint",spark_token:"Token"}],["NWCWallet","Nostr Wallet Connect",{nwc_pairing_url:"Pairing URL"}],["BreezSdkWallet","Breez SDK",{breez_api_key:"Breez API Key",breez_greenlight_seed:"Greenlight Seed",breez_greenlight_device_key:"Greenlight Device Key",breez_greenlight_device_cert:"Greenlight Device Cert",breez_greenlight_invite_code:"Greenlight Invite Code"}],["StrikeWallet","Strike (alpha)",{strike_api_endpoint:"API Endpoint",strike_api_key:"API Key"}],["BreezLiquidSdkWallet","Breez Liquid SDK",{breez_liquid_api_key:"Breez API Key (can be empty)",breez_liquid_seed:"Liquid seed phrase",breez_liquid_fee_offset_sat:"Offset amount in sats to increase fee limit"}]]})}),window.app.component("lnbits-admin-fiat-providers",{props:["form-data"],template:"#lnbits-admin-fiat-providers",mixins:[window.windowMixin],data:()=>({formAddStripeUser:"",hideInputToggle:!0}),methods:{addStripeAllowedUser(){const t=this.formAddStripeUser||"";t.length&&!this.formData.stripe_limits.allowed_users.includes(t)&&(this.formData.stripe_limits.allowed_users=[...this.formData.stripe_limits.allowed_users,t],this.formAddStripeUser="")},removeStripeAllowedUser(t){this.formData.stripe_limits.allowed_users=this.formData.stripe_limits.allowed_users.filter((e=>e!==t))},checkFiatProvider(t){LNbits.api.request("PUT",`/api/v1/fiat/check/${t}`).then((t=>{const e=t.data;Quasar.Notify.create({type:e.success?"positive":"warning",message:e.message,icon:null})})).catch(LNbits.utils.notifyApiError)}}}),window.app.component("lnbits-admin-exchange-providers",{props:["form-data"],template:"#lnbits-admin-exchange-providers",mixins:[window.windowMixin],data:()=>({exchangeData:{selectedProvider:null,showTickerConversion:!1,convertFromTicker:null,convertToTicker:null},exchangesTable:{columns:[{name:"name",align:"left",label:"Exchange Name",field:"name",sortable:!0},{name:"api_url",align:"left",label:"URL",field:"api_url",sortable:!1},{name:"path",align:"left",label:"JSON Path",field:"path",sortable:!1},{name:"exclude_to",align:"left",label:"Exclude Currencies",field:"exclude_to",sortable:!1},{name:"ticker_conversion",align:"left",label:"Ticker Conversion",field:"ticker_conversion",sortable:!1}],pagination:{sortBy:"name",rowsPerPage:100,page:1,rowsNumber:100},search:null,hideEmpty:!0}}),mounted(){this.getExchangeRateHistory()},created(){const t=window.location.hash.replace("#","");"exchange_providers"===t&&this.showExchangeProvidersTab(t)},methods:{getExchangeRateHistory(){LNbits.api.request("GET","/api/v1/rate/history",this.g.user.wallets[0].inkey).then((t=>{this.initExchangeChart(t.data)})).catch((function(t){LNbits.utils.notifyApiError(t)}))},showExchangeProvidersTab(t){"exchange_providers"===t&&this.getExchangeRateHistory()},addExchangeProvider(){this.formData.lnbits_exchange_rate_providers=[{name:"",api_url:"",path:"",exclude_to:[]},...this.formData.lnbits_exchange_rate_providers]},removeExchangeProvider(t){this.formData.lnbits_exchange_rate_providers=this.formData.lnbits_exchange_rate_providers.filter((e=>e!==t))},removeExchangeTickerConversion(t,e){t.ticker_conversion=t.ticker_conversion.filter((t=>t!==e)),this.formData.touch=null},addExchangeTickerConversion(){this.exchangeData.selectedProvider&&(this.exchangeData.selectedProvider.ticker_conversion.push(`${this.exchangeData.convertFromTicker}:${this.exchangeData.convertToTicker}`),this.formData.touch=null,this.exchangeData.showTickerConversion=!1)},showTickerConversionDialog(t){this.exchangeData.convertFromTicker=null,this.exchangeData.convertToTicker=null,this.exchangeData.selectedProvider=t,this.exchangeData.showTickerConversion=!0},initExchangeChart(t){const e=t.map((t=>Quasar.date.formatDate(new Date(1e3*t.timestamp),"HH:mm"))),a=[...this.formData.lnbits_exchange_rate_providers,{name:"LNbits"}].map((e=>({label:e.name,data:t.map((t=>t.rates[e.name])),pointStyle:!0,borderWidth:"LNbits"===e.name?4:1,tension:.4})));this.exchangeRatesChart=new Chart(this.$refs.exchangeRatesChart.getContext("2d"),{type:"line",options:{plugins:{legend:{display:!1}}},data:{labels:e,datasets:a}})}}}),window.app.component("lnbits-admin-security",{props:["form-data"],template:"#lnbits-admin-security",mixins:[window.windowMixin],data:()=>({logs:[],formBlockedIPs:"",serverlogEnabled:!1,nostrAcceptedUrl:"",formAllowedIPs:"",formCallbackUrlRule:""}),created(){},methods:{addAllowedIPs(){const t=this.formAllowedIPs.trim(),e=this.formData.lnbits_allowed_ips;t&&t.length&&!e.includes(t)&&(this.formData.lnbits_allowed_ips=[...e,t],this.formAllowedIPs="")},removeAllowedIPs(t){const e=this.formData.lnbits_allowed_ips;this.formData.lnbits_allowed_ips=e.filter((e=>e!==t))},addBlockedIPs(){const t=this.formBlockedIPs.trim(),e=this.formData.lnbits_blocked_ips;t&&t.length&&!e.includes(t)&&(this.formData.lnbits_blocked_ips=[...e,t],this.formBlockedIPs="")},removeBlockedIPs(t){const e=this.formData.lnbits_blocked_ips;this.formData.lnbits_blocked_ips=e.filter((e=>e!==t))},addCallbackUrlRule(){const t=this.formCallbackUrlRule.trim(),e=this.formData.lnbits_callback_url_rules;t&&t.length&&!e.includes(t)&&(this.formData.lnbits_callback_url_rules=[...e,t],this.formCallbackUrlRule="")},removeCallbackUrlRule(t){const e=this.formData.lnbits_callback_url_rules;this.formData.lnbits_callback_url_rules=e.filter((e=>e!==t))},addNostrUrl(){const t=this.nostrAcceptedUrl.trim();this.removeNostrUrl(t),this.formData.nostr_absolute_request_urls.push(t),this.nostrAcceptedUrl=""},removeNostrUrl(t){this.formData.nostr_absolute_request_urls=this.formData.nostr_absolute_request_urls.filter((e=>e!==t))},async toggleServerLog(){if(this.serverlogEnabled=!this.serverlogEnabled,this.serverlogEnabled){const t="http:"!==location.protocol?"wss://":"ws://",e=await LNbits.utils.digestMessage(this.g.user.id),a=t+document.domain+":"+location.port+"/api/v1/ws/"+e;this.ws=new WebSocket(a),this.ws.addEventListener("message",(async({data:t})=>{this.logs.push(t.toString());const e=this.$refs.logScroll;if(e){const t=e.getScrollTarget(),a=0;e.setScrollPosition(t.scrollHeight,a)}}))}else this.ws.close()}}}),window.app.component("lnbits-admin-users",{props:["form-data"],template:"#lnbits-admin-users",mixins:[window.windowMixin],data:()=>({formAddUser:"",formAddAdmin:""}),methods:{addAllowedUser(){let t=this.formAddUser,e=this.formData.lnbits_allowed_users;t&&t.length&&!e.includes(t)&&(this.formData.lnbits_allowed_users=[...e,t],this.formAddUser="")},removeAllowedUser(t){let e=this.formData.lnbits_allowed_users;this.formData.lnbits_allowed_users=e.filter((e=>e!==t))},addAdminUser(){let t=this.formAddAdmin,e=this.formData.lnbits_admin_users;t&&t.length&&!e.includes(t)&&(this.formData.lnbits_admin_users=[...e,t],this.formAddAdmin="")},removeAdminUser(t){let e=this.formData.lnbits_admin_users;this.formData.lnbits_admin_users=e.filter((e=>e!==t))}}}),window.app.component("lnbits-admin-server",{props:["form-data"],template:"#lnbits-admin-server",mixins:[window.windowMixin],data:()=>({currencies:[]}),async created(){this.currencies=await LNbits.api.getCurrencies()}}),window.app.component("lnbits-admin-extensions",{props:["form-data"],template:"#lnbits-admin-extensions",mixins:[window.windowMixin],data:()=>({formAddExtensionsManifest:""}),methods:{addExtensionsManifest(){const t=this.formAddExtensionsManifest.trim(),e=this.formData.lnbits_extensions_manifests;t&&t.length&&!e.includes(t)&&(this.formData.lnbits_extensions_manifests=[...e,t],this.formAddExtensionsManifest="")},removeExtensionsManifest(t){const e=this.formData.lnbits_extensions_manifests;this.formData.lnbits_extensions_manifests=e.filter((e=>e!==t))}}}),window.app.component("lnbits-admin-notifications",{props:["form-data"],template:"#lnbits-admin-notifications",mixins:[window.windowMixin],data:()=>({nostrNotificationIdentifier:"",emailNotificationAddress:""}),methods:{sendTestEmail(){LNbits.api.request("GET","/admin/api/v1/testemail",this.g.user.wallets[0].adminkey).then((t=>{if("error"===t.data.status)throw new Error(t.data.message);this.$q.notify({message:"Test email sent!",color:"positive"})})).catch((t=>{this.$q.notify({message:t.message,color:"negative"})}))},addNostrNotificationIdentifier(){const t=this.nostrNotificationIdentifier.trim(),e=this.formData.lnbits_nostr_notifications_identifiers;t&&t.length&&!e.includes(t)&&(this.formData.lnbits_nostr_notifications_identifiers=[...e,t],this.nostrNotificationIdentifier="")},removeNostrNotificationIdentifier(t){const e=this.formData.lnbits_nostr_notifications_identifiers;this.formData.lnbits_nostr_notifications_identifiers=e.filter((e=>e!==t))},addEmailNotificationAddress(){const t=this.emailNotificationAddress.trim(),e=this.formData.lnbits_email_notifications_to_emails;t&&t.length&&!e.includes(t)&&(this.formData.lnbits_email_notifications_to_emails=[...e,t],this.emailNotificationAddress="")},removeEmailNotificationAddress(t){const e=this.formData.lnbits_email_notifications_to_emails;this.formData.lnbits_email_notifications_to_emails=e.filter((e=>e!==t))}}}),window.app.component("lnbits-admin-site-customisation",{props:["form-data"],template:"#lnbits-admin-site-customisation",mixins:[window.windowMixin],data:()=>({lnbits_theme_options:["classic","bitcoin","flamingo","cyber","freedom","mint","autumn","monochrome","salvador"],colors:["primary","secondary","accent","positive","negative","info","warning","red","yellow","orange"],reactionOptions:["none","confettiBothSides","confettiFireworks","confettiStars","confettiTop"],globalBorderOptions:["retro-border","hard-border","neon-border","no-border"]}),methods:{}}),window.app.component("lnbits-admin-library",{props:["form-data"],template:"#lnbits-admin-library",mixins:[window.windowMixin],data:()=>({library_images:[]}),async created(){await this.getUploadedImages()},methods:{onImageInput(t){const e=t.target.files[0];e&&this.uploadImage(e)},uploadImage(t){const e=new FormData;e.append("file",t),LNbits.api.request("POST","/admin/api/v1/images",this.g.user.wallets[0].adminkey,e,{headers:{"Content-Type":"multipart/form-data"}}).then((()=>{this.$q.notify({type:"positive",message:"Image uploaded!",icon:null}),this.getUploadedImages()})).catch(LNbits.utils.notifyApiError)},getUploadedImages(){LNbits.api.request("GET","/admin/api/v1/images",this.g.user.wallets[0].inkey).then((t=>{this.library_images=t.data.map((t=>({...t,url:`${window.origin}/${t.directory}/${t.filename}`})))})).catch(LNbits.utils.notifyApiError)},deleteImage(t){LNbits.utils.confirmDialog("Are you sure you want to delete this image?").onOk((()=>{LNbits.api.request("DELETE",`/admin/api/v1/images/${t}`,this.g.user.wallets[0].adminkey).then((()=>{this.$q.notify({type:"positive",message:"Image deleted!",icon:null}),this.getUploadedImages()})).catch(LNbits.utils.notifyApiError)}))}}}),window.app.component("lnbits-admin-audit",{props:["form-data"],template:"#lnbits-admin-audit",mixins:[window.windowMixin],data:()=>({formAddIncludePath:"",formAddExcludePath:"",formAddIncludeResponseCode:""}),methods:{addIncludePath(){if(""===this.formAddIncludePath)return;const t=this.formData.lnbits_audit_include_paths;t.includes(this.formAddIncludePath)||(this.formData.lnbits_audit_include_paths=[...t,this.formAddIncludePath]),this.formAddIncludePath=""},removeIncludePath(t){this.formData.lnbits_audit_include_paths=this.formData.lnbits_audit_include_paths.filter((e=>e!==t))},addExcludePath(){if(""===this.formAddExcludePath)return;const t=this.formData.lnbits_audit_exclude_paths;t.includes(this.formAddExcludePath)||(this.formData.lnbits_audit_exclude_paths=[...t,this.formAddExcludePath]),this.formAddExcludePath=""},removeExcludePath(t){this.formData.lnbits_audit_exclude_paths=this.formData.lnbits_audit_exclude_paths.filter((e=>e!==t))},addIncludeResponseCode(){if(""===this.formAddIncludeResponseCode)return;const t=this.formData.lnbits_audit_http_response_codes;t.includes(this.formAddIncludeResponseCode)||(this.formData.lnbits_audit_http_response_codes=[...t,this.formAddIncludeResponseCode]),this.formAddIncludeResponseCode=""},removeIncludeResponseCode(t){this.formData.lnbits_audit_http_response_codes=this.formData.lnbits_audit_http_response_codes.filter((e=>e!==t))}}}),window.app.component("lnbits-qrcode",{mixins:[window.windowMixin],template:"#lnbits-qrcode",components:{QrcodeVue:QrcodeVue},props:{value:{type:String,required:!0},nfc:{type:Boolean,default:!1},showButtons:{type:Boolean,default:!0},href:{type:String,default:""},margin:{type:Number,default:3},maxWidth:{type:Number,default:450},logo:{type:String,default:LNBITS_QR_LOGO}},data:()=>({nfcTagWriting:!1,nfcSupported:"undefined"!=typeof NDEFReader}),methods:{clickQrCode(t){if(""===this.href)return this.copyText(this.value),t.preventDefault(),t.stopPropagation(),!1},async writeNfcTag(){try{if(!this.nfcSupported)throw{toString:function(){return"NFC not supported on this device or browser."}};const t=new NDEFReader;this.nfcTagWriting=!0,this.$q.notify({message:"Tap your NFC tag to write the LNURL-withdraw link to it."}),await t.write({records:[{recordType:"url",data:this.value,lang:"en"}]}),this.nfcTagWriting=!1,this.$q.notify({type:"positive",message:"NFC tag written successfully."})}catch(t){this.nfcTagWriting=!1,this.$q.notify({type:"negative",message:t?t.toString():"An unexpected error has occurred."})}},downloadSVG(){const t=this.$refs.qrCode.$el;if(!t)return void console.error("SVG element not found");let e=(new XMLSerializer).serializeToString(t);e.match(/^