Compare commits

...
Author SHA1 Message Date
dni ⚡andGitHub 475ae5736e chore: update to v1.0.0-rc9 (#3043) 2025-03-12 12:29:01 +01:00
PatMulliganandGitHub d7f0f4da0c create backwards compatibility for <=v1.0 (#3041)
in v0.12.12, the key is "payment_request" instead of the new "bolt11"
2025-03-12 12:27:24 +01:00
Tiago VasconcelosandGitHub f7b3444be4 fix: unfuck the swap mess (#3023) 2025-03-12 12:14:20 +01:00
ArcandGitHub 28e32a54a4 tweak: use qr code image for favicon as well (#3031)
use qr code image for favicon as well
2025-03-12 12:12:43 +01:00
ArcandGitHub 1833adc0a1 Tweak: moved custom denomination to fakewallet funding source (#3030) 2025-03-12 12:10:58 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>dni ⚡
b759ec7468 chore(deps): bump axios from 1.7.7 to 1.8.2 (#3042)
* chore(deps): bump axios from 1.7.7 to 1.8.2

Bumps [axios](https://github.com/axios/axios) from 1.7.7 to 1.8.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.7.7...v1.8.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* bundle

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: dni  <office@dnilabs.com>
2025-03-12 12:10:27 +01:00
2661d12ff1 tweak: fixes service fee badge (#3029)
* tweak: fixes service fee badge

---------

Co-authored-by: dni  <office@dnilabs.com>
2025-03-12 12:08:08 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>dni ⚡
ed419f27ac chore(deps): bump vue-i18n from 10.0.5 to 10.0.6 (#3022)
* chore(deps): bump vue-i18n from 10.0.5 to 10.0.6

Bumps [vue-i18n](https://github.com/intlify/vue-i18n/tree/HEAD/packages/vue-i18n) from 10.0.5 to 10.0.6.
- [Release notes](https://github.com/intlify/vue-i18n/releases)
- [Changelog](https://github.com/intlify/vue-i18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/intlify/vue-i18n/commits/v10.0.6/packages/vue-i18n)

---
updated-dependencies:
- dependency-name: vue-i18n
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* bundle

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: dni  <office@dnilabs.com>
2025-03-12 12:01:18 +01:00
dni ⚡andGitHub c3efd48108 chore: use latest 1.X poetry version (#3039) 2025-03-11 09:54:36 +01:00
ArcandGitHub 4a28c22d79 tweak: fix 'test email' btn (#3026) 2025-03-10 18:37:11 +00:00
ArcandGitHub 0d0eb36de4 tweak: fix funding source hide (#3027)
added to title rather than all the inputs
2025-03-10 18:36:48 +00:00
ArcandGitHub 2aa73bfe75 tweak: punchy bitcoin theme (#3028) 2025-03-10 18:36:23 +00:00
Vlad StanandGitHub ebd080192c feat: return payment status in the websocket response (#3025) 2025-03-10 12:48:40 +02:00
Tiago VasconcelosandGitHub 41abe63f18 hide text on small screen (#3021) 2025-03-07 14:23:46 +00:00
dni ⚡andGitHub f61471b0f2 feat: add test email and improve on email notifications (#3018) 2025-03-05 09:53:40 +02:00
Vlad StanandGitHub 9bd037b6e7 [fix] settings cleanup (#3013) 2025-03-04 16:30:54 +02:00
a4ca88b6a4 tweak: dont show full id on wallet select box (#3017)
* tweak: dont show full id on wallet select box
* chore bundle

---------

Co-authored-by: dni  <office@dnilabs.com>
2025-03-04 11:23:02 +01:00
ArcandGitHub f2b9aafc51 Tweak: Changed naming for password area (#3016) 2025-03-04 11:13:41 +01:00
30 changed files with 316 additions and 300 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ inputs:
default: "3.10"
poetry-version:
description: "Poetry Version"
default: "1.7.0"
default: "1.8.5"
node-version:
description: "Node Version"
default: "20.x"
-2
View File
@@ -24,7 +24,6 @@ from .users import (
check_admin_settings,
create_user_account,
create_user_account_no_ckeck,
init_admin_settings,
update_user_account,
update_user_extensions,
)
@@ -58,7 +57,6 @@ __all__ = [
"check_admin_settings",
"create_user_account",
"create_user_account_no_ckeck",
"init_admin_settings",
"update_user_account",
"update_user_extensions",
# websockets
+39 -23
View File
@@ -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.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.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()
async def send_email_notification(message: str) -> dict:
await send_email(
settings.lnbits_email_notifications_server,
settings.lnbits_email_notifications_port,
settings.lnbits_email_notifications_password,
settings.lnbits_email_notifications_email,
settings.lnbits_email_notifications_to_emails,
"LNbits Notification",
message,
)
return {"status": "ok"}
async def send_email_notification(
message: str, subject: str = "LNbits Notification"
) -> dict:
if not settings.lnbits_email_notifications_enabled:
return {"status": "error", "message": "Email notifications are disabled"}
try:
await send_email(
settings.lnbits_email_notifications_server,
settings.lnbits_email_notifications_port,
settings.lnbits_email_notifications_username,
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(
server: str,
port: int,
username: str,
password: str,
from_email: str,
to_emails: list,
to_emails: list[str],
subject: 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["From"] = from_email
msg["To"] = ", ".join(to_emails)
msg["Subject"] = subject
msg.attach(MIMEText(message, "plain"))
try:
with smtplib.SMTP(server, port) as smtp_server:
smtp_server.starttls()
smtp_server.login(from_email, password)
smtp_server.sendmail(from_email, to_emails, msg.as_string())
logger.debug(f"Emails sent successfully to: {', '.join(to_emails)}")
except Exception as e:
logger.debug(f"Failed to send email: {e}")
username = username if len(username) > 0 else from_email
with smtplib.SMTP(server, port) as smtp_server:
smtp_server.starttls()
smtp_server.login(username, password)
smtp_server.sendmail(from_email, to_emails, msg.as_string())
return True
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(
json.dumps({"pending": payment.pending}), payment.payment_hash
json.dumps({"pending": payment.pending, "status": payment.status}),
payment.payment_hash,
)
-9
View File
@@ -8,7 +8,6 @@ from lnbits.core.models.extensions import UserExtension
from lnbits.settings import (
EditableSettings,
SuperSettings,
send_admin_user_to_saas,
settings,
)
@@ -154,14 +153,6 @@ async def check_admin_settings():
with open(Path(settings.lnbits_data_folder) / ".super_user", "w") as file:
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)
if account and account.extra and account.extra.provider == "env":
settings.first_install = True
@@ -150,7 +150,6 @@
<div class="col-sm-12">
<q-separator></q-separator>
</div>
<div class="col-12">
<strong v-text="$t('notifications_email_config')"></strong>
<q-item tag="label" v-ripple>
@@ -194,6 +193,24 @@
/>
</q-item-section>
</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-section>
<q-item-label
@@ -212,7 +229,17 @@
/>
</q-item-section>
</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-section>
<q-item-label
@@ -249,9 +276,6 @@
</div>
</q-item-section>
</q-item>
</div>
<div class="col">
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
@@ -53,17 +53,6 @@
:label="$t('lnbits_wallet')"
></q-input>
</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">
<p><span v-text="$t('ui_qr_code_logo')"></span></p>
<q-input
+5 -5
View File
@@ -45,7 +45,7 @@
<div class="row">
<div class="col">
<h4 class="q-my-none">
<span v-text="$t('password_config')"></span>
<span v-text="$t('password')"></span>
</h4>
</div>
<div class="col">
@@ -102,7 +102,7 @@
unelevated
color="primary"
class="float-right"
:label="$t('change_password')"
:label="$t('update_password')"
>
</q-btn>
</q-card-section>
@@ -110,7 +110,7 @@
<q-card-section>
<div class="col q-mb-sm">
<h4 class="q-my-none">
<span v-text="$t('pubkey')"></span>
Nostr <span v-text="$t('pubkey')"></span>
</h4>
</div>
<q-input
@@ -287,7 +287,7 @@
</q-btn>
<q-btn
@click="showUpdateCredentials()"
:label="$t('update_credentials')"
:label="$t('change_password')"
filled
color="primary"
class="float-right"
@@ -356,7 +356,7 @@
flat
@click="themeChoiceFunc('bitcoin')"
icon="circle"
color="orange"
color="deep-orange"
size="md"
><q-tooltip>bitcoin</q-tooltip>
</q-btn>
+14 -12
View File
@@ -87,18 +87,20 @@
>
{{SITE_TITLE}}
</h5>
<h6
class="q-my-sm"
v-if="'{{LNBITS_SHOW_HOME_PAGE_ELEMENTS}}' == 'True'"
>
{{SITE_TAGLINE}}
</h6>
<p
class="q-my-sm"
v-if="'{{LNBITS_SHOW_HOME_PAGE_ELEMENTS}}' == 'True'"
>
{{SITE_DESCRIPTION}}
</p>
<template v-if="$q.screen.gt.sm">
<h6
class="q-my-sm"
v-if="'{{LNBITS_SHOW_HOME_PAGE_ELEMENTS}}' == 'True'"
>
{{SITE_TAGLINE}}
</h6>
<p
class="q-my-sm"
v-if="'{{LNBITS_SHOW_HOME_PAGE_ELEMENTS}}' == 'True'"
>
{{SITE_DESCRIPTION}}
</p>
</template>
<!-- <div
class="gt-sm"
v-html="formatDescription"
+7 -7
View File
@@ -726,6 +726,13 @@
</div>
<div v-if="g.fiatTracking">
<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">
<span
v-text="parse.invoice.fiatAmount"
@@ -733,13 +740,6 @@
></span>
</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>
<q-separator></q-separator>
+13
View File
@@ -16,6 +16,7 @@ from lnbits.core.services import (
get_balance_delta,
update_cached_settings,
)
from lnbits.core.services.notifications import send_email_notification
from lnbits.core.services.settings import dict_to_settings
from lnbits.decorators import check_admin, check_super_user
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])
async def api_get_settings(
user: User = Depends(check_admin),
+1 -2
View File
@@ -1,5 +1,4 @@
from http import HTTPStatus
from pathlib import Path
from typing import Annotated, List, Optional, Union
from urllib.parse import urlencode, urlparse
@@ -37,7 +36,7 @@ generic_router = APIRouter(
@generic_router.get("/favicon.ico", response_class=FileResponse)
async def favicon():
return FileResponse(Path("lnbits", "static", "favicon.ico"))
return RedirectResponse(settings.lnbits_qr_logo)
@generic_router.get("/", response_class=HTMLResponse)
-5
View File
@@ -10,7 +10,6 @@ from urllib.parse import urlparse
import jinja2
import jwt
import shortuuid
from fastapi import Request
from fastapi.routing import APIRoute
from packaging import version
from pydantic.schema import field_schema
@@ -344,7 +343,3 @@ def path_segments(path: str) -> list[str]:
def normalize_path(path: Optional[str]) -> str:
path = path or ""
return "/" + "/".join(path_segments(path))
def normalized_path(request: Request) -> str:
return "/" + "/".join(path_segments(request.url.path))
+57 -69
View File
@@ -4,16 +4,16 @@ import importlib
import importlib.metadata
import inspect
import json
import os
import re
from datetime import datetime, timezone
from enum import Enum
from hashlib import sha256
from os import path
from pathlib import Path
from time import gmtime, strftime, time
from typing import Any
from uuid import uuid4
import httpx
from loguru import logger
from pydantic import BaseModel, BaseSettings, Extra, Field, validator
@@ -260,7 +260,7 @@ class ThemesSettings(LNbitsSettings):
lnbits_ad_space_enabled: bool = Field(default=False)
lnbits_allowed_currencies: list[str] = Field(default=[])
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_theme: str = Field(default="salvador")
lnbits_default_border: str = Field(default="hard-border")
@@ -271,13 +271,12 @@ class ThemesSettings(LNbitsSettings):
class OpsSettings(LNbitsSettings):
lnbits_baseurl: str = Field(default="http://127.0.0.1:5000/")
lnbits_hide_api: bool = Field(default=False)
lnbits_denomination: str = Field(default="sats")
class FeeSettings(LNbitsSettings):
lnbits_reserve_fee_min: int = Field(default=2000)
lnbits_reserve_fee_percent: float = Field(default=1.0)
lnbits_service_fee: float = Field(default=0)
lnbits_reserve_fee_min: int = Field(default=2000, ge=0)
lnbits_reserve_fee_percent: float = Field(default=1.0, ge=0)
lnbits_service_fee: float = Field(default=0, ge=0)
lnbits_service_fee_ignore_internal: bool = Field(default=True)
lnbits_service_fee_max: int = Field(default=0)
lnbits_service_fee_wallet: str | None = Field(default=None)
@@ -293,9 +292,9 @@ class FeeSettings(LNbitsSettings):
class ExchangeProvidersSettings(LNbitsSettings):
lnbits_exchange_rate_cache_seconds: int = Field(default=30)
lnbits_exchange_history_size: int = Field(default=60)
lnbits_exchange_history_refresh_interval_seconds: int = Field(default=300)
lnbits_exchange_rate_cache_seconds: int = Field(default=30, ge=0)
lnbits_exchange_history_size: int = Field(default=60, ge=0)
lnbits_exchange_history_refresh_interval_seconds: int = Field(default=300, ge=0)
lnbits_exchange_rate_providers: list[ExchangeRateProvider] = Field(
default=[
@@ -360,7 +359,7 @@ class ExchangeProvidersSettings(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_allowed_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,}$"]
)
lnbits_wallet_limit_max_balance: int = Field(default=0)
lnbits_wallet_limit_daily_max_withdraw: int = Field(default=0)
lnbits_wallet_limit_secs_between_trans: 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, ge=0)
lnbits_wallet_limit_secs_between_trans: int = Field(default=0, ge=0)
lnbits_only_allow_incoming_payments: bool = Field(default=False)
lnbits_watchdog_switch_to_voidwallet: bool = Field(default=False)
lnbits_watchdog_interval_minutes: int = Field(default=60)
lnbits_watchdog_delta: int = Field(default=1_000_000)
lnbits_watchdog_interval_minutes: int = Field(default=60, gt=0)
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_incoming_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, ge=0)
def is_wallet_max_balance_exceeded(self, amount):
return (
@@ -396,6 +395,7 @@ class NotificationsSettings(LNbitsSettings):
lnbits_telegram_notifications_chat_id: str = Field(default="")
lnbits_email_notifications_enabled: bool = Field(default=False)
lnbits_email_notifications_email: str = Field(default="")
lnbits_email_notifications_username: str = Field(default="")
lnbits_email_notifications_password: str = Field(default="")
lnbits_email_notifications_server: str = Field(default="smtp.protonmail.ch")
lnbits_email_notifications_port: int = Field(default=587)
@@ -406,13 +406,18 @@ class NotificationsSettings(LNbitsSettings):
notification_balance_delta_changed: bool = Field(default=True)
lnbits_notification_server_start_stop: bool = Field(default=True)
lnbits_notification_watchdog: bool = Field(default=False)
lnbits_notification_server_status_hours: int = Field(default=24)
lnbits_notification_incoming_payment_amount_sats: int = Field(default=1_000_000)
lnbits_notification_outgoing_payment_amount_sats: int = Field(default=1_000_000)
lnbits_notification_server_status_hours: int = Field(default=24, gt=0)
lnbits_notification_incoming_payment_amount_sats: int = Field(
default=1_000_000, ge=0
)
lnbits_notification_outgoing_payment_amount_sats: int = Field(
default=1_000_000, ge=0
)
class FakeWalletFundingSource(LNbitsSettings):
fake_wallet_secret: str = Field(default="ToTheMoon1")
lnbits_denomination: str = Field(default="sats")
class LNbitsFundingSource(LNbitsSettings):
@@ -535,7 +540,7 @@ class BoltzFundingSource(LNbitsSettings):
class LightningSettings(LNbitsSettings):
lightning_invoice_expiry: int = Field(default=3600)
lightning_invoice_expiry: int = Field(default=3600, gt=0)
class FundingSourcesSettings(
@@ -562,7 +567,7 @@ class FundingSourcesSettings(
lnbits_backend_wallet_class: str = Field(default="VoidWallet")
# 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
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):
@@ -601,7 +606,7 @@ class AuthMethods(Enum):
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_allowed_methods: list[str] = Field(
default=[
@@ -611,7 +616,7 @@ class AuthSettings(LNbitsSettings):
)
# How many seconds after login the user is allowed to update its credentials.
# 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):
return method.value in self.auth_allowed_methods
@@ -643,7 +648,7 @@ class AuditSettings(LNbitsSettings):
lnbits_audit_enabled: bool = Field(default=True)
# 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_path_params: bool = Field(default=True)
@@ -780,7 +785,7 @@ class EnvSettings(LNbitsSettings):
debug_database: bool = Field(default=False)
bundle_assets: bool = Field(default=True)
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="*")
lnbits_title: str = Field(default="LNbits API")
lnbits_path: str = Field(default=".")
@@ -792,24 +797,27 @@ class EnvSettings(LNbitsSettings):
enable_log_to_file: bool = Field(default=True)
log_rotation: str = Field(default="100 MB")
log_retention: str = Field(default="3 months")
server_startup_time: int = Field(default=time())
cleanup_wallets_days: int = Field(default=90)
funding_source_max_retries: int = Field(default=4)
cleanup_wallets_days: int = Field(default=90, ge=0)
funding_source_max_retries: int = Field(default=4, ge=0)
@property
def has_default_extension_path(self) -> bool:
return self.lnbits_extensions_path == "lnbits"
@property
def lnbits_server_up_time(self) -> str:
up_time = int(time() - self.server_startup_time)
return strftime("%H:%M:%S", gmtime(up_time))
class SaaSSettings(LNbitsSettings):
lnbits_saas_callback: str | None = Field(default=None)
lnbits_saas_secret: str | None = Field(default=None)
lnbits_saas_instance_id: str | None = Field(default=None)
def check_auth_secret_key(self):
if self.auth_secret_key:
return
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")
if auth_key_file.is_file():
with open(auth_key_file) as file:
self.auth_secret_key = file.readline()
return
self.auth_secret_key = uuid4().hex
with open(auth_key_file, "w+") as file:
file.write(self.auth_secret_key)
class PersistenceSettings(LNbitsSettings):
@@ -861,6 +869,13 @@ class TransientSettings(InstalledExtensionsSettings, ExchangeHistorySettings):
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
def readonly_fields(cls):
return [f for f in inspect.signature(cls).parameters if not f.startswith("_")]
@@ -869,7 +884,6 @@ class TransientSettings(InstalledExtensionsSettings, ExchangeHistorySettings):
class ReadOnlySettings(
EnvSettings,
ExtensionsInstallSettings,
SaaSSettings,
PersistenceSettings,
SuperUserSettings,
):
@@ -948,31 +962,6 @@ def set_cli_settings(**kwargs):
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()
transient_variables = TransientSettings.readonly_fields()
@@ -981,9 +970,8 @@ settings = Settings()
settings.lnbits_path = str(path.dirname(path.realpath(__file__)))
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:
settings.user_agent = f"LNbits/{settings.version}"
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+7 -5
View File
File diff suppressed because one or more lines are too long
+6 -6
View File
@@ -110,16 +110,16 @@ body[data-theme=bitcoin].body--light {
}
[data-theme=bitcoin] .bg-primary {
background: #ff9853 !important;
background: #ea611d !important;
}
[data-theme=bitcoin] .text-primary {
color: #ff9853 !important;
color: #ea611d !important;
}
[data-theme=bitcoin] .bg-secondary {
background: #ff7353 !important;
background: #e56f35 !important;
}
[data-theme=bitcoin] .text-secondary {
color: #ff7353 !important;
color: #e56f35 !important;
}
[data-theme=bitcoin] .bg-dark {
background: #2d293b !important;
@@ -134,10 +134,10 @@ body[data-theme=bitcoin].body--light {
color: #333646 !important;
}
[data-theme=bitcoin] .bg-marginal-bg {
background: #2d293b !important;
background: #000000 !important;
}
[data-theme=bitcoin] .text-marginal-bg {
color: #2d293b !important;
color: #000000 !important;
}
[data-theme=bitcoin] .bg-marginal-text {
background: #fff !important;
+12 -5
View File
@@ -65,8 +65,9 @@ window.localisation.en = {
view_github: 'View on GitHub',
voidwallet_active: 'VoidWallet is active! Payments disabled',
use_with_caution: 'USE WITH CAUTION - {name} wallet is still in BETA',
service_fee: 'Service fee: {amount} % per transaction',
service_fee_max: 'Service fee: {amount} % per transaction (max {max} sats)',
service_fee_badge: 'Service fee: {amount} % per transaction',
service_fee_max_badge:
'Service fee: {amount} % per transaction (max {max} {denom})',
service_fee_tooltip:
'Service fee charged by the LNbits server admin per outgoing transaction',
toggle_darkmode: 'Toggle Dark Mode',
@@ -175,6 +176,7 @@ window.localisation.en = {
payment_proof: 'Payment Proof',
update: 'Update',
update_available: 'Update {version} available!',
funding_sources: 'Funding Sources',
latest_update: 'You are on the latest version {version}.',
notifications: 'Notifications',
notifications_configure: 'Configure Notifications',
@@ -198,9 +200,13 @@ window.localisation.en = {
notifications_email_config: 'Email Configuration',
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_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_desc:
'Password for the email you will send from',
@@ -310,6 +316,7 @@ window.localisation.en = {
password: 'Password',
password_config: 'Password Config',
password_repeat: 'Password repeat',
update_password: 'Update Password',
change_password: 'Change Password',
update_credentials: 'Update Credentials',
update_pubkey: 'Update Public Key',
@@ -500,8 +507,8 @@ window.localisation.en = {
denomination: 'Denomination',
denomination_hint: 'The name for the FakeWallet token',
denomination_error: 'Denomination must be 3 characters, or `sats`',
ui_qr_code_logo: 'QR Code Logo',
ui_qr_code_logo_hint: 'URL to logo image in QR code',
ui_qr_code_logo: 'QR Code/Favicon Logo',
ui_qr_code_logo_hint: 'QR code and favicon logo url',
ui_custom_image: 'Custom Image',
ui_custom_image_label: 'URL to custom image',
ui_custom_image_hint: 'Image showed at homepage/login',
+24 -2
View File
@@ -438,7 +438,7 @@ window.AdminPageLogic = {
LNbits.api
.request('GET', '/admin/api/v1/restart/')
.then(response => {
Quasar.Notify.create({
this.$q.notify({
type: 'positive',
message: 'Success! Restarted Server',
icon: null
@@ -450,7 +450,29 @@ window.AdminPageLogic = {
formatDate(date) {
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() {
LNbits.api
.request('GET', '/admin/api/v1/audit', this.g.user.wallets[0].adminkey)
+1 -1
View File
@@ -209,7 +209,7 @@ window.LNbits = {
})
obj.walletOptions = obj.wallets.map(obj => {
return {
label: [obj.name, ' - ', obj.id].join(''),
label: [obj.name, ' - ', obj.id.substring(0, 5), '...'].join(''),
value: obj.id
}
})
@@ -37,7 +37,8 @@ window.app.component('lnbits-funding-sources', {
'FakeWallet',
'Fake Wallet',
{
fake_wallet_secret: 'Secret'
fake_wallet_secret: 'Secret',
lnbits_denomination: '"sats" or 3 Letter Custom Denomination'
}
],
[
+3 -3
View File
@@ -8,11 +8,11 @@ $themes: (
marginal-text: #fff
),
'bitcoin': (
primary: #ff9853,
secondary: #ff7353,
primary: #ea611d,
secondary: #e56f35,
dark: #2d293b,
info: #333646,
marginal-bg: #2d293b,
marginal-bg: #000000,
marginal-text: #fff
),
'freedom': (
+45 -85
View File
@@ -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) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
@@ -1266,23 +1266,6 @@
var toFiniteNumber = function toFiniteNumber(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.
@@ -1399,8 +1382,6 @@
findKey: findKey,
global: _global,
isContextDefined: isContextDefined,
ALPHABET: ALPHABET,
generateString: generateString,
isSpecCompliantForm: isSpecCompliantForm,
toJSONObject: toJSONObject,
isAsyncFn: isAsyncFn,
@@ -1735,7 +1716,7 @@
*
* @param {string} url The base of the url (e.g., http://www.google.com)
* @param {object} [params] The params to be appended
* @param {?object} options
* @param {?(object|Function)} options
*
* @returns {string} The formatted url
*/
@@ -1745,6 +1726,11 @@
return url;
}
var _encode = options && options.encode || encode;
if (utils$1.isFunction(options)) {
options = {
serialize: options
};
}
var serializeFn = options && options.serialize;
var serializedParams;
if (serializeFn) {
@@ -2642,60 +2628,14 @@
};
};
var isURLSameOrigin = platform.hasStandardBrowserEnv ?
// Standard browser envs have full support of the APIs needed to test
// whether the request URL is of the same origin as current location.
function standardBrowserEnv() {
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;
var isURLSameOrigin = platform.hasStandardBrowserEnv ? function (origin, isMSIE) {
return function (url) {
url = new URL(url, platform.origin);
return origin.protocol === url.protocol && origin.host === url.host && (isMSIE || origin.port === url.port);
};
}() :
// Non standard browser envs (web workers, react-native) lack needed support.
function nonStandardBrowserEnv() {
return function isURLSameOrigin() {
return true;
};
}();
}(new URL(platform.origin), platform.navigator && /(msie|trident)/i.test(platform.navigator.userAgent)) : function () {
return true;
};
var cookies = platform.hasStandardBrowserEnv ?
// Standard browser envs support document.cookie
@@ -2761,8 +2701,9 @@
*
* @returns {string} The combined full path
*/
function buildFullPath(baseURL, requestedURL) {
if (baseURL && !isAbsoluteURL(requestedURL)) {
function buildFullPath(baseURL, requestedURL, allowAbsoluteUrls) {
var isRelativeUrl = !isAbsoluteURL(requestedURL);
if (baseURL && isRelativeUrl || allowAbsoluteUrls == false) {
return combineURLs(baseURL, requestedURL);
}
return requestedURL;
@@ -2785,7 +2726,7 @@
// eslint-disable-next-line no-param-reassign
config2 = config2 || {};
var config = {};
function getMergedValue(target, source, caseless) {
function getMergedValue(target, source, prop, caseless) {
if (utils$1.isPlainObject(target) && utils$1.isPlainObject(source)) {
return utils$1.merge.call({
caseless: caseless
@@ -2799,11 +2740,11 @@
}
// eslint-disable-next-line consistent-return
function mergeDeepProperties(a, b, caseless) {
function mergeDeepProperties(a, b, prop, caseless) {
if (!utils$1.isUndefined(b)) {
return getMergedValue(a, b, caseless);
return getMergedValue(a, b, prop, caseless);
} else if (!utils$1.isUndefined(a)) {
return getMergedValue(undefined, a, caseless);
return getMergedValue(undefined, a, prop, caseless);
}
}
@@ -2860,8 +2801,8 @@
socketPath: defaultToConfig2,
responseEncoding: defaultToConfig2,
validateStatus: mergeDirectKeys,
headers: function headers(a, b) {
return mergeDeepProperties(headersToObject(a), headersToObject(b), true);
headers: function headers(a, b, prop) {
return mergeDeepProperties(headersToObject(a), headersToObject(b), prop, true);
}
};
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 = {};
@@ -3756,6 +3697,13 @@
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
@@ -3838,7 +3786,8 @@
_context.prev = 6;
_context.t0 = _context["catch"](0);
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
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
config.method = (config.method || this.defaults.method || 'get').toLowerCase();
@@ -3968,7 +3928,7 @@
key: "getUri",
value: function getUri(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);
}
}]);
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -89,11 +89,11 @@
>
{% if LNBITS_SERVICE_FEE_MAX > 0 %}
<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>
{%else%}
<span
v-text='$t("service_fee", { amount: "{{ LNBITS_SERVICE_FEE }}" })'
v-text='$t("service_fee_badge", { amount: "{{ LNBITS_SERVICE_FEE}}"})'
></span>
{%endif%}
<q-tooltip
+10 -8
View File
@@ -1057,7 +1057,16 @@
<template id="lnbits-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="col-12">
<p>Active Funding<small> (Requires server restart)</small></p>
@@ -1095,13 +1104,6 @@
:label="prop.label"
: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>
</div>
</div>
+4 -2
View File
@@ -87,13 +87,15 @@ class LNbitsWallet(Wallet):
r.raise_for_status()
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
return InvoiceResponse(
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:
return InvoiceResponse(
False, None, None, "Server error: 'invalid json response'"
+27 -22
View File
@@ -6,7 +6,7 @@
"": {
"name": "lnbits",
"dependencies": {
"axios": "^1.7.7",
"axios": "^1.8.2",
"chart.js": "^4.4.4",
"moment": "^2.30.1",
"nostr-tools": "^2.7.2",
@@ -15,7 +15,7 @@
"showdown": "^2.1.0",
"underscore": "^1.13.7",
"vue": "3.5.8",
"vue-i18n": "^10.0.5",
"vue-i18n": "^10.0.6",
"vue-qrcode-reader": "^5.5.10",
"vue-router": "4.4.5",
"vuex": "4.1.0"
@@ -72,12 +72,13 @@
}
},
"node_modules/@intlify/core-base": {
"version": "10.0.5",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-10.0.5.tgz",
"integrity": "sha512-F3snDTQs0MdvnnyzTDTVkOYVAZOE/MHwRvF7mn7Jw1yuih4NrFYLNYIymGlLmq4HU2iIdzYsZ7f47bOcwY73XQ==",
"version": "10.0.6",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-10.0.6.tgz",
"integrity": "sha512-/NINGvy7t8qSCyyuqMIPmHS6CBQjqPIPVOps0Rb7xWrwwkwHJKtahiFnW1HC4iQVhzoYwEW6Js0923zTScLDiA==",
"license": "MIT",
"dependencies": {
"@intlify/message-compiler": "10.0.5",
"@intlify/shared": "10.0.5"
"@intlify/message-compiler": "10.0.6",
"@intlify/shared": "10.0.6"
},
"engines": {
"node": ">= 16"
@@ -87,11 +88,12 @@
}
},
"node_modules/@intlify/message-compiler": {
"version": "10.0.5",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-10.0.5.tgz",
"integrity": "sha512-6GT1BJ852gZ0gItNZN2krX5QAmea+cmdjMvsWohArAZ3GmHdnNANEcF9JjPXAMRtQ6Ux5E269ymamg/+WU6tQA==",
"version": "10.0.6",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-10.0.6.tgz",
"integrity": "sha512-QcUYprK+e4X2lU6eJDxLuf/mUtCuVPj2RFBoFRlJJxK3wskBejzlRvh1Q0lQCi9tDOnD4iUK1ftcGylE3X3idA==",
"license": "MIT",
"dependencies": {
"@intlify/shared": "10.0.5",
"@intlify/shared": "10.0.6",
"source-map-js": "^1.0.2"
},
"engines": {
@@ -102,9 +104,10 @@
}
},
"node_modules/@intlify/shared": {
"version": "10.0.5",
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-10.0.5.tgz",
"integrity": "sha512-bmsP4L2HqBF6i6uaMqJMcFBONVjKt+siGluRq4Ca4C0q7W2eMaVZr8iCgF9dKbcVXutftkC7D6z2SaSMmLiDyA==",
"version": "10.0.6",
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-10.0.6.tgz",
"integrity": "sha512-2xqwm05YPpo7TM//+v0bzS0FWiTzsjpSMnWdt7ZXs5/ZfQIedSuBXIrskd8HZ7c/cZzo1G9ALHTksnv/74vk/Q==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
@@ -399,9 +402,10 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.7.7",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz",
"integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
@@ -1301,12 +1305,13 @@
}
},
"node_modules/vue-i18n": {
"version": "10.0.5",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-10.0.5.tgz",
"integrity": "sha512-9/gmDlCblz3i8ypu/afiIc/SUIfTTE1mr0mZhb9pk70xo2csHAM9mp2gdQ3KD2O0AM3Hz/5ypb+FycTj/lHlPQ==",
"version": "10.0.6",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-10.0.6.tgz",
"integrity": "sha512-pQPspK5H4srzlu+47+HEY2tmiY3GyYIvSPgSBdQaYVWv7t1zj1t9p1FvHlxBXyJ17t9stG/Vxj+pykrvPWBLeQ==",
"license": "MIT",
"dependencies": {
"@intlify/core-base": "10.0.5",
"@intlify/shared": "10.0.5",
"@intlify/core-base": "10.0.6",
"@intlify/shared": "10.0.6",
"@vue/devtools-api": "^6.5.0"
},
"engines": {
+2 -2
View File
@@ -20,7 +20,7 @@
"sass": "^1.78.0"
},
"dependencies": {
"axios": "^1.7.7",
"axios": "^1.8.2",
"chart.js": "^4.4.4",
"moment": "^2.30.1",
"qrcode.vue": "^3.4.1",
@@ -29,7 +29,7 @@
"nostr-tools": "^2.7.2",
"underscore": "^1.13.7",
"vue": "3.5.8",
"vue-i18n": "^10.0.5",
"vue-i18n": "^10.0.6",
"vue-qrcode-reader": "^5.5.10",
"vue-router": "4.4.5",
"vuex": "4.1.0"
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "lnbits"
version = "1.0.0-rc8"
version = "1.0.0-rc9"
description = "LNbits, free and open-source Lightning wallet and accounts system."
authors = ["Alan Bits <alan@lnbits.com>"]
readme = "README.md"