Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
475ae5736e | ||
|
|
d7f0f4da0c | ||
|
|
f7b3444be4 | ||
|
|
28e32a54a4 | ||
|
|
1833adc0a1 | ||
|
|
b759ec7468 | ||
|
|
2661d12ff1 | ||
|
|
ed419f27ac | ||
|
|
c3efd48108 | ||
|
|
4a28c22d79 | ||
|
|
0d0eb36de4 | ||
|
|
2aa73bfe75 | ||
|
|
ebd080192c | ||
|
|
41abe63f18 | ||
|
|
f61471b0f2 | ||
|
|
9bd037b6e7 | ||
|
|
a4ca88b6a4 | ||
|
|
f2b9aafc51 |
@@ -7,7 +7,7 @@ inputs:
|
|||||||
default: "3.10"
|
default: "3.10"
|
||||||
poetry-version:
|
poetry-version:
|
||||||
description: "Poetry Version"
|
description: "Poetry Version"
|
||||||
default: "1.7.0"
|
default: "1.8.5"
|
||||||
node-version:
|
node-version:
|
||||||
description: "Node Version"
|
description: "Node Version"
|
||||||
default: "20.x"
|
default: "20.x"
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ from .users import (
|
|||||||
check_admin_settings,
|
check_admin_settings,
|
||||||
create_user_account,
|
create_user_account,
|
||||||
create_user_account_no_ckeck,
|
create_user_account_no_ckeck,
|
||||||
init_admin_settings,
|
|
||||||
update_user_account,
|
update_user_account,
|
||||||
update_user_extensions,
|
update_user_extensions,
|
||||||
)
|
)
|
||||||
@@ -58,7 +57,6 @@ __all__ = [
|
|||||||
"check_admin_settings",
|
"check_admin_settings",
|
||||||
"create_user_account",
|
"create_user_account",
|
||||||
"create_user_account_no_ckeck",
|
"create_user_account_no_ckeck",
|
||||||
"init_admin_settings",
|
|
||||||
"update_user_account",
|
"update_user_account",
|
||||||
"update_user_extensions",
|
"update_user_extensions",
|
||||||
# websockets
|
# websockets
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ from lnbits.core.models.notifications import (
|
|||||||
)
|
)
|
||||||
from lnbits.core.services.nostr import fetch_nip5_details, send_nostr_dm
|
from lnbits.core.services.nostr import fetch_nip5_details, send_nostr_dm
|
||||||
from lnbits.core.services.websockets import websocket_manager
|
from lnbits.core.services.websockets import websocket_manager
|
||||||
from lnbits.helpers import check_callback_url
|
from lnbits.helpers import check_callback_url, is_valid_email_address
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.utils.nostr import normalize_private_key
|
from lnbits.utils.nostr import normalize_private_key
|
||||||
|
|
||||||
@@ -111,41 +111,56 @@ async def send_telegram_message(token: str, chat_id: str, message: str) -> dict:
|
|||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
|
|
||||||
async def send_email_notification(message: str) -> dict:
|
async def send_email_notification(
|
||||||
await send_email(
|
message: str, subject: str = "LNbits Notification"
|
||||||
settings.lnbits_email_notifications_server,
|
) -> dict:
|
||||||
settings.lnbits_email_notifications_port,
|
if not settings.lnbits_email_notifications_enabled:
|
||||||
settings.lnbits_email_notifications_password,
|
return {"status": "error", "message": "Email notifications are disabled"}
|
||||||
settings.lnbits_email_notifications_email,
|
try:
|
||||||
settings.lnbits_email_notifications_to_emails,
|
await send_email(
|
||||||
"LNbits Notification",
|
settings.lnbits_email_notifications_server,
|
||||||
message,
|
settings.lnbits_email_notifications_port,
|
||||||
)
|
settings.lnbits_email_notifications_username,
|
||||||
return {"status": "ok"}
|
settings.lnbits_email_notifications_password,
|
||||||
|
settings.lnbits_email_notifications_email,
|
||||||
|
settings.lnbits_email_notifications_to_emails,
|
||||||
|
subject,
|
||||||
|
message,
|
||||||
|
)
|
||||||
|
return {"status": "ok"}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error sending email notification: {e}")
|
||||||
|
return {"status": "error", "message": str(e)}
|
||||||
|
|
||||||
|
|
||||||
async def send_email(
|
async def send_email(
|
||||||
server: str,
|
server: str,
|
||||||
port: int,
|
port: int,
|
||||||
|
username: str,
|
||||||
password: str,
|
password: str,
|
||||||
from_email: str,
|
from_email: str,
|
||||||
to_emails: list,
|
to_emails: list[str],
|
||||||
subject: str,
|
subject: str,
|
||||||
message: str,
|
message: str,
|
||||||
):
|
) -> bool:
|
||||||
|
if not is_valid_email_address(from_email):
|
||||||
|
raise ValueError(f"Invalid from email address: {from_email}")
|
||||||
|
if len(to_emails) == 0:
|
||||||
|
raise ValueError("No email addresses provided")
|
||||||
|
for email in to_emails:
|
||||||
|
if not is_valid_email_address(email):
|
||||||
|
raise ValueError(f"Invalid email address: {email}")
|
||||||
msg = MIMEMultipart()
|
msg = MIMEMultipart()
|
||||||
msg["From"] = from_email
|
msg["From"] = from_email
|
||||||
msg["To"] = ", ".join(to_emails)
|
msg["To"] = ", ".join(to_emails)
|
||||||
msg["Subject"] = subject
|
msg["Subject"] = subject
|
||||||
msg.attach(MIMEText(message, "plain"))
|
msg.attach(MIMEText(message, "plain"))
|
||||||
try:
|
username = username if len(username) > 0 else from_email
|
||||||
with smtplib.SMTP(server, port) as smtp_server:
|
with smtplib.SMTP(server, port) as smtp_server:
|
||||||
smtp_server.starttls()
|
smtp_server.starttls()
|
||||||
smtp_server.login(from_email, password)
|
smtp_server.login(username, password)
|
||||||
smtp_server.sendmail(from_email, to_emails, msg.as_string())
|
smtp_server.sendmail(from_email, to_emails, msg.as_string())
|
||||||
logger.debug(f"Emails sent successfully to: {', '.join(to_emails)}")
|
return True
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"Failed to send email: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
def is_message_type_enabled(message_type: NotificationType) -> bool:
|
def is_message_type_enabled(message_type: NotificationType) -> bool:
|
||||||
@@ -246,7 +261,8 @@ async def send_ws_payment_notification(wallet: Wallet, payment: Payment):
|
|||||||
await websocket_manager.send_data(payment_notification, wallet.adminkey)
|
await websocket_manager.send_data(payment_notification, wallet.adminkey)
|
||||||
|
|
||||||
await websocket_manager.send_data(
|
await websocket_manager.send_data(
|
||||||
json.dumps({"pending": payment.pending}), payment.payment_hash
|
json.dumps({"pending": payment.pending, "status": payment.status}),
|
||||||
|
payment.payment_hash,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ from lnbits.core.models.extensions import UserExtension
|
|||||||
from lnbits.settings import (
|
from lnbits.settings import (
|
||||||
EditableSettings,
|
EditableSettings,
|
||||||
SuperSettings,
|
SuperSettings,
|
||||||
send_admin_user_to_saas,
|
|
||||||
settings,
|
settings,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -154,14 +153,6 @@ async def check_admin_settings():
|
|||||||
with open(Path(settings.lnbits_data_folder) / ".super_user", "w") as file:
|
with open(Path(settings.lnbits_data_folder) / ".super_user", "w") as file:
|
||||||
file.write(settings.super_user)
|
file.write(settings.super_user)
|
||||||
|
|
||||||
# callback for saas
|
|
||||||
if (
|
|
||||||
settings.lnbits_saas_callback
|
|
||||||
and settings.lnbits_saas_secret
|
|
||||||
and settings.lnbits_saas_instance_id
|
|
||||||
):
|
|
||||||
send_admin_user_to_saas()
|
|
||||||
|
|
||||||
account = await get_account(settings.super_user)
|
account = await get_account(settings.super_user)
|
||||||
if account and account.extra and account.extra.provider == "env":
|
if account and account.extra and account.extra.provider == "env":
|
||||||
settings.first_install = True
|
settings.first_install = True
|
||||||
|
|||||||
@@ -150,7 +150,6 @@
|
|||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<q-separator></q-separator>
|
<q-separator></q-separator>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<strong v-text="$t('notifications_email_config')"></strong>
|
<strong v-text="$t('notifications_email_config')"></strong>
|
||||||
<q-item tag="label" v-ripple>
|
<q-item tag="label" v-ripple>
|
||||||
@@ -194,6 +193,24 @@
|
|||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
<q-item tag="label" v-ripple>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label
|
||||||
|
v-text="$t('notifications_send_email_username')"
|
||||||
|
></q-item-label>
|
||||||
|
<q-item-label
|
||||||
|
caption
|
||||||
|
v-text="$t('notifications_send_email_username_desc')"
|
||||||
|
></q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-input
|
||||||
|
:type="hideInputToggle ? 'password' : 'text'"
|
||||||
|
filled
|
||||||
|
v-model="formData.lnbits_email_notifications_username"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
<q-item tag="label" v-ripple>
|
<q-item tag="label" v-ripple>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label
|
<q-item-label
|
||||||
@@ -212,7 +229,17 @@
|
|||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
<q-item>
|
||||||
|
<q-btn
|
||||||
|
@click="sendTestEmail()"
|
||||||
|
:label="$t('notifications_send_test_email')"
|
||||||
|
color="primary"
|
||||||
|
class="q-mt-md"
|
||||||
|
></q-btn>
|
||||||
|
</q-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
<q-item tag="label" v-ripple>
|
<q-item tag="label" v-ripple>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label
|
<q-item-label
|
||||||
@@ -249,9 +276,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col">
|
|
||||||
<q-item tag="label" v-ripple>
|
<q-item tag="label" v-ripple>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label
|
<q-item-label
|
||||||
|
|||||||
@@ -53,17 +53,6 @@
|
|||||||
:label="$t('lnbits_wallet')"
|
:label="$t('lnbits_wallet')"
|
||||||
></q-input>
|
></q-input>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-4">
|
|
||||||
<p><span v-text="$t('denomination')"></span></p>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
type="text"
|
|
||||||
v-model="formData.lnbits_denomination"
|
|
||||||
label="sats"
|
|
||||||
:hint="$t('denomination_hint')"
|
|
||||||
:rules="[(val) => !val || val.length == 3 || val == 'sats' || $t('denomination_error')]"
|
|
||||||
></q-input>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-4">
|
<div class="col-12 col-md-4">
|
||||||
<p><span v-text="$t('ui_qr_code_logo')"></span></p>
|
<p><span v-text="$t('ui_qr_code_logo')"></span></p>
|
||||||
<q-input
|
<q-input
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h4 class="q-my-none">
|
<h4 class="q-my-none">
|
||||||
<span v-text="$t('password_config')"></span>
|
<span v-text="$t('password')"></span>
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
unelevated
|
unelevated
|
||||||
color="primary"
|
color="primary"
|
||||||
class="float-right"
|
class="float-right"
|
||||||
:label="$t('change_password')"
|
:label="$t('update_password')"
|
||||||
>
|
>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div class="col q-mb-sm">
|
<div class="col q-mb-sm">
|
||||||
<h4 class="q-my-none">
|
<h4 class="q-my-none">
|
||||||
<span v-text="$t('pubkey')"></span>
|
Nostr <span v-text="$t('pubkey')"></span>
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<q-input
|
<q-input
|
||||||
@@ -287,7 +287,7 @@
|
|||||||
</q-btn>
|
</q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
@click="showUpdateCredentials()"
|
@click="showUpdateCredentials()"
|
||||||
:label="$t('update_credentials')"
|
:label="$t('change_password')"
|
||||||
filled
|
filled
|
||||||
color="primary"
|
color="primary"
|
||||||
class="float-right"
|
class="float-right"
|
||||||
@@ -356,7 +356,7 @@
|
|||||||
flat
|
flat
|
||||||
@click="themeChoiceFunc('bitcoin')"
|
@click="themeChoiceFunc('bitcoin')"
|
||||||
icon="circle"
|
icon="circle"
|
||||||
color="orange"
|
color="deep-orange"
|
||||||
size="md"
|
size="md"
|
||||||
><q-tooltip>bitcoin</q-tooltip>
|
><q-tooltip>bitcoin</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
|
|||||||
@@ -87,18 +87,20 @@
|
|||||||
>
|
>
|
||||||
{{SITE_TITLE}}
|
{{SITE_TITLE}}
|
||||||
</h5>
|
</h5>
|
||||||
<h6
|
<template v-if="$q.screen.gt.sm">
|
||||||
class="q-my-sm"
|
<h6
|
||||||
v-if="'{{LNBITS_SHOW_HOME_PAGE_ELEMENTS}}' == 'True'"
|
class="q-my-sm"
|
||||||
>
|
v-if="'{{LNBITS_SHOW_HOME_PAGE_ELEMENTS}}' == 'True'"
|
||||||
{{SITE_TAGLINE}}
|
>
|
||||||
</h6>
|
{{SITE_TAGLINE}}
|
||||||
<p
|
</h6>
|
||||||
class="q-my-sm"
|
<p
|
||||||
v-if="'{{LNBITS_SHOW_HOME_PAGE_ELEMENTS}}' == 'True'"
|
class="q-my-sm"
|
||||||
>
|
v-if="'{{LNBITS_SHOW_HOME_PAGE_ELEMENTS}}' == 'True'"
|
||||||
{{SITE_DESCRIPTION}}
|
>
|
||||||
</p>
|
{{SITE_DESCRIPTION}}
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
<!-- <div
|
<!-- <div
|
||||||
class="gt-sm"
|
class="gt-sm"
|
||||||
v-html="formatDescription"
|
v-html="formatDescription"
|
||||||
|
|||||||
@@ -726,6 +726,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="g.fiatTracking">
|
<div v-if="g.fiatTracking">
|
||||||
<div v-if="isFiatPriority">
|
<div v-if="isFiatPriority">
|
||||||
|
<h5 class="q-my-none text-bold">
|
||||||
|
<span
|
||||||
|
v-text="walletFormatBalance(parse.invoice.sat)"
|
||||||
|
></span>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div v-else style="opacity: 0.75">
|
||||||
<div class="text-h5 text-italic">
|
<div class="text-h5 text-italic">
|
||||||
<span
|
<span
|
||||||
v-text="parse.invoice.fiatAmount"
|
v-text="parse.invoice.fiatAmount"
|
||||||
@@ -733,13 +740,6 @@
|
|||||||
></span>
|
></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else style="opacity: 0.75">
|
|
||||||
<h5 class="q-my-none text-bold">
|
|
||||||
<span
|
|
||||||
v-text="walletFormatBalance(parse.invoice.sat)"
|
|
||||||
></span>
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<q-separator></q-separator>
|
<q-separator></q-separator>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from lnbits.core.services import (
|
|||||||
get_balance_delta,
|
get_balance_delta,
|
||||||
update_cached_settings,
|
update_cached_settings,
|
||||||
)
|
)
|
||||||
|
from lnbits.core.services.notifications import send_email_notification
|
||||||
from lnbits.core.services.settings import dict_to_settings
|
from lnbits.core.services.settings import dict_to_settings
|
||||||
from lnbits.decorators import check_admin, check_super_user
|
from lnbits.decorators import check_admin, check_super_user
|
||||||
from lnbits.server import server_restart
|
from lnbits.server import server_restart
|
||||||
@@ -50,6 +51,18 @@ async def api_monitor():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.get(
|
||||||
|
"/api/v1/testemail",
|
||||||
|
name="TestEmail",
|
||||||
|
description="send a test email to the admin",
|
||||||
|
dependencies=[Depends(check_admin)],
|
||||||
|
)
|
||||||
|
async def api_test_email():
|
||||||
|
return await send_email_notification(
|
||||||
|
"This is a LNbits test email.", "LNbits Test Email"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@admin_router.get("/api/v1/settings", response_model=Optional[AdminSettings])
|
@admin_router.get("/api/v1/settings", response_model=Optional[AdminSettings])
|
||||||
async def api_get_settings(
|
async def api_get_settings(
|
||||||
user: User = Depends(check_admin),
|
user: User = Depends(check_admin),
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from pathlib import Path
|
|
||||||
from typing import Annotated, List, Optional, Union
|
from typing import Annotated, List, Optional, Union
|
||||||
from urllib.parse import urlencode, urlparse
|
from urllib.parse import urlencode, urlparse
|
||||||
|
|
||||||
@@ -37,7 +36,7 @@ generic_router = APIRouter(
|
|||||||
|
|
||||||
@generic_router.get("/favicon.ico", response_class=FileResponse)
|
@generic_router.get("/favicon.ico", response_class=FileResponse)
|
||||||
async def favicon():
|
async def favicon():
|
||||||
return FileResponse(Path("lnbits", "static", "favicon.ico"))
|
return RedirectResponse(settings.lnbits_qr_logo)
|
||||||
|
|
||||||
|
|
||||||
@generic_router.get("/", response_class=HTMLResponse)
|
@generic_router.get("/", response_class=HTMLResponse)
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ from urllib.parse import urlparse
|
|||||||
import jinja2
|
import jinja2
|
||||||
import jwt
|
import jwt
|
||||||
import shortuuid
|
import shortuuid
|
||||||
from fastapi import Request
|
|
||||||
from fastapi.routing import APIRoute
|
from fastapi.routing import APIRoute
|
||||||
from packaging import version
|
from packaging import version
|
||||||
from pydantic.schema import field_schema
|
from pydantic.schema import field_schema
|
||||||
@@ -344,7 +343,3 @@ def path_segments(path: str) -> list[str]:
|
|||||||
def normalize_path(path: Optional[str]) -> str:
|
def normalize_path(path: Optional[str]) -> str:
|
||||||
path = path or ""
|
path = path or ""
|
||||||
return "/" + "/".join(path_segments(path))
|
return "/" + "/".join(path_segments(path))
|
||||||
|
|
||||||
|
|
||||||
def normalized_path(request: Request) -> str:
|
|
||||||
return "/" + "/".join(path_segments(request.url.path))
|
|
||||||
|
|||||||
@@ -1,477 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import base64
|
|
||||||
import json
|
|
||||||
from http import HTTPStatus
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from fastapi import HTTPException
|
|
||||||
from httpx import HTTPStatusError
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from lnbits.db import Filters, Page
|
|
||||||
from lnbits.nodes import Node
|
|
||||||
from lnbits.nodes.base import (
|
|
||||||
ChannelBalance,
|
|
||||||
ChannelPoint,
|
|
||||||
ChannelState,
|
|
||||||
ChannelStats,
|
|
||||||
NodeChannel,
|
|
||||||
NodeFees,
|
|
||||||
NodeInfoResponse,
|
|
||||||
NodeInvoice,
|
|
||||||
NodeInvoiceFilters,
|
|
||||||
NodePayment,
|
|
||||||
NodePaymentsFilters,
|
|
||||||
NodePeerInfo,
|
|
||||||
PublicNodeInfo,
|
|
||||||
)
|
|
||||||
from lnbits.utils.cache import cache
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from lnbits.wallets import PhoenixdWallet
|
|
||||||
|
|
||||||
|
|
||||||
def msat(raw: str) -> int:
|
|
||||||
return int(raw) * 1000
|
|
||||||
|
|
||||||
|
|
||||||
def _decode_bytes(data: str) -> str:
|
|
||||||
return base64.b64decode(data).hex()
|
|
||||||
|
|
||||||
|
|
||||||
def _encode_bytes(data: str) -> str:
|
|
||||||
return base64.b64encode(bytes.fromhex(data)).decode()
|
|
||||||
|
|
||||||
|
|
||||||
def _encode_urlsafe_bytes(data: str) -> str:
|
|
||||||
return base64.urlsafe_b64encode(bytes.fromhex(data)).decode()
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_channel_point(raw: str) -> ChannelPoint:
|
|
||||||
funding_tx, output_index = raw.split(":")
|
|
||||||
return ChannelPoint(
|
|
||||||
funding_txid=funding_tx,
|
|
||||||
output_index=int(output_index),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PhoenixdNode(Node):
|
|
||||||
wallet: PhoenixdWallet
|
|
||||||
|
|
||||||
async def request(self, method: str, path: str, json: dict | None = None, **kwargs):
|
|
||||||
response = await self.wallet.client.request(
|
|
||||||
method, f"{self.wallet.endpoint}{path}", json=json, **kwargs
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
response.raise_for_status()
|
|
||||||
except HTTPStatusError as exc:
|
|
||||||
json = exc.response.json()
|
|
||||||
if json:
|
|
||||||
error = json.get("error") or json
|
|
||||||
raise HTTPException(
|
|
||||||
exc.response.status_code, detail=error.get("message")
|
|
||||||
) from exc
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
def get(self, path: str, **kwargs):
|
|
||||||
return self.request("GET", path, **kwargs)
|
|
||||||
|
|
||||||
async def _get_id(self) -> str:
|
|
||||||
info = await self.get("/v1/getinfo")
|
|
||||||
return info["identity_pubkey"]
|
|
||||||
|
|
||||||
async def get_peer_ids(self) -> list[str]:
|
|
||||||
response = await self.get("/v1/peers")
|
|
||||||
return [p["pub_key"] for p in response["peers"]]
|
|
||||||
|
|
||||||
async def connect_peer(self, uri: str):
|
|
||||||
try:
|
|
||||||
pubkey, host = uri.split("@")
|
|
||||||
except ValueError as exc:
|
|
||||||
raise HTTPException(400, detail="Invalid peer URI") from exc
|
|
||||||
await self.request(
|
|
||||||
"POST",
|
|
||||||
"/v1/peers",
|
|
||||||
json={
|
|
||||||
"addr": {"pubkey": pubkey, "host": host},
|
|
||||||
"perm": True,
|
|
||||||
"timeout": 30,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
async def disconnect_peer(self, peer_id: str):
|
|
||||||
try:
|
|
||||||
await self.request("DELETE", "/v1/peers/" + peer_id)
|
|
||||||
except HTTPException as exc:
|
|
||||||
if "unable to disconnect" in exc.detail:
|
|
||||||
raise HTTPException(
|
|
||||||
HTTPStatus.BAD_REQUEST, detail="Peer is not connected"
|
|
||||||
) from exc
|
|
||||||
raise
|
|
||||||
|
|
||||||
async def _get_peer_info(self, peer_id: str) -> NodePeerInfo:
|
|
||||||
try:
|
|
||||||
response = await self.get("/v1/graph/node/" + peer_id)
|
|
||||||
except HTTPException:
|
|
||||||
return NodePeerInfo(id=peer_id)
|
|
||||||
node = response["node"]
|
|
||||||
return NodePeerInfo(
|
|
||||||
id=peer_id,
|
|
||||||
alias=node["alias"],
|
|
||||||
color=node["color"].strip("#"),
|
|
||||||
last_timestamp=node["last_update"],
|
|
||||||
addresses=[a["addr"] for a in node["addresses"]],
|
|
||||||
)
|
|
||||||
|
|
||||||
async def open_channel(
|
|
||||||
self,
|
|
||||||
peer_id: str,
|
|
||||||
local_amount: int,
|
|
||||||
push_amount: int | None = None,
|
|
||||||
fee_rate: int | None = None,
|
|
||||||
) -> ChannelPoint:
|
|
||||||
response = await self.request(
|
|
||||||
"POST",
|
|
||||||
"/v1/channels",
|
|
||||||
json={
|
|
||||||
"node_pubkey": _encode_bytes(peer_id),
|
|
||||||
"sat_per_vbyte": fee_rate,
|
|
||||||
"local_funding_amount": local_amount,
|
|
||||||
"push_sat": push_amount,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return ChannelPoint(
|
|
||||||
# WHY IS THIS REVERSED?!
|
|
||||||
funding_txid=bytes(
|
|
||||||
reversed(base64.b64decode(response["funding_txid_bytes"]))
|
|
||||||
).hex(),
|
|
||||||
output_index=response["output_index"],
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _close_channel(
|
|
||||||
self,
|
|
||||||
point: ChannelPoint,
|
|
||||||
force: bool = False,
|
|
||||||
):
|
|
||||||
async with self.wallet.client.stream(
|
|
||||||
"DELETE",
|
|
||||||
f"{self.wallet.endpoint}/v1/channels/{point.funding_txid}/{point.output_index}",
|
|
||||||
params={"force": force},
|
|
||||||
timeout=None,
|
|
||||||
) as stream:
|
|
||||||
async for chunk in stream.aiter_text():
|
|
||||||
if not chunk:
|
|
||||||
continue
|
|
||||||
chunk = json.loads(chunk)
|
|
||||||
if "error" in chunk:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
detail=chunk["error"].get("message"),
|
|
||||||
)
|
|
||||||
logger.info(f"LND Channel close update: {chunk.get('result')}")
|
|
||||||
|
|
||||||
async def close_channel(
|
|
||||||
self,
|
|
||||||
short_id: str | None = None,
|
|
||||||
point: ChannelPoint | None = None,
|
|
||||||
force: bool = False,
|
|
||||||
):
|
|
||||||
if short_id:
|
|
||||||
logger.debug(f"Closing channel with short_id: {short_id}")
|
|
||||||
if not point:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST, detail="Channel point required"
|
|
||||||
)
|
|
||||||
|
|
||||||
asyncio.create_task(self._close_channel(point, force)) # noqa: RUF006
|
|
||||||
|
|
||||||
async def set_channel_fee(self, channel_id: str, base_msat: int, ppm: int):
|
|
||||||
# https://lightning.engineering/api-docs/api/lnd/lightning/update-channel-policy/
|
|
||||||
channel = await self.get_channel(channel_id)
|
|
||||||
if not channel:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Channel not found"
|
|
||||||
)
|
|
||||||
if not channel.point:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST, detail="Channel point required"
|
|
||||||
)
|
|
||||||
await self.request(
|
|
||||||
"POST",
|
|
||||||
"/v1/chanpolicy",
|
|
||||||
json={
|
|
||||||
"base_fee_msat": base_msat,
|
|
||||||
"fee_rate_ppm": ppm,
|
|
||||||
"chan_point": {
|
|
||||||
"funding_txid_str": channel.point.funding_txid,
|
|
||||||
"output_index": channel.point.output_index,
|
|
||||||
},
|
|
||||||
# https://docs.lightning.engineering/lightning-network-tools/lnd/optimal-configuration-of-a-routing-node#channel-defaults
|
|
||||||
"time_lock_delta": 80,
|
|
||||||
# 'max_htlc_msat': <uint64>,
|
|
||||||
# 'min_htlc_msat': <uint64>,
|
|
||||||
# 'inbound_fee': <InboundFee>,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
async def get_channel(self, channel_id: str) -> NodeChannel | None:
|
|
||||||
channel_info = await self.get(f"/v1/graph/edge/{channel_id}")
|
|
||||||
info = await self.get("/v1/getinfo")
|
|
||||||
if info["identity_pubkey"] == channel_info["node1_pub"]:
|
|
||||||
my_node_key = "node1"
|
|
||||||
peer_node_key = "node2"
|
|
||||||
else:
|
|
||||||
my_node_key = "node2"
|
|
||||||
peer_node_key = "node1"
|
|
||||||
peer_id = channel_info[f"{peer_node_key}_pub"]
|
|
||||||
peer_b64 = _encode_urlsafe_bytes(peer_id)
|
|
||||||
channels = await self.get(f"/v1/channels?peer={peer_b64}")
|
|
||||||
if "error" in channel_info and "error" in channels:
|
|
||||||
logger.debug("LND get_channel", channels)
|
|
||||||
return None
|
|
||||||
if len(channels["channels"]) == 0:
|
|
||||||
logger.debug(f"LND get_channel no channels founds with id {peer_b64}")
|
|
||||||
return None
|
|
||||||
for channel in channels["channels"]:
|
|
||||||
if channel["chan_id"] == channel_id:
|
|
||||||
peer_info = await self.get_peer_info(peer_id)
|
|
||||||
return NodeChannel(
|
|
||||||
id=channel.get("chan_id"),
|
|
||||||
peer_id=peer_info.id,
|
|
||||||
name=peer_info.alias,
|
|
||||||
color=peer_info.color,
|
|
||||||
state=(
|
|
||||||
ChannelState.ACTIVE
|
|
||||||
if channel["active"]
|
|
||||||
else ChannelState.INACTIVE
|
|
||||||
),
|
|
||||||
fee_ppm=channel_info[f"{my_node_key}_policy"][
|
|
||||||
"fee_rate_milli_msat"
|
|
||||||
],
|
|
||||||
fee_base_msat=channel_info[f"{my_node_key}_policy"][
|
|
||||||
"fee_base_msat"
|
|
||||||
],
|
|
||||||
point=_parse_channel_point(channel["channel_point"]),
|
|
||||||
balance=ChannelBalance(
|
|
||||||
local_msat=msat(channel["local_balance"]),
|
|
||||||
remote_msat=msat(channel["remote_balance"]),
|
|
||||||
total_msat=msat(channel["capacity"]),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def get_channels(self) -> list[NodeChannel]:
|
|
||||||
normal, pending, closed = await asyncio.gather(
|
|
||||||
self.get("/v1/channels"),
|
|
||||||
self.get("/v1/channels/pending"),
|
|
||||||
self.get("/v1/channels/closed"),
|
|
||||||
)
|
|
||||||
|
|
||||||
channels = []
|
|
||||||
|
|
||||||
async def parse_pending(raw_channels, state):
|
|
||||||
for channel in raw_channels:
|
|
||||||
channel = channel["channel"]
|
|
||||||
info = await self.get_peer_info(channel["remote_node_pub"])
|
|
||||||
channels.append(
|
|
||||||
NodeChannel(
|
|
||||||
peer_id=info.id,
|
|
||||||
state=state,
|
|
||||||
name=info.alias,
|
|
||||||
color=info.color,
|
|
||||||
id=channel.get("chan_id", "node is for pending channels"),
|
|
||||||
point=_parse_channel_point(channel["channel_point"]),
|
|
||||||
balance=ChannelBalance(
|
|
||||||
local_msat=msat(channel["local_balance"]),
|
|
||||||
remote_msat=msat(channel["remote_balance"]),
|
|
||||||
total_msat=msat(channel["capacity"]),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
await parse_pending(pending["pending_open_channels"], ChannelState.PENDING)
|
|
||||||
await parse_pending(
|
|
||||||
pending["pending_force_closing_channels"], ChannelState.CLOSED
|
|
||||||
)
|
|
||||||
await parse_pending(pending["waiting_close_channels"], ChannelState.CLOSED)
|
|
||||||
|
|
||||||
for channel in closed["channels"]:
|
|
||||||
info = await self.get_peer_info(channel["remote_pubkey"])
|
|
||||||
channels.append(
|
|
||||||
NodeChannel(
|
|
||||||
id=channel.get("chan_id", "node is for closing channels"),
|
|
||||||
peer_id=info.id,
|
|
||||||
state=ChannelState.CLOSED,
|
|
||||||
name=info.alias,
|
|
||||||
color=info.color,
|
|
||||||
point=_parse_channel_point(channel["channel_point"]),
|
|
||||||
balance=ChannelBalance(
|
|
||||||
local_msat=0,
|
|
||||||
remote_msat=0,
|
|
||||||
total_msat=msat(channel["capacity"]),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
for channel in normal["channels"]:
|
|
||||||
info = await self.get_peer_info(channel["remote_pubkey"])
|
|
||||||
channels.append(
|
|
||||||
NodeChannel(
|
|
||||||
id=channel["chan_id"],
|
|
||||||
short_id=channel["chan_id"],
|
|
||||||
point=_parse_channel_point(channel["channel_point"]),
|
|
||||||
peer_id=channel["remote_pubkey"],
|
|
||||||
balance=ChannelBalance(
|
|
||||||
local_msat=msat(channel["local_balance"]),
|
|
||||||
remote_msat=msat(channel["remote_balance"]),
|
|
||||||
total_msat=msat(channel["capacity"]),
|
|
||||||
),
|
|
||||||
state=(
|
|
||||||
ChannelState.ACTIVE
|
|
||||||
if channel["active"]
|
|
||||||
else ChannelState.INACTIVE
|
|
||||||
),
|
|
||||||
# name=channel['peer_alias'],
|
|
||||||
name=info.alias,
|
|
||||||
color=info.color,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return channels
|
|
||||||
|
|
||||||
async def get_public_info(self) -> PublicNodeInfo:
|
|
||||||
info = await self.get("/v1/getinfo")
|
|
||||||
channels = await self.get_channels()
|
|
||||||
return PublicNodeInfo(
|
|
||||||
backend_name="LND",
|
|
||||||
id=info["identity_pubkey"],
|
|
||||||
color=info["color"].lstrip("#"),
|
|
||||||
alias=info["alias"],
|
|
||||||
num_peers=info["num_peers"],
|
|
||||||
blockheight=info["block_height"],
|
|
||||||
addresses=info["uris"],
|
|
||||||
channel_stats=ChannelStats.from_list(channels),
|
|
||||||
)
|
|
||||||
|
|
||||||
async def get_info(self) -> NodeInfoResponse:
|
|
||||||
public = await self.get_public_info()
|
|
||||||
onchain = await self.get("/v1/balance/blockchain")
|
|
||||||
fee_report = await self.get("/v1/fees")
|
|
||||||
balance = await self.get("/v1/balance/channels")
|
|
||||||
return NodeInfoResponse(
|
|
||||||
**public.dict(),
|
|
||||||
onchain_balance_sat=onchain["total_balance"],
|
|
||||||
onchain_confirmed_sat=onchain["confirmed_balance"],
|
|
||||||
balance_msat=balance["local_balance"]["msat"],
|
|
||||||
fees=NodeFees(
|
|
||||||
total_msat=0,
|
|
||||||
daily_msat=fee_report["day_fee_sum"],
|
|
||||||
weekly_msat=fee_report["week_fee_sum"],
|
|
||||||
monthly_msat=fee_report["month_fee_sum"],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
async def get_payments(
|
|
||||||
self, filters: Filters[NodePaymentsFilters]
|
|
||||||
) -> Page[NodePayment]:
|
|
||||||
count_key = "node:payments_count"
|
|
||||||
payments_count = cache.get(count_key)
|
|
||||||
if not payments_count and filters.offset:
|
|
||||||
# this forces fetching the payments count
|
|
||||||
await self.get_payments(Filters(limit=1))
|
|
||||||
payments_count = cache.get(count_key)
|
|
||||||
|
|
||||||
if filters.offset and payments_count:
|
|
||||||
index_offset = max(payments_count + 1 - filters.offset, 0)
|
|
||||||
else:
|
|
||||||
index_offset = 0
|
|
||||||
|
|
||||||
response = await self.get(
|
|
||||||
"/v1/payments",
|
|
||||||
params={
|
|
||||||
"index_offset": index_offset,
|
|
||||||
"max_payments": filters.limit,
|
|
||||||
"include_incomplete": True,
|
|
||||||
"reversed": True,
|
|
||||||
"count_total_payments": not index_offset,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if not filters.offset:
|
|
||||||
payments_count = int(response["total_num_payments"])
|
|
||||||
|
|
||||||
cache.set(count_key, payments_count)
|
|
||||||
|
|
||||||
payments = [
|
|
||||||
NodePayment(
|
|
||||||
payment_hash=payment["payment_hash"],
|
|
||||||
pending=payment["status"] == "IN_FLIGHT",
|
|
||||||
amount=payment["value_msat"],
|
|
||||||
fee=payment["fee_msat"],
|
|
||||||
time=payment["creation_date"],
|
|
||||||
destination=(
|
|
||||||
await self.get_peer_info(
|
|
||||||
payment["htlcs"][0]["route"]["hops"][-1]["pub_key"]
|
|
||||||
)
|
|
||||||
if payment["htlcs"]
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
bolt11=payment["payment_request"],
|
|
||||||
preimage=payment["payment_preimage"],
|
|
||||||
)
|
|
||||||
for payment in response["payments"]
|
|
||||||
]
|
|
||||||
|
|
||||||
payments.sort(key=lambda p: p.time, reverse=True)
|
|
||||||
|
|
||||||
return Page(data=payments, total=payments_count or 0)
|
|
||||||
|
|
||||||
async def get_invoices(
|
|
||||||
self, filters: Filters[NodeInvoiceFilters]
|
|
||||||
) -> Page[NodeInvoice]:
|
|
||||||
last_invoice_key = "node:last_invoice_index"
|
|
||||||
last_invoice_index = cache.get(last_invoice_key)
|
|
||||||
if not last_invoice_index and filters.offset:
|
|
||||||
# this forces fetching the last invoice index so
|
|
||||||
await self.get_invoices(Filters(limit=1))
|
|
||||||
last_invoice_index = cache.get(last_invoice_key)
|
|
||||||
|
|
||||||
if filters.offset and last_invoice_index:
|
|
||||||
index_offset = max(last_invoice_index + 1 - filters.offset, 0)
|
|
||||||
else:
|
|
||||||
index_offset = 0
|
|
||||||
|
|
||||||
response = await self.get(
|
|
||||||
"/v1/invoices",
|
|
||||||
params={
|
|
||||||
"index_offset": index_offset,
|
|
||||||
"num_max_invoices": filters.limit,
|
|
||||||
"reversed": True,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if not filters.offset:
|
|
||||||
last_invoice_index = int(response["last_index_offset"])
|
|
||||||
|
|
||||||
cache.set(last_invoice_key, last_invoice_index)
|
|
||||||
|
|
||||||
invoices = [
|
|
||||||
NodeInvoice(
|
|
||||||
payment_hash=_decode_bytes(invoice["r_hash"]),
|
|
||||||
amount=invoice["value_msat"],
|
|
||||||
memo=invoice["memo"],
|
|
||||||
pending=invoice["state"] == "OPEN",
|
|
||||||
paid_at=invoice["settle_date"],
|
|
||||||
expiry=int(invoice["creation_date"]) + int(invoice["expiry"]),
|
|
||||||
preimage=_decode_bytes(invoice["r_preimage"]),
|
|
||||||
bolt11=invoice["payment_request"],
|
|
||||||
)
|
|
||||||
for invoice in reversed(response["invoices"])
|
|
||||||
]
|
|
||||||
|
|
||||||
return Page(
|
|
||||||
data=invoices,
|
|
||||||
total=last_invoice_index or 0,
|
|
||||||
)
|
|
||||||
+57
-69
@@ -4,16 +4,16 @@ import importlib
|
|||||||
import importlib.metadata
|
import importlib.metadata
|
||||||
import inspect
|
import inspect
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from hashlib import sha256
|
|
||||||
from os import path
|
from os import path
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from time import gmtime, strftime, time
|
from time import gmtime, strftime, time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
import httpx
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pydantic import BaseModel, BaseSettings, Extra, Field, validator
|
from pydantic import BaseModel, BaseSettings, Extra, Field, validator
|
||||||
|
|
||||||
@@ -260,7 +260,7 @@ class ThemesSettings(LNbitsSettings):
|
|||||||
lnbits_ad_space_enabled: bool = Field(default=False)
|
lnbits_ad_space_enabled: bool = Field(default=False)
|
||||||
lnbits_allowed_currencies: list[str] = Field(default=[])
|
lnbits_allowed_currencies: list[str] = Field(default=[])
|
||||||
lnbits_default_accounting_currency: str | None = Field(default=None)
|
lnbits_default_accounting_currency: str | None = Field(default=None)
|
||||||
lnbits_qr_logo: str = Field(default="/static/images/logos/lnbits.png")
|
lnbits_qr_logo: str = Field(default="/static/favicon.ico")
|
||||||
lnbits_default_reaction: str = Field(default="confettiBothSides")
|
lnbits_default_reaction: str = Field(default="confettiBothSides")
|
||||||
lnbits_default_theme: str = Field(default="salvador")
|
lnbits_default_theme: str = Field(default="salvador")
|
||||||
lnbits_default_border: str = Field(default="hard-border")
|
lnbits_default_border: str = Field(default="hard-border")
|
||||||
@@ -271,13 +271,12 @@ class ThemesSettings(LNbitsSettings):
|
|||||||
class OpsSettings(LNbitsSettings):
|
class OpsSettings(LNbitsSettings):
|
||||||
lnbits_baseurl: str = Field(default="http://127.0.0.1:5000/")
|
lnbits_baseurl: str = Field(default="http://127.0.0.1:5000/")
|
||||||
lnbits_hide_api: bool = Field(default=False)
|
lnbits_hide_api: bool = Field(default=False)
|
||||||
lnbits_denomination: str = Field(default="sats")
|
|
||||||
|
|
||||||
|
|
||||||
class FeeSettings(LNbitsSettings):
|
class FeeSettings(LNbitsSettings):
|
||||||
lnbits_reserve_fee_min: int = Field(default=2000)
|
lnbits_reserve_fee_min: int = Field(default=2000, ge=0)
|
||||||
lnbits_reserve_fee_percent: float = Field(default=1.0)
|
lnbits_reserve_fee_percent: float = Field(default=1.0, ge=0)
|
||||||
lnbits_service_fee: float = Field(default=0)
|
lnbits_service_fee: float = Field(default=0, ge=0)
|
||||||
lnbits_service_fee_ignore_internal: bool = Field(default=True)
|
lnbits_service_fee_ignore_internal: bool = Field(default=True)
|
||||||
lnbits_service_fee_max: int = Field(default=0)
|
lnbits_service_fee_max: int = Field(default=0)
|
||||||
lnbits_service_fee_wallet: str | None = Field(default=None)
|
lnbits_service_fee_wallet: str | None = Field(default=None)
|
||||||
@@ -293,9 +292,9 @@ class FeeSettings(LNbitsSettings):
|
|||||||
|
|
||||||
|
|
||||||
class ExchangeProvidersSettings(LNbitsSettings):
|
class ExchangeProvidersSettings(LNbitsSettings):
|
||||||
lnbits_exchange_rate_cache_seconds: int = Field(default=30)
|
lnbits_exchange_rate_cache_seconds: int = Field(default=30, ge=0)
|
||||||
lnbits_exchange_history_size: int = Field(default=60)
|
lnbits_exchange_history_size: int = Field(default=60, ge=0)
|
||||||
lnbits_exchange_history_refresh_interval_seconds: int = Field(default=300)
|
lnbits_exchange_history_refresh_interval_seconds: int = Field(default=300, ge=0)
|
||||||
|
|
||||||
lnbits_exchange_rate_providers: list[ExchangeRateProvider] = Field(
|
lnbits_exchange_rate_providers: list[ExchangeRateProvider] = Field(
|
||||||
default=[
|
default=[
|
||||||
@@ -360,7 +359,7 @@ class ExchangeProvidersSettings(LNbitsSettings):
|
|||||||
|
|
||||||
|
|
||||||
class SecuritySettings(LNbitsSettings):
|
class SecuritySettings(LNbitsSettings):
|
||||||
lnbits_rate_limit_no: str = Field(default="200")
|
lnbits_rate_limit_no: int = Field(default=200, ge=0)
|
||||||
lnbits_rate_limit_unit: str = Field(default="minute")
|
lnbits_rate_limit_unit: str = Field(default="minute")
|
||||||
lnbits_allowed_ips: list[str] = Field(default=[])
|
lnbits_allowed_ips: list[str] = Field(default=[])
|
||||||
lnbits_blocked_ips: list[str] = Field(default=[])
|
lnbits_blocked_ips: list[str] = Field(default=[])
|
||||||
@@ -368,16 +367,16 @@ class SecuritySettings(LNbitsSettings):
|
|||||||
default=["^(?!\\d+\\.\\d+\\.\\d+\\.\\d+$)(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}$"]
|
default=["^(?!\\d+\\.\\d+\\.\\d+\\.\\d+$)(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}$"]
|
||||||
)
|
)
|
||||||
|
|
||||||
lnbits_wallet_limit_max_balance: int = Field(default=0)
|
lnbits_wallet_limit_max_balance: int = Field(default=0, ge=0)
|
||||||
lnbits_wallet_limit_daily_max_withdraw: int = Field(default=0)
|
lnbits_wallet_limit_daily_max_withdraw: int = Field(default=0, ge=0)
|
||||||
lnbits_wallet_limit_secs_between_trans: int = Field(default=0)
|
lnbits_wallet_limit_secs_between_trans: int = Field(default=0, ge=0)
|
||||||
lnbits_only_allow_incoming_payments: bool = Field(default=False)
|
lnbits_only_allow_incoming_payments: bool = Field(default=False)
|
||||||
lnbits_watchdog_switch_to_voidwallet: bool = Field(default=False)
|
lnbits_watchdog_switch_to_voidwallet: bool = Field(default=False)
|
||||||
lnbits_watchdog_interval_minutes: int = Field(default=60)
|
lnbits_watchdog_interval_minutes: int = Field(default=60, gt=0)
|
||||||
lnbits_watchdog_delta: int = Field(default=1_000_000)
|
lnbits_watchdog_delta: int = Field(default=1_000_000, gt=0)
|
||||||
|
|
||||||
lnbits_max_outgoing_payment_amount_sats: int = Field(default=10_000_000)
|
lnbits_max_outgoing_payment_amount_sats: int = Field(default=10_000_000, ge=0)
|
||||||
lnbits_max_incoming_payment_amount_sats: int = Field(default=10_000_000)
|
lnbits_max_incoming_payment_amount_sats: int = Field(default=10_000_000, ge=0)
|
||||||
|
|
||||||
def is_wallet_max_balance_exceeded(self, amount):
|
def is_wallet_max_balance_exceeded(self, amount):
|
||||||
return (
|
return (
|
||||||
@@ -396,6 +395,7 @@ class NotificationsSettings(LNbitsSettings):
|
|||||||
lnbits_telegram_notifications_chat_id: str = Field(default="")
|
lnbits_telegram_notifications_chat_id: str = Field(default="")
|
||||||
lnbits_email_notifications_enabled: bool = Field(default=False)
|
lnbits_email_notifications_enabled: bool = Field(default=False)
|
||||||
lnbits_email_notifications_email: str = Field(default="")
|
lnbits_email_notifications_email: str = Field(default="")
|
||||||
|
lnbits_email_notifications_username: str = Field(default="")
|
||||||
lnbits_email_notifications_password: str = Field(default="")
|
lnbits_email_notifications_password: str = Field(default="")
|
||||||
lnbits_email_notifications_server: str = Field(default="smtp.protonmail.ch")
|
lnbits_email_notifications_server: str = Field(default="smtp.protonmail.ch")
|
||||||
lnbits_email_notifications_port: int = Field(default=587)
|
lnbits_email_notifications_port: int = Field(default=587)
|
||||||
@@ -406,13 +406,18 @@ class NotificationsSettings(LNbitsSettings):
|
|||||||
notification_balance_delta_changed: bool = Field(default=True)
|
notification_balance_delta_changed: bool = Field(default=True)
|
||||||
lnbits_notification_server_start_stop: bool = Field(default=True)
|
lnbits_notification_server_start_stop: bool = Field(default=True)
|
||||||
lnbits_notification_watchdog: bool = Field(default=False)
|
lnbits_notification_watchdog: bool = Field(default=False)
|
||||||
lnbits_notification_server_status_hours: int = Field(default=24)
|
lnbits_notification_server_status_hours: int = Field(default=24, gt=0)
|
||||||
lnbits_notification_incoming_payment_amount_sats: int = Field(default=1_000_000)
|
lnbits_notification_incoming_payment_amount_sats: int = Field(
|
||||||
lnbits_notification_outgoing_payment_amount_sats: int = Field(default=1_000_000)
|
default=1_000_000, ge=0
|
||||||
|
)
|
||||||
|
lnbits_notification_outgoing_payment_amount_sats: int = Field(
|
||||||
|
default=1_000_000, ge=0
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FakeWalletFundingSource(LNbitsSettings):
|
class FakeWalletFundingSource(LNbitsSettings):
|
||||||
fake_wallet_secret: str = Field(default="ToTheMoon1")
|
fake_wallet_secret: str = Field(default="ToTheMoon1")
|
||||||
|
lnbits_denomination: str = Field(default="sats")
|
||||||
|
|
||||||
|
|
||||||
class LNbitsFundingSource(LNbitsSettings):
|
class LNbitsFundingSource(LNbitsSettings):
|
||||||
@@ -535,7 +540,7 @@ class BoltzFundingSource(LNbitsSettings):
|
|||||||
|
|
||||||
|
|
||||||
class LightningSettings(LNbitsSettings):
|
class LightningSettings(LNbitsSettings):
|
||||||
lightning_invoice_expiry: int = Field(default=3600)
|
lightning_invoice_expiry: int = Field(default=3600, gt=0)
|
||||||
|
|
||||||
|
|
||||||
class FundingSourcesSettings(
|
class FundingSourcesSettings(
|
||||||
@@ -562,7 +567,7 @@ class FundingSourcesSettings(
|
|||||||
lnbits_backend_wallet_class: str = Field(default="VoidWallet")
|
lnbits_backend_wallet_class: str = Field(default="VoidWallet")
|
||||||
# How long to wait for the payment to be confirmed before returning a pending status
|
# How long to wait for the payment to be confirmed before returning a pending status
|
||||||
# It will not fail the payment, it will make it return pending after the timeout
|
# It will not fail the payment, it will make it return pending after the timeout
|
||||||
lnbits_funding_source_pay_invoice_wait_seconds: int = Field(default=5)
|
lnbits_funding_source_pay_invoice_wait_seconds: int = Field(default=5, ge=0)
|
||||||
|
|
||||||
|
|
||||||
class WebPushSettings(LNbitsSettings):
|
class WebPushSettings(LNbitsSettings):
|
||||||
@@ -601,7 +606,7 @@ class AuthMethods(Enum):
|
|||||||
|
|
||||||
|
|
||||||
class AuthSettings(LNbitsSettings):
|
class AuthSettings(LNbitsSettings):
|
||||||
auth_token_expire_minutes: int = Field(default=525600)
|
auth_token_expire_minutes: int = Field(default=525600, gt=0)
|
||||||
auth_all_methods = [a.value for a in AuthMethods]
|
auth_all_methods = [a.value for a in AuthMethods]
|
||||||
auth_allowed_methods: list[str] = Field(
|
auth_allowed_methods: list[str] = Field(
|
||||||
default=[
|
default=[
|
||||||
@@ -611,7 +616,7 @@ class AuthSettings(LNbitsSettings):
|
|||||||
)
|
)
|
||||||
# How many seconds after login the user is allowed to update its credentials.
|
# How many seconds after login the user is allowed to update its credentials.
|
||||||
# A fresh login is required afterwards.
|
# A fresh login is required afterwards.
|
||||||
auth_credetials_update_threshold: int = Field(default=120)
|
auth_credetials_update_threshold: int = Field(default=120, gt=0)
|
||||||
|
|
||||||
def is_auth_method_allowed(self, method: AuthMethods):
|
def is_auth_method_allowed(self, method: AuthMethods):
|
||||||
return method.value in self.auth_allowed_methods
|
return method.value in self.auth_allowed_methods
|
||||||
@@ -643,7 +648,7 @@ class AuditSettings(LNbitsSettings):
|
|||||||
lnbits_audit_enabled: bool = Field(default=True)
|
lnbits_audit_enabled: bool = Field(default=True)
|
||||||
|
|
||||||
# number of days to keep the audit entry
|
# number of days to keep the audit entry
|
||||||
lnbits_audit_retention_days: int = Field(default=7)
|
lnbits_audit_retention_days: int = Field(default=7, ge=0)
|
||||||
|
|
||||||
lnbits_audit_log_ip_address: bool = Field(default=False)
|
lnbits_audit_log_ip_address: bool = Field(default=False)
|
||||||
lnbits_audit_log_path_params: bool = Field(default=True)
|
lnbits_audit_log_path_params: bool = Field(default=True)
|
||||||
@@ -780,7 +785,7 @@ class EnvSettings(LNbitsSettings):
|
|||||||
debug_database: bool = Field(default=False)
|
debug_database: bool = Field(default=False)
|
||||||
bundle_assets: bool = Field(default=True)
|
bundle_assets: bool = Field(default=True)
|
||||||
host: str = Field(default="127.0.0.1")
|
host: str = Field(default="127.0.0.1")
|
||||||
port: int = Field(default=5000)
|
port: int = Field(default=5000, gt=0)
|
||||||
forwarded_allow_ips: str = Field(default="*")
|
forwarded_allow_ips: str = Field(default="*")
|
||||||
lnbits_title: str = Field(default="LNbits API")
|
lnbits_title: str = Field(default="LNbits API")
|
||||||
lnbits_path: str = Field(default=".")
|
lnbits_path: str = Field(default=".")
|
||||||
@@ -792,24 +797,27 @@ class EnvSettings(LNbitsSettings):
|
|||||||
enable_log_to_file: bool = Field(default=True)
|
enable_log_to_file: bool = Field(default=True)
|
||||||
log_rotation: str = Field(default="100 MB")
|
log_rotation: str = Field(default="100 MB")
|
||||||
log_retention: str = Field(default="3 months")
|
log_retention: str = Field(default="3 months")
|
||||||
server_startup_time: int = Field(default=time())
|
|
||||||
cleanup_wallets_days: int = Field(default=90)
|
cleanup_wallets_days: int = Field(default=90, ge=0)
|
||||||
funding_source_max_retries: int = Field(default=4)
|
funding_source_max_retries: int = Field(default=4, ge=0)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_default_extension_path(self) -> bool:
|
def has_default_extension_path(self) -> bool:
|
||||||
return self.lnbits_extensions_path == "lnbits"
|
return self.lnbits_extensions_path == "lnbits"
|
||||||
|
|
||||||
@property
|
def check_auth_secret_key(self):
|
||||||
def lnbits_server_up_time(self) -> str:
|
if self.auth_secret_key:
|
||||||
up_time = int(time() - self.server_startup_time)
|
return
|
||||||
return strftime("%H:%M:%S", gmtime(up_time))
|
if not os.path.isdir(settings.lnbits_data_folder):
|
||||||
|
os.mkdir(settings.lnbits_data_folder)
|
||||||
|
auth_key_file = Path(settings.lnbits_data_folder, ".lnbits_auth_key")
|
||||||
class SaaSSettings(LNbitsSettings):
|
if auth_key_file.is_file():
|
||||||
lnbits_saas_callback: str | None = Field(default=None)
|
with open(auth_key_file) as file:
|
||||||
lnbits_saas_secret: str | None = Field(default=None)
|
self.auth_secret_key = file.readline()
|
||||||
lnbits_saas_instance_id: str | None = Field(default=None)
|
return
|
||||||
|
self.auth_secret_key = uuid4().hex
|
||||||
|
with open(auth_key_file, "w+") as file:
|
||||||
|
file.write(self.auth_secret_key)
|
||||||
|
|
||||||
|
|
||||||
class PersistenceSettings(LNbitsSettings):
|
class PersistenceSettings(LNbitsSettings):
|
||||||
@@ -861,6 +869,13 @@ class TransientSettings(InstalledExtensionsSettings, ExchangeHistorySettings):
|
|||||||
|
|
||||||
lnbits_all_extensions_ids: set[str] = Field(default=[])
|
lnbits_all_extensions_ids: set[str] = Field(default=[])
|
||||||
|
|
||||||
|
server_startup_time: int = Field(default=time())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lnbits_server_up_time(self) -> str:
|
||||||
|
up_time = int(time() - self.server_startup_time)
|
||||||
|
return strftime("%H:%M:%S", gmtime(up_time))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def readonly_fields(cls):
|
def readonly_fields(cls):
|
||||||
return [f for f in inspect.signature(cls).parameters if not f.startswith("_")]
|
return [f for f in inspect.signature(cls).parameters if not f.startswith("_")]
|
||||||
@@ -869,7 +884,6 @@ class TransientSettings(InstalledExtensionsSettings, ExchangeHistorySettings):
|
|||||||
class ReadOnlySettings(
|
class ReadOnlySettings(
|
||||||
EnvSettings,
|
EnvSettings,
|
||||||
ExtensionsInstallSettings,
|
ExtensionsInstallSettings,
|
||||||
SaaSSettings,
|
|
||||||
PersistenceSettings,
|
PersistenceSettings,
|
||||||
SuperUserSettings,
|
SuperUserSettings,
|
||||||
):
|
):
|
||||||
@@ -948,31 +962,6 @@ def set_cli_settings(**kwargs):
|
|||||||
setattr(settings, key, value)
|
setattr(settings, key, value)
|
||||||
|
|
||||||
|
|
||||||
def send_admin_user_to_saas():
|
|
||||||
if settings.lnbits_saas_callback:
|
|
||||||
with httpx.Client() as client:
|
|
||||||
headers = {
|
|
||||||
"Content-Type": "application/json; charset=utf-8",
|
|
||||||
"X-API-KEY": settings.lnbits_saas_secret,
|
|
||||||
}
|
|
||||||
payload = {
|
|
||||||
"instance_id": settings.lnbits_saas_instance_id,
|
|
||||||
"adminuser": settings.super_user,
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
client.post(
|
|
||||||
settings.lnbits_saas_callback,
|
|
||||||
headers=headers,
|
|
||||||
json=payload,
|
|
||||||
)
|
|
||||||
logger.success("sent super_user to saas application")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(
|
|
||||||
"error sending super_user to saas:"
|
|
||||||
f" {settings.lnbits_saas_callback}. Error: {e!s}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
readonly_variables = ReadOnlySettings.readonly_fields()
|
readonly_variables = ReadOnlySettings.readonly_fields()
|
||||||
transient_variables = TransientSettings.readonly_fields()
|
transient_variables = TransientSettings.readonly_fields()
|
||||||
|
|
||||||
@@ -981,9 +970,8 @@ settings = Settings()
|
|||||||
settings.lnbits_path = str(path.dirname(path.realpath(__file__)))
|
settings.lnbits_path = str(path.dirname(path.realpath(__file__)))
|
||||||
|
|
||||||
settings.version = importlib.metadata.version("lnbits")
|
settings.version = importlib.metadata.version("lnbits")
|
||||||
settings.auth_secret_key = (
|
|
||||||
settings.auth_secret_key or sha256(settings.super_user.encode("utf-8")).hexdigest()
|
settings.check_auth_secret_key()
|
||||||
)
|
|
||||||
|
|
||||||
if not settings.user_agent:
|
if not settings.user_agent:
|
||||||
settings.user_agent = f"LNbits/{settings.version}"
|
settings.user_agent = f"LNbits/{settings.version}"
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+7
-5
File diff suppressed because one or more lines are too long
@@ -110,16 +110,16 @@ body[data-theme=bitcoin].body--light {
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-theme=bitcoin] .bg-primary {
|
[data-theme=bitcoin] .bg-primary {
|
||||||
background: #ff9853 !important;
|
background: #ea611d !important;
|
||||||
}
|
}
|
||||||
[data-theme=bitcoin] .text-primary {
|
[data-theme=bitcoin] .text-primary {
|
||||||
color: #ff9853 !important;
|
color: #ea611d !important;
|
||||||
}
|
}
|
||||||
[data-theme=bitcoin] .bg-secondary {
|
[data-theme=bitcoin] .bg-secondary {
|
||||||
background: #ff7353 !important;
|
background: #e56f35 !important;
|
||||||
}
|
}
|
||||||
[data-theme=bitcoin] .text-secondary {
|
[data-theme=bitcoin] .text-secondary {
|
||||||
color: #ff7353 !important;
|
color: #e56f35 !important;
|
||||||
}
|
}
|
||||||
[data-theme=bitcoin] .bg-dark {
|
[data-theme=bitcoin] .bg-dark {
|
||||||
background: #2d293b !important;
|
background: #2d293b !important;
|
||||||
@@ -134,10 +134,10 @@ body[data-theme=bitcoin].body--light {
|
|||||||
color: #333646 !important;
|
color: #333646 !important;
|
||||||
}
|
}
|
||||||
[data-theme=bitcoin] .bg-marginal-bg {
|
[data-theme=bitcoin] .bg-marginal-bg {
|
||||||
background: #2d293b !important;
|
background: #000000 !important;
|
||||||
}
|
}
|
||||||
[data-theme=bitcoin] .text-marginal-bg {
|
[data-theme=bitcoin] .text-marginal-bg {
|
||||||
color: #2d293b !important;
|
color: #000000 !important;
|
||||||
}
|
}
|
||||||
[data-theme=bitcoin] .bg-marginal-text {
|
[data-theme=bitcoin] .bg-marginal-text {
|
||||||
background: #fff !important;
|
background: #fff !important;
|
||||||
|
|||||||
@@ -65,8 +65,9 @@ window.localisation.en = {
|
|||||||
view_github: 'View on GitHub',
|
view_github: 'View on GitHub',
|
||||||
voidwallet_active: 'VoidWallet is active! Payments disabled',
|
voidwallet_active: 'VoidWallet is active! Payments disabled',
|
||||||
use_with_caution: 'USE WITH CAUTION - {name} wallet is still in BETA',
|
use_with_caution: 'USE WITH CAUTION - {name} wallet is still in BETA',
|
||||||
service_fee: 'Service fee: {amount} % per transaction',
|
service_fee_badge: 'Service fee: {amount} % per transaction',
|
||||||
service_fee_max: 'Service fee: {amount} % per transaction (max {max} sats)',
|
service_fee_max_badge:
|
||||||
|
'Service fee: {amount} % per transaction (max {max} {denom})',
|
||||||
service_fee_tooltip:
|
service_fee_tooltip:
|
||||||
'Service fee charged by the LNbits server admin per outgoing transaction',
|
'Service fee charged by the LNbits server admin per outgoing transaction',
|
||||||
toggle_darkmode: 'Toggle Dark Mode',
|
toggle_darkmode: 'Toggle Dark Mode',
|
||||||
@@ -175,6 +176,7 @@ window.localisation.en = {
|
|||||||
payment_proof: 'Payment Proof',
|
payment_proof: 'Payment Proof',
|
||||||
update: 'Update',
|
update: 'Update',
|
||||||
update_available: 'Update {version} available!',
|
update_available: 'Update {version} available!',
|
||||||
|
funding_sources: 'Funding Sources',
|
||||||
latest_update: 'You are on the latest version {version}.',
|
latest_update: 'You are on the latest version {version}.',
|
||||||
notifications: 'Notifications',
|
notifications: 'Notifications',
|
||||||
notifications_configure: 'Configure Notifications',
|
notifications_configure: 'Configure Notifications',
|
||||||
@@ -198,9 +200,13 @@ window.localisation.en = {
|
|||||||
|
|
||||||
notifications_email_config: 'Email Configuration',
|
notifications_email_config: 'Email Configuration',
|
||||||
notifications_enable_email: 'Enable Email',
|
notifications_enable_email: 'Enable Email',
|
||||||
notifications_enable_email_desc: 'Send notfications over Email',
|
notifications_enable_email_desc: 'Send notfications over email',
|
||||||
|
notifications_send_test_email: 'Send test email',
|
||||||
notifications_send_email: 'Send email',
|
notifications_send_email: 'Send email',
|
||||||
notifications_send_email_desc: 'Email you will send from',
|
notifications_send_email_desc: 'Email you will send from',
|
||||||
|
notifications_send_email_username: 'Username',
|
||||||
|
notifications_send_email_username_desc:
|
||||||
|
'Username, will use the email if not set',
|
||||||
notifications_send_email_password: 'Send email password',
|
notifications_send_email_password: 'Send email password',
|
||||||
notifications_send_email_password_desc:
|
notifications_send_email_password_desc:
|
||||||
'Password for the email you will send from',
|
'Password for the email you will send from',
|
||||||
@@ -310,6 +316,7 @@ window.localisation.en = {
|
|||||||
password: 'Password',
|
password: 'Password',
|
||||||
password_config: 'Password Config',
|
password_config: 'Password Config',
|
||||||
password_repeat: 'Password repeat',
|
password_repeat: 'Password repeat',
|
||||||
|
update_password: 'Update Password',
|
||||||
change_password: 'Change Password',
|
change_password: 'Change Password',
|
||||||
update_credentials: 'Update Credentials',
|
update_credentials: 'Update Credentials',
|
||||||
update_pubkey: 'Update Public Key',
|
update_pubkey: 'Update Public Key',
|
||||||
@@ -500,8 +507,8 @@ window.localisation.en = {
|
|||||||
denomination: 'Denomination',
|
denomination: 'Denomination',
|
||||||
denomination_hint: 'The name for the FakeWallet token',
|
denomination_hint: 'The name for the FakeWallet token',
|
||||||
denomination_error: 'Denomination must be 3 characters, or `sats`',
|
denomination_error: 'Denomination must be 3 characters, or `sats`',
|
||||||
ui_qr_code_logo: 'QR Code Logo',
|
ui_qr_code_logo: 'QR Code/Favicon Logo',
|
||||||
ui_qr_code_logo_hint: 'URL to logo image in QR code',
|
ui_qr_code_logo_hint: 'QR code and favicon logo url',
|
||||||
ui_custom_image: 'Custom Image',
|
ui_custom_image: 'Custom Image',
|
||||||
ui_custom_image_label: 'URL to custom image',
|
ui_custom_image_label: 'URL to custom image',
|
||||||
ui_custom_image_hint: 'Image showed at homepage/login',
|
ui_custom_image_hint: 'Image showed at homepage/login',
|
||||||
|
|||||||
@@ -438,7 +438,7 @@ window.AdminPageLogic = {
|
|||||||
LNbits.api
|
LNbits.api
|
||||||
.request('GET', '/admin/api/v1/restart/')
|
.request('GET', '/admin/api/v1/restart/')
|
||||||
.then(response => {
|
.then(response => {
|
||||||
Quasar.Notify.create({
|
this.$q.notify({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: 'Success! Restarted Server',
|
message: 'Success! Restarted Server',
|
||||||
icon: null
|
icon: null
|
||||||
@@ -450,7 +450,29 @@ window.AdminPageLogic = {
|
|||||||
formatDate(date) {
|
formatDate(date) {
|
||||||
return moment(date * 1000).fromNow()
|
return moment(date * 1000).fromNow()
|
||||||
},
|
},
|
||||||
|
sendTestEmail() {
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'GET',
|
||||||
|
'/admin/api/v1/testemail',
|
||||||
|
this.g.user.wallets[0].adminkey
|
||||||
|
)
|
||||||
|
.then(response => {
|
||||||
|
if (response.data.status === 'error') {
|
||||||
|
throw new Error(response.data.message)
|
||||||
|
}
|
||||||
|
this.$q.notify({
|
||||||
|
message: 'Test email sent!',
|
||||||
|
color: 'positive'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.$q.notify({
|
||||||
|
message: error.message,
|
||||||
|
color: 'negative'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
getAudit() {
|
getAudit() {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request('GET', '/admin/api/v1/audit', this.g.user.wallets[0].adminkey)
|
.request('GET', '/admin/api/v1/audit', this.g.user.wallets[0].adminkey)
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ window.LNbits = {
|
|||||||
})
|
})
|
||||||
obj.walletOptions = obj.wallets.map(obj => {
|
obj.walletOptions = obj.wallets.map(obj => {
|
||||||
return {
|
return {
|
||||||
label: [obj.name, ' - ', obj.id].join(''),
|
label: [obj.name, ' - ', obj.id.substring(0, 5), '...'].join(''),
|
||||||
value: obj.id
|
value: obj.id
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ window.app.component('lnbits-funding-sources', {
|
|||||||
'FakeWallet',
|
'FakeWallet',
|
||||||
'Fake Wallet',
|
'Fake Wallet',
|
||||||
{
|
{
|
||||||
fake_wallet_secret: 'Secret'
|
fake_wallet_secret: 'Secret',
|
||||||
|
lnbits_denomination: '"sats" or 3 Letter Custom Denomination'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ $themes: (
|
|||||||
marginal-text: #fff
|
marginal-text: #fff
|
||||||
),
|
),
|
||||||
'bitcoin': (
|
'bitcoin': (
|
||||||
primary: #ff9853,
|
primary: #ea611d,
|
||||||
secondary: #ff7353,
|
secondary: #e56f35,
|
||||||
dark: #2d293b,
|
dark: #2d293b,
|
||||||
info: #333646,
|
info: #333646,
|
||||||
marginal-bg: #2d293b,
|
marginal-bg: #000000,
|
||||||
marginal-text: #fff
|
marginal-text: #fff
|
||||||
),
|
),
|
||||||
'freedom': (
|
'freedom': (
|
||||||
|
|||||||
Vendored
+45
-85
@@ -1,4 +1,4 @@
|
|||||||
// Axios v1.7.7 Copyright (c) 2024 Matt Zabriskie and contributors
|
/*! Axios v1.8.2 Copyright (c) 2025 Matt Zabriskie and contributors */
|
||||||
(function (global, factory) {
|
(function (global, factory) {
|
||||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||||
typeof define === 'function' && define.amd ? define(factory) :
|
typeof define === 'function' && define.amd ? define(factory) :
|
||||||
@@ -1266,23 +1266,6 @@
|
|||||||
var toFiniteNumber = function toFiniteNumber(value, defaultValue) {
|
var toFiniteNumber = function toFiniteNumber(value, defaultValue) {
|
||||||
return value != null && Number.isFinite(value = +value) ? value : defaultValue;
|
return value != null && Number.isFinite(value = +value) ? value : defaultValue;
|
||||||
};
|
};
|
||||||
var ALPHA = 'abcdefghijklmnopqrstuvwxyz';
|
|
||||||
var DIGIT = '0123456789';
|
|
||||||
var ALPHABET = {
|
|
||||||
DIGIT: DIGIT,
|
|
||||||
ALPHA: ALPHA,
|
|
||||||
ALPHA_DIGIT: ALPHA + ALPHA.toUpperCase() + DIGIT
|
|
||||||
};
|
|
||||||
var generateString = function generateString() {
|
|
||||||
var size = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 16;
|
|
||||||
var alphabet = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ALPHABET.ALPHA_DIGIT;
|
|
||||||
var str = '';
|
|
||||||
var length = alphabet.length;
|
|
||||||
while (size--) {
|
|
||||||
str += alphabet[Math.random() * length | 0];
|
|
||||||
}
|
|
||||||
return str;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the thing is a FormData object, return true, otherwise return false.
|
* If the thing is a FormData object, return true, otherwise return false.
|
||||||
@@ -1399,8 +1382,6 @@
|
|||||||
findKey: findKey,
|
findKey: findKey,
|
||||||
global: _global,
|
global: _global,
|
||||||
isContextDefined: isContextDefined,
|
isContextDefined: isContextDefined,
|
||||||
ALPHABET: ALPHABET,
|
|
||||||
generateString: generateString,
|
|
||||||
isSpecCompliantForm: isSpecCompliantForm,
|
isSpecCompliantForm: isSpecCompliantForm,
|
||||||
toJSONObject: toJSONObject,
|
toJSONObject: toJSONObject,
|
||||||
isAsyncFn: isAsyncFn,
|
isAsyncFn: isAsyncFn,
|
||||||
@@ -1735,7 +1716,7 @@
|
|||||||
*
|
*
|
||||||
* @param {string} url The base of the url (e.g., http://www.google.com)
|
* @param {string} url The base of the url (e.g., http://www.google.com)
|
||||||
* @param {object} [params] The params to be appended
|
* @param {object} [params] The params to be appended
|
||||||
* @param {?object} options
|
* @param {?(object|Function)} options
|
||||||
*
|
*
|
||||||
* @returns {string} The formatted url
|
* @returns {string} The formatted url
|
||||||
*/
|
*/
|
||||||
@@ -1745,6 +1726,11 @@
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
var _encode = options && options.encode || encode;
|
var _encode = options && options.encode || encode;
|
||||||
|
if (utils$1.isFunction(options)) {
|
||||||
|
options = {
|
||||||
|
serialize: options
|
||||||
|
};
|
||||||
|
}
|
||||||
var serializeFn = options && options.serialize;
|
var serializeFn = options && options.serialize;
|
||||||
var serializedParams;
|
var serializedParams;
|
||||||
if (serializeFn) {
|
if (serializeFn) {
|
||||||
@@ -2642,60 +2628,14 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
var isURLSameOrigin = platform.hasStandardBrowserEnv ?
|
var isURLSameOrigin = platform.hasStandardBrowserEnv ? function (origin, isMSIE) {
|
||||||
// Standard browser envs have full support of the APIs needed to test
|
return function (url) {
|
||||||
// whether the request URL is of the same origin as current location.
|
url = new URL(url, platform.origin);
|
||||||
function standardBrowserEnv() {
|
return origin.protocol === url.protocol && origin.host === url.host && (isMSIE || origin.port === url.port);
|
||||||
var msie = platform.navigator && /(msie|trident)/i.test(platform.navigator.userAgent);
|
|
||||||
var urlParsingNode = document.createElement('a');
|
|
||||||
var originURL;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse a URL to discover its components
|
|
||||||
*
|
|
||||||
* @param {String} url The URL to be parsed
|
|
||||||
* @returns {Object}
|
|
||||||
*/
|
|
||||||
function resolveURL(url) {
|
|
||||||
var href = url;
|
|
||||||
if (msie) {
|
|
||||||
// IE needs attribute set twice to normalize properties
|
|
||||||
urlParsingNode.setAttribute('href', href);
|
|
||||||
href = urlParsingNode.href;
|
|
||||||
}
|
|
||||||
urlParsingNode.setAttribute('href', href);
|
|
||||||
|
|
||||||
// urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils
|
|
||||||
return {
|
|
||||||
href: urlParsingNode.href,
|
|
||||||
protocol: urlParsingNode.protocol ? urlParsingNode.protocol.replace(/:$/, '') : '',
|
|
||||||
host: urlParsingNode.host,
|
|
||||||
search: urlParsingNode.search ? urlParsingNode.search.replace(/^\?/, '') : '',
|
|
||||||
hash: urlParsingNode.hash ? urlParsingNode.hash.replace(/^#/, '') : '',
|
|
||||||
hostname: urlParsingNode.hostname,
|
|
||||||
port: urlParsingNode.port,
|
|
||||||
pathname: urlParsingNode.pathname.charAt(0) === '/' ? urlParsingNode.pathname : '/' + urlParsingNode.pathname
|
|
||||||
};
|
|
||||||
}
|
|
||||||
originURL = resolveURL(window.location.href);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if a URL shares the same origin as the current location
|
|
||||||
*
|
|
||||||
* @param {String} requestURL The URL to test
|
|
||||||
* @returns {boolean} True if URL shares the same origin, otherwise false
|
|
||||||
*/
|
|
||||||
return function isURLSameOrigin(requestURL) {
|
|
||||||
var parsed = utils$1.isString(requestURL) ? resolveURL(requestURL) : requestURL;
|
|
||||||
return parsed.protocol === originURL.protocol && parsed.host === originURL.host;
|
|
||||||
};
|
};
|
||||||
}() :
|
}(new URL(platform.origin), platform.navigator && /(msie|trident)/i.test(platform.navigator.userAgent)) : function () {
|
||||||
// Non standard browser envs (web workers, react-native) lack needed support.
|
return true;
|
||||||
function nonStandardBrowserEnv() {
|
};
|
||||||
return function isURLSameOrigin() {
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
}();
|
|
||||||
|
|
||||||
var cookies = platform.hasStandardBrowserEnv ?
|
var cookies = platform.hasStandardBrowserEnv ?
|
||||||
// Standard browser envs support document.cookie
|
// Standard browser envs support document.cookie
|
||||||
@@ -2761,8 +2701,9 @@
|
|||||||
*
|
*
|
||||||
* @returns {string} The combined full path
|
* @returns {string} The combined full path
|
||||||
*/
|
*/
|
||||||
function buildFullPath(baseURL, requestedURL) {
|
function buildFullPath(baseURL, requestedURL, allowAbsoluteUrls) {
|
||||||
if (baseURL && !isAbsoluteURL(requestedURL)) {
|
var isRelativeUrl = !isAbsoluteURL(requestedURL);
|
||||||
|
if (baseURL && isRelativeUrl || allowAbsoluteUrls == false) {
|
||||||
return combineURLs(baseURL, requestedURL);
|
return combineURLs(baseURL, requestedURL);
|
||||||
}
|
}
|
||||||
return requestedURL;
|
return requestedURL;
|
||||||
@@ -2785,7 +2726,7 @@
|
|||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
config2 = config2 || {};
|
config2 = config2 || {};
|
||||||
var config = {};
|
var config = {};
|
||||||
function getMergedValue(target, source, caseless) {
|
function getMergedValue(target, source, prop, caseless) {
|
||||||
if (utils$1.isPlainObject(target) && utils$1.isPlainObject(source)) {
|
if (utils$1.isPlainObject(target) && utils$1.isPlainObject(source)) {
|
||||||
return utils$1.merge.call({
|
return utils$1.merge.call({
|
||||||
caseless: caseless
|
caseless: caseless
|
||||||
@@ -2799,11 +2740,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line consistent-return
|
// eslint-disable-next-line consistent-return
|
||||||
function mergeDeepProperties(a, b, caseless) {
|
function mergeDeepProperties(a, b, prop, caseless) {
|
||||||
if (!utils$1.isUndefined(b)) {
|
if (!utils$1.isUndefined(b)) {
|
||||||
return getMergedValue(a, b, caseless);
|
return getMergedValue(a, b, prop, caseless);
|
||||||
} else if (!utils$1.isUndefined(a)) {
|
} else if (!utils$1.isUndefined(a)) {
|
||||||
return getMergedValue(undefined, a, caseless);
|
return getMergedValue(undefined, a, prop, caseless);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2860,8 +2801,8 @@
|
|||||||
socketPath: defaultToConfig2,
|
socketPath: defaultToConfig2,
|
||||||
responseEncoding: defaultToConfig2,
|
responseEncoding: defaultToConfig2,
|
||||||
validateStatus: mergeDirectKeys,
|
validateStatus: mergeDirectKeys,
|
||||||
headers: function headers(a, b) {
|
headers: function headers(a, b, prop) {
|
||||||
return mergeDeepProperties(headersToObject(a), headersToObject(b), true);
|
return mergeDeepProperties(headersToObject(a), headersToObject(b), prop, true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
utils$1.forEach(Object.keys(Object.assign({}, config1, config2)), function computeConfigValue(prop) {
|
utils$1.forEach(Object.keys(Object.assign({}, config1, config2)), function computeConfigValue(prop) {
|
||||||
@@ -3717,7 +3658,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var VERSION = "1.7.7";
|
var VERSION = "1.8.2";
|
||||||
|
|
||||||
var validators$1 = {};
|
var validators$1 = {};
|
||||||
|
|
||||||
@@ -3756,6 +3697,13 @@
|
|||||||
return validator ? validator(value, opt, opts) : true;
|
return validator ? validator(value, opt, opts) : true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
validators$1.spelling = function spelling(correctSpelling) {
|
||||||
|
return function (value, opt) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn("".concat(opt, " is likely a misspelling of ").concat(correctSpelling));
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assert object's properties type
|
* Assert object's properties type
|
||||||
@@ -3838,7 +3786,8 @@
|
|||||||
_context.prev = 6;
|
_context.prev = 6;
|
||||||
_context.t0 = _context["catch"](0);
|
_context.t0 = _context["catch"](0);
|
||||||
if (_context.t0 instanceof Error) {
|
if (_context.t0 instanceof Error) {
|
||||||
Error.captureStackTrace ? Error.captureStackTrace(dummy = {}) : dummy = new Error();
|
dummy = {};
|
||||||
|
Error.captureStackTrace ? Error.captureStackTrace(dummy) : dummy = new Error();
|
||||||
|
|
||||||
// slice off the Error: ... line
|
// slice off the Error: ... line
|
||||||
stack = dummy.stack ? dummy.stack.replace(/^.+\n/, '') : '';
|
stack = dummy.stack ? dummy.stack.replace(/^.+\n/, '') : '';
|
||||||
@@ -3901,6 +3850,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set config.allowAbsoluteUrls
|
||||||
|
if (config.allowAbsoluteUrls !== undefined) ; else if (this.defaults.allowAbsoluteUrls !== undefined) {
|
||||||
|
config.allowAbsoluteUrls = this.defaults.allowAbsoluteUrls;
|
||||||
|
} else {
|
||||||
|
config.allowAbsoluteUrls = true;
|
||||||
|
}
|
||||||
|
validator.assertOptions(config, {
|
||||||
|
baseUrl: validators.spelling('baseURL'),
|
||||||
|
withXsrfToken: validators.spelling('withXSRFToken')
|
||||||
|
}, true);
|
||||||
|
|
||||||
// Set config.method
|
// Set config.method
|
||||||
config.method = (config.method || this.defaults.method || 'get').toLowerCase();
|
config.method = (config.method || this.defaults.method || 'get').toLowerCase();
|
||||||
|
|
||||||
@@ -3968,7 +3928,7 @@
|
|||||||
key: "getUri",
|
key: "getUri",
|
||||||
value: function getUri(config) {
|
value: function getUri(config) {
|
||||||
config = mergeConfig(this.defaults, config);
|
config = mergeConfig(this.defaults, config);
|
||||||
var fullPath = buildFullPath(config.baseURL, config.url);
|
var fullPath = buildFullPath(config.baseURL, config.url, config.allowAbsoluteUrls);
|
||||||
return buildURL(fullPath, config.params, config.paramsSerializer);
|
return buildURL(fullPath, config.params, config.paramsSerializer);
|
||||||
}
|
}
|
||||||
}]);
|
}]);
|
||||||
|
|||||||
+3
-3
File diff suppressed because one or more lines are too long
@@ -89,11 +89,11 @@
|
|||||||
>
|
>
|
||||||
{% if LNBITS_SERVICE_FEE_MAX > 0 %}
|
{% if LNBITS_SERVICE_FEE_MAX > 0 %}
|
||||||
<span
|
<span
|
||||||
v-text='$t("service_fee_max", { amount: "{{ LNBITS_SERVICE_FEE }}", max: "{{ LNBITS_SERVICE_FEE_MAX }}"})'
|
v-text='$t("service_fee_max_badge", { amount: "{{ LNBITS_SERVICE_FEE }}", max: "{{ LNBITS_SERVICE_FEE_MAX }}", denom: "{{ LNBITS_DENOMINATION }}"})'
|
||||||
></span>
|
></span>
|
||||||
{%else%}
|
{%else%}
|
||||||
<span
|
<span
|
||||||
v-text='$t("service_fee", { amount: "{{ LNBITS_SERVICE_FEE }}" })'
|
v-text='$t("service_fee_badge", { amount: "{{ LNBITS_SERVICE_FEE}}"})'
|
||||||
></span>
|
></span>
|
||||||
{%endif%}
|
{%endif%}
|
||||||
<q-tooltip
|
<q-tooltip
|
||||||
|
|||||||
@@ -1057,7 +1057,16 @@
|
|||||||
|
|
||||||
<template id="lnbits-funding-sources">
|
<template id="lnbits-funding-sources">
|
||||||
<div class="funding-sources">
|
<div class="funding-sources">
|
||||||
<h6 class="q-mt-xl q-mb-md">Funding Sources</h6>
|
<h6 class="q-my-none q-mb-sm">
|
||||||
|
<span v-text="$t('funding_sources')"></span>
|
||||||
|
<q-btn
|
||||||
|
round
|
||||||
|
flat
|
||||||
|
@click="this.hideInput = !this.hideInput"
|
||||||
|
:icon="this.hideInput ? 'visibility_off' : 'visibility'"
|
||||||
|
></q-btn>
|
||||||
|
</h6>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<p>Active Funding<small> (Requires server restart)</small></p>
|
<p>Active Funding<small> (Requires server restart)</small></p>
|
||||||
@@ -1095,13 +1104,6 @@
|
|||||||
:label="prop.label"
|
:label="prop.label"
|
||||||
:hint="prop.hint"
|
:hint="prop.hint"
|
||||||
>
|
>
|
||||||
<template v-slot:append>
|
|
||||||
<q-icon
|
|
||||||
:name="hideInput ? 'visibility_off' : 'visibility'"
|
|
||||||
class="cursor-pointer"
|
|
||||||
@click="this.hideInput = !this.hideInput"
|
|
||||||
></q-icon>
|
|
||||||
</template>
|
|
||||||
</q-input>
|
</q-input>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -87,13 +87,15 @@ class LNbitsWallet(Wallet):
|
|||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
data = r.json()
|
data = r.json()
|
||||||
|
|
||||||
if r.is_error or "bolt11" not in data:
|
# Backwards compatibility for pre-v1 which used the key "payment_request"
|
||||||
|
payment_str = data.get("bolt11") or data.get("payment_request")
|
||||||
|
if r.is_error or not payment_str:
|
||||||
error_message = data["detail"] if "detail" in data else r.text
|
error_message = data["detail"] if "detail" in data else r.text
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
False, None, None, f"Server error: '{error_message}'"
|
False, None, None, f"Server error: '{error_message}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
return InvoiceResponse(True, data["checking_id"], data["bolt11"], None)
|
return InvoiceResponse(True, data["checking_id"], payment_str, None)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
False, None, None, "Server error: 'invalid json response'"
|
False, None, None, "Server error: 'invalid json response'"
|
||||||
|
|||||||
Generated
+27
-22
@@ -6,7 +6,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "lnbits",
|
"name": "lnbits",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.8.2",
|
||||||
"chart.js": "^4.4.4",
|
"chart.js": "^4.4.4",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"nostr-tools": "^2.7.2",
|
"nostr-tools": "^2.7.2",
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
"showdown": "^2.1.0",
|
"showdown": "^2.1.0",
|
||||||
"underscore": "^1.13.7",
|
"underscore": "^1.13.7",
|
||||||
"vue": "3.5.8",
|
"vue": "3.5.8",
|
||||||
"vue-i18n": "^10.0.5",
|
"vue-i18n": "^10.0.6",
|
||||||
"vue-qrcode-reader": "^5.5.10",
|
"vue-qrcode-reader": "^5.5.10",
|
||||||
"vue-router": "4.4.5",
|
"vue-router": "4.4.5",
|
||||||
"vuex": "4.1.0"
|
"vuex": "4.1.0"
|
||||||
@@ -72,12 +72,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@intlify/core-base": {
|
"node_modules/@intlify/core-base": {
|
||||||
"version": "10.0.5",
|
"version": "10.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-10.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-10.0.6.tgz",
|
||||||
"integrity": "sha512-F3snDTQs0MdvnnyzTDTVkOYVAZOE/MHwRvF7mn7Jw1yuih4NrFYLNYIymGlLmq4HU2iIdzYsZ7f47bOcwY73XQ==",
|
"integrity": "sha512-/NINGvy7t8qSCyyuqMIPmHS6CBQjqPIPVOps0Rb7xWrwwkwHJKtahiFnW1HC4iQVhzoYwEW6Js0923zTScLDiA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@intlify/message-compiler": "10.0.5",
|
"@intlify/message-compiler": "10.0.6",
|
||||||
"@intlify/shared": "10.0.5"
|
"@intlify/shared": "10.0.6"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 16"
|
"node": ">= 16"
|
||||||
@@ -87,11 +88,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@intlify/message-compiler": {
|
"node_modules/@intlify/message-compiler": {
|
||||||
"version": "10.0.5",
|
"version": "10.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-10.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-10.0.6.tgz",
|
||||||
"integrity": "sha512-6GT1BJ852gZ0gItNZN2krX5QAmea+cmdjMvsWohArAZ3GmHdnNANEcF9JjPXAMRtQ6Ux5E269ymamg/+WU6tQA==",
|
"integrity": "sha512-QcUYprK+e4X2lU6eJDxLuf/mUtCuVPj2RFBoFRlJJxK3wskBejzlRvh1Q0lQCi9tDOnD4iUK1ftcGylE3X3idA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@intlify/shared": "10.0.5",
|
"@intlify/shared": "10.0.6",
|
||||||
"source-map-js": "^1.0.2"
|
"source-map-js": "^1.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -102,9 +104,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@intlify/shared": {
|
"node_modules/@intlify/shared": {
|
||||||
"version": "10.0.5",
|
"version": "10.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-10.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-10.0.6.tgz",
|
||||||
"integrity": "sha512-bmsP4L2HqBF6i6uaMqJMcFBONVjKt+siGluRq4Ca4C0q7W2eMaVZr8iCgF9dKbcVXutftkC7D6z2SaSMmLiDyA==",
|
"integrity": "sha512-2xqwm05YPpo7TM//+v0bzS0FWiTzsjpSMnWdt7ZXs5/ZfQIedSuBXIrskd8HZ7c/cZzo1G9ALHTksnv/74vk/Q==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 16"
|
"node": ">= 16"
|
||||||
},
|
},
|
||||||
@@ -399,9 +402,10 @@
|
|||||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.7.7",
|
"version": "1.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz",
|
||||||
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
|
"integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.6",
|
"follow-redirects": "^1.15.6",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
@@ -1301,12 +1305,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vue-i18n": {
|
"node_modules/vue-i18n": {
|
||||||
"version": "10.0.5",
|
"version": "10.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-10.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-10.0.6.tgz",
|
||||||
"integrity": "sha512-9/gmDlCblz3i8ypu/afiIc/SUIfTTE1mr0mZhb9pk70xo2csHAM9mp2gdQ3KD2O0AM3Hz/5ypb+FycTj/lHlPQ==",
|
"integrity": "sha512-pQPspK5H4srzlu+47+HEY2tmiY3GyYIvSPgSBdQaYVWv7t1zj1t9p1FvHlxBXyJ17t9stG/Vxj+pykrvPWBLeQ==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@intlify/core-base": "10.0.5",
|
"@intlify/core-base": "10.0.6",
|
||||||
"@intlify/shared": "10.0.5",
|
"@intlify/shared": "10.0.6",
|
||||||
"@vue/devtools-api": "^6.5.0"
|
"@vue/devtools-api": "^6.5.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
+2
-2
@@ -20,7 +20,7 @@
|
|||||||
"sass": "^1.78.0"
|
"sass": "^1.78.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.8.2",
|
||||||
"chart.js": "^4.4.4",
|
"chart.js": "^4.4.4",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"qrcode.vue": "^3.4.1",
|
"qrcode.vue": "^3.4.1",
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
"nostr-tools": "^2.7.2",
|
"nostr-tools": "^2.7.2",
|
||||||
"underscore": "^1.13.7",
|
"underscore": "^1.13.7",
|
||||||
"vue": "3.5.8",
|
"vue": "3.5.8",
|
||||||
"vue-i18n": "^10.0.5",
|
"vue-i18n": "^10.0.6",
|
||||||
"vue-qrcode-reader": "^5.5.10",
|
"vue-qrcode-reader": "^5.5.10",
|
||||||
"vue-router": "4.4.5",
|
"vue-router": "4.4.5",
|
||||||
"vuex": "4.1.0"
|
"vuex": "4.1.0"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "lnbits"
|
name = "lnbits"
|
||||||
version = "1.0.0-rc8"
|
version = "1.0.0-rc9"
|
||||||
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
||||||
authors = ["Alan Bits <alan@lnbits.com>"]
|
authors = ["Alan Bits <alan@lnbits.com>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|||||||
Reference in New Issue
Block a user