Compare commits

..
Author SHA1 Message Date
dni ⚡andGitHub 07428ecf94 chore: update to version v1.5.4-rc1 (#3935) 2026-04-16 15:43:33 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
867e3d06f6 chore(deps): bump python-multipart from 0.0.22 to 0.0.26 (#3933)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-16 14:05:45 +02:00
4b4b6d0bcd feat: adds an upload for assets on backgroundImage in account and site customisation (#3929)
Co-authored-by: dni  <office@dnilabs.com>
Co-authored-by: alan <alan@lnbits.com>
2026-04-16 14:05:24 +02:00
07b1521dad feat: Add phoenixd mnemonic display (#3931)
Co-authored-by: alan <alan@lnbits.com>
2026-04-16 13:42:58 +02:00
7a2ddd9826 chore: update axios upgrade to 1.15.0 (#3936)
Co-authored-by: alan <alan@lnbits.com>
2026-04-16 13:25:37 +02:00
ArcandGitHub 0eb4b477b7 feat: ui, adds full/thumb buttons to assets (#3928) 2026-04-16 13:17:11 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
06a0ba58ce chore(deps): bump pytest from 9.0.2 to 9.0.3 (#3930)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-16 13:15:33 +02:00
dni ⚡andGitHub 5399b36027 chore: revert changing default auth (#3934) 2026-04-16 11:30:40 +02:00
ArcandGitHub 385fb4f9bc fix: first_install for local (#3927) 2026-04-14 14:50:20 +01:00
ArcandGitHub 8db76b8864 fix: Appimage (#3926) 2026-04-12 23:01:25 +01:00
ArcandGitHub 9e3ab0ef26 feat: max users + extensions env (#3919) 2026-04-12 22:38:36 +01:00
ArcandGitHub 04c9b67997 fix: funding source ui (#3920) 2026-04-12 22:34:27 +01:00
Vlad StanandGitHub 116f982aab fix: webhook can fail for multiple reasons (#3921) 2026-04-08 18:17:39 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
63cc89e2b6 chore(deps): bump pygments from 2.19.2 to 2.20.0 (#3912)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 09:49:38 +03:00
Vlad StanandGitHub 183e6e5661 [test] Codex tests (#3911) 2026-03-31 09:48:43 +03:00
Vlad StanandGitHub 6b3fd80e46 [tests] Wallets test editor (#3910) 2026-03-30 13:12:26 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5999a773be chore(deps): bump cryptography from 46.0.5 to 46.0.6 (#3909)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-30 10:16:01 +03:00
dni ⚡andGitHub 7e0fadad3b refactor: move payment code from tasks.py to service/payments.py (#3800) 2026-03-26 11:28:23 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b2d6243697 chore(deps): bump requests from 2.32.5 to 2.33.0 (#3903)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-26 11:07:50 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6b5a77fb3b chore(deps): bump picomatch from 2.3.1 to 2.3.2 (#3901)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-26 09:58:34 +02:00
dni ⚡andGitHub cc8fb68b02 ci: only run ci on pull request (#3902) 2026-03-26 09:37:36 +02:00
Vlad StanandGitHub 658da6b28e feat: optimize QR code value for bach32 (#3900) 2026-03-25 16:22:03 +02:00
Vlad StanandGitHub 7d734ecb74 fix: use --offline for uv (#3896) 2026-03-25 13:10:47 +02:00
dni ⚡andGitHub 9177dd195b chore: update to version 1.5.3 (#3899) 2026-03-25 12:08:39 +01:00
15faab4f38 [feat] reset the first install token (#3894) (#3898)
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2026-03-25 11:57:37 +01:00
46 changed files with 7527 additions and 406 deletions
+4
View File
@@ -24,6 +24,10 @@ LOG_ROTATION="100 MB"
LOG_RETENTION="3 months"
# for database cleanup commands
# CLEANUP_WALLETS_DAYS=90
# Hard limit for total created users. Set to 0 to disable the limit.
# LNBITS_MAX_USERS=0
# Hard limit for total installed extensions. Set to 0 to disable the limit.
# LNBITS_MAX_EXTENSIONS=0
# === Admin Settings ===
+3
View File
@@ -69,7 +69,10 @@ jobs:
--onefile \
--name lnbits \
--hidden-import=embit \
--hidden-import=bitstring.bitstore_bitarray \
--collect-all embit \
--collect-all bitstring \
--collect-all bitarray \
--collect-all lnbits \
--collect-all sqlalchemy \
--collect-all breez_sdk \
+1 -5
View File
@@ -1,9 +1,5 @@
name: LNbits CI
on:
push:
branches:
- main
- dev
pull_request:
@@ -69,7 +65,7 @@ jobs:
make: openapi
regtest:
needs: [ lint, test-api, test-wallets, test-unit, migration, openapi ]
needs: [ lint ]
uses: ./.github/workflows/regtest.yml
strategy:
matrix:
+1
View File
@@ -6,6 +6,7 @@ __pycache__
*$py.class
.mypy_cache
.vscode
.codex
*-lock.json
.python-version
+1 -1
View File
@@ -43,4 +43,4 @@ ENV LNBITS_HOST="0.0.0.0"
EXPOSE 5000
CMD ["sh", "-c", "uv run lnbits --port $LNBITS_PORT --host $LNBITS_HOST --forwarded-allow-ips='*'"]
CMD ["sh", "-c", "uv --offline run lnbits --port $LNBITS_PORT --host $LNBITS_HOST --forwarded-allow-ips='*'"]
+4
View File
@@ -12,6 +12,7 @@ from .extensions import (
drop_extension_db,
get_installed_extension,
get_installed_extensions,
get_installed_extensions_count,
get_user_active_extensions_ids,
get_user_extension,
get_user_extensions,
@@ -58,6 +59,7 @@ from .users import (
get_account_by_username,
get_account_by_username_or_email,
get_accounts,
get_accounts_count,
get_user,
get_user_access_control_lists,
get_user_from_account,
@@ -117,11 +119,13 @@ __all__ = [
"get_account_by_username",
"get_account_by_username_or_email",
"get_accounts",
"get_accounts_count",
"get_admin_settings",
"get_db_version",
"get_db_versions",
"get_installed_extension",
"get_installed_extensions",
"get_installed_extensions_count",
"get_latest_payments_by_extension",
"get_payment",
"get_payments",
+7
View File
@@ -90,6 +90,13 @@ async def get_installed_extensions(
return all_extensions
async def get_installed_extensions_count(conn: Connection | None = None) -> int:
row: dict | None = await (conn or db).fetchone(
"SELECT COUNT(*) as count FROM installed_extensions"
)
return int(row["count"]) if row else 0
async def get_user_extension(
user_id: str, extension: str, conn: Connection | None = None
) -> UserExtension | None:
+2 -1
View File
@@ -105,7 +105,8 @@ async def get_settings_field(
)
if not row:
return None
return SettingsField(id=row["id"], value=json.loads(row["value"]), tag=row["tag"])
value = json.loads(row["value"]) if row["value"] else None
return SettingsField(id=row["id"], value=value, tag=row["tag"])
async def set_settings_field(id_: str, value: Any | None, tag: str | None = "core"):
+7
View File
@@ -37,6 +37,13 @@ async def create_account(
return account
async def get_accounts_count(conn: Connection | None = None) -> int:
row: dict | None = await (conn or db).fetchone(
"SELECT COUNT(*) as count FROM accounts"
)
return int(row["count"]) if row else 0
async def update_account(account: Account, conn: Connection | None = None) -> Account:
account.updated_at = datetime.now(timezone.utc)
await (conn or db).update("accounts", account)
+12
View File
@@ -9,6 +9,7 @@ from lnbits.core.crud import (
delete_installed_extension,
get_db_version,
get_installed_extension,
get_installed_extensions_count,
update_installed_extension_state,
)
from lnbits.core.crud.extensions import (
@@ -38,6 +39,8 @@ async def install_extension(
if installed_ext and installed_ext.meta:
ext_info.meta.payments = installed_ext.meta.payments
await check_extensions_limit(installed_ext)
if not skip_download:
await ext_info.download_archive()
@@ -63,6 +66,15 @@ async def install_extension(
return extension
async def check_extensions_limit(installed_ext: InstallableExtension | None = None):
if settings.lnbits_max_extensions == 0 or installed_ext:
return
extensions_count = await get_installed_extensions_count()
if extensions_count >= settings.lnbits_max_extensions:
raise ValueError("Max amount of extensions have been installed")
async def uninstall_extension(ext_id: str):
await stop_extension_background_work(ext_id)
+32 -1
View File
@@ -13,13 +13,13 @@ from lnbits.core.crud.payments import get_daily_stats
from lnbits.core.db import db
from lnbits.core.models import PaymentDailyStats, PaymentFilters
from lnbits.core.models.payments import CreateInvoice
from lnbits.core.services.fiat_providers import handle_fiat_payment_confirmation
from lnbits.db import Connection, Filters
from lnbits.decorators import check_user_extension_access
from lnbits.exceptions import InvoiceError, PaymentError, UnsupportedError
from lnbits.fiat import get_fiat_provider
from lnbits.helpers import check_callback_url
from lnbits.settings import settings
from lnbits.tasks import create_task, internal_invoice_queue_put
from lnbits.utils.crypto import fake_privkey, random_secret_and_hash, verify_preimage
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis, satoshis_amount_as_fiat
from lnbits.wallets import fake_wallet, get_funding_source
@@ -509,6 +509,8 @@ async def update_wallet_balance(
)
payment.status = PaymentState.SUCCESS
await update_payment(payment, conn=conn)
from lnbits.tasks import internal_invoice_queue_put
await internal_invoice_queue_put(payment.checking_id)
@@ -819,6 +821,8 @@ async def _pay_external_invoice(
fee_reserve_msat = fee_reserve(amount_msat, internal=False)
from lnbits.tasks import create_task
task = create_task(
_fundingsource_pay_invoice(checking_id, payment.bolt11, fee_reserve_msat)
)
@@ -1068,3 +1072,30 @@ async def _send_payment_notification_in_background(
if not wallet:
raise PaymentError(f"Could not fetch wallet '{wallet_id}'.", status="failed")
send_payment_notification_in_background(wallet, payment)
async def update_invoice_callback(checking_id: str) -> Payment | None:
"""
Takes a checking_id of an incoming payment, from either paid_invoices_stream()
or internal_invoice_queue. Checks its status, updates and returns it.
returns None if no payment was found or it not and incoming payment.
"""
payment = await get_standalone_payment(checking_id, incoming=True)
if not payment:
logger.warning(f"No payment found for '{checking_id}'.")
return None
if not payment.is_in:
logger.warning(f"Payment '{checking_id}' is not incoming, skipping.")
return None
status = await check_payment_status(
payment, skip_internal_payment_notifications=True
)
payment.fee = status.fee_msat or payment.fee
# only overwrite preimage if status.preimage provides it
payment.preimage = status.preimage or payment.preimage
payment.status = PaymentState.SUCCESS
await update_payment(payment)
if payment.fiat_provider:
await handle_fiat_payment_confirmation(payment)
return payment
+18
View File
@@ -23,6 +23,7 @@ from ..crud import (
get_account_by_email,
get_account_by_pubkey,
get_account_by_username,
get_accounts_count,
get_super_settings,
get_user_extensions,
get_user_from_account,
@@ -55,6 +56,8 @@ async def create_user_account_no_ckeck(
conn: Connection | None = None,
) -> User:
async with db.reuse_conn(conn) if conn else db.connect() as conn:
await check_users_limit(conn)
if account:
account.validate_fields()
if account.username and await get_account_by_username(
@@ -95,6 +98,15 @@ async def create_user_account_no_ckeck(
return user
async def check_users_limit(conn: Connection | None = None):
if settings.lnbits_max_users == 0:
return
users_count = await get_accounts_count(conn=conn)
if users_count >= settings.lnbits_max_users:
raise ValueError("Max amount of users have been created")
async def update_user_account(account: Account) -> Account:
account.validate_fields()
@@ -173,6 +185,12 @@ async def check_admin_settings():
if account and account.extra and account.extra.provider == "env":
settings.first_install = True
if settings.has_first_install_token_changed():
logger.warning("First install token is changed. Resetting admin settings.")
new_settings = await init_admin_settings()
settings.super_user = new_settings.super_user
settings.first_install = True
logger.success(
"✔️ Admin UI is enabled. run `uv run lnbits-cli superuser` "
"to get the superuser."
+8
View File
@@ -12,6 +12,7 @@ from fastapi.responses import JSONResponse, RedirectResponse
from fastapi_sso.sso.base import OpenID, SSOBase
from loguru import logger
from lnbits.core.crud.settings import set_settings_field
from lnbits.core.crud.users import (
get_user_access_control_lists,
update_user_access_control_list,
@@ -548,6 +549,13 @@ async def first_install(data: UpdateSuperuserPassword) -> JSONResponse:
account.hash_password(data.password)
await update_account(account)
settings.first_install = False
# only confrm it after the super user has been successfully updated
if settings.first_install_token:
settings.first_install_token_confirmed = data.first_install_token
await set_settings_field(
"first_install_token_confirmed", data.first_install_token
)
return _auth_success_response(account.username, account.id, account.email)
+6 -2
View File
@@ -98,12 +98,16 @@ async def api_install_extension(data: CreateExtension):
ext_info.clean_extension_files()
detail = (
str(exc)
if isinstance(exc, AssertionError)
if isinstance(exc, (AssertionError, ValueError))
else f"Failed to install extension '{ext_info.id}'."
f"({ext_info.installed_version})."
)
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
status_code=(
HTTPStatus.BAD_REQUEST
if isinstance(exc, (AssertionError, ValueError))
else HTTPStatus.INTERNAL_SERVER_ERROR
),
detail=detail,
) from exc
+14 -2
View File
@@ -324,7 +324,8 @@ class AssetSettings(LNbitsSettings):
"heif",
"heics",
"text/plain",
"text/json" "text/xml",
"text/json",
"text/xml",
"application/json",
"application/pdf",
]
@@ -447,6 +448,7 @@ class SecuritySettings(LNbitsSettings):
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)
first_install_token_confirmed: str | None = Field(default=None)
def is_wallet_max_balance_exceeded(self, amount):
return (
@@ -587,6 +589,8 @@ class ZBDFundingSource(LNbitsSettings):
class PhoenixdFundingSource(LNbitsSettings):
phoenixd_api_endpoint: str | None = Field(default="http://localhost:9740/")
phoenixd_api_password: str | None = Field(default=None)
phoenixd_data_dir: str | None = Field(default=None)
phoenixd_mnemonic: str | None = Field(default=None)
class AlbyFundingSource(LNbitsSettings):
@@ -1006,7 +1010,6 @@ class EnvSettings(LNbitsSettings):
debug_database: bool = Field(default=False)
bundle_assets: bool = Field(default=True)
# When enabled, auth cookies require HTTPS and SSO will reject insecure HTTP.
# Set to false for local/dev environments that run without TLS.
auth_https_only: bool = Field(default=True)
host: str = Field(default="127.0.0.1")
port: int = Field(default=5000, gt=0)
@@ -1025,11 +1028,20 @@ class EnvSettings(LNbitsSettings):
cleanup_wallets_days: int = Field(default=90, ge=0)
funding_source_max_retries: int = Field(default=4, ge=0)
lnbits_max_users: int = Field(default=0, ge=0)
lnbits_max_extensions: int = Field(default=0, ge=0)
@property
def has_default_extension_path(self) -> bool:
return self.lnbits_extensions_path == "lnbits"
def has_first_install_token_changed(self) -> bool:
if not self.first_install_token:
return False
if not settings.first_install_token_confirmed:
return False
return self.first_install_token != settings.first_install_token_confirmed
def check_auth_secret_key(self):
if self.auth_secret_key:
return
File diff suppressed because one or more lines are too long
+11 -11
View File
File diff suppressed because one or more lines are too long
+2
View File
@@ -275,6 +275,8 @@ window.localisation.en = {
requires_server_restart:
'Changing these settings requires a server restart to take effect.',
funding_source_info: 'Select the active funding wallet',
phoenixd_warning:
"Phoenixd mnemonic is only available if phoenixd data-dir is specified and is readable by LNbits. It's not indicative of phoenixd not running. It just means LNbits cannot access the mnemonic to display it here.",
latest_update: 'You are on the latest version {version}.',
notifications: 'Notifications',
notifications_configure: 'Configure Notifications',
@@ -202,7 +202,18 @@ window.app.component('lnbits-admin-funding-sources', {
'Phoenixd',
{
phoenixd_api_endpoint: 'Endpoint',
phoenixd_api_password: 'Key'
phoenixd_api_password: 'Key',
phoenixd_data_dir: {
label: 'Data Directory',
hint: 'Directory where phoenixd stores its data, including the seed phrase.'
},
phoenixd_mnemonic: {
label: 'Phoenixd Seed Phrase',
hint: 'Only available if phoenixd data-dir is specified',
readonly: true,
copy: true,
qrcode: true
}
}
],
[
@@ -41,5 +41,37 @@ window.app.component('lnbits-admin-site-customisation', {
]
}
},
methods: {}
methods: {
onBackgroundImageInput(e) {
const file = e.target.files[0]
if (file) {
this.uploadBackgroundImage(file)
}
e.target.value = null
},
async uploadBackgroundImage(file) {
const formData = new FormData()
formData.append('file', file)
try {
const {data} = await LNbits.api.request(
'POST',
'/api/v1/assets?public_asset=true',
null,
formData,
{
headers: {'Content-Type': 'multipart/form-data'}
}
)
const assetUrl = `${window.location.origin}/api/v1/assets/${data.id}/thumbnail`
this.formData.lnbits_default_bgimage = assetUrl
Quasar.Notify.create({
type: 'positive',
message: 'Background image uploaded.',
icon: null
})
} catch (e) {
LNbits.utils.notifyApiError(e)
}
}
}
})
@@ -165,5 +165,26 @@ window.app.component('lnbits-qrcode', {
this.$refs.qrCode.$el.style.maxWidth = this.maxWidth + 'px'
this.$refs.qrCode.$el.setAttribute('width', '100%')
this.$refs.qrCode.$el.removeAttribute('height')
},
computed: {
optimizedValue() {
const separatorIndex = this.value.indexOf(':')
const type =
separatorIndex === -1 ? '' : this.value.substring(0, separatorIndex)
const value =
separatorIndex === -1
? this.value
: this.value.substring(separatorIndex + 1)
if (this.utils.isValidBech32(value)) {
const normalizedValue = value.toUpperCase()
if (type) {
return `${type.toUpperCase()}:${normalizedValue}`
}
return normalizedValue
}
return this.value
}
}
})
+33 -8
View File
@@ -545,31 +545,56 @@ window.PageAccount = {
if (file) {
this.uploadAsset(file)
}
e.target.value = null
},
async uploadAsset(file) {
onBackgroundImageInput(e) {
const file = e.target.files[0]
if (file) {
this.uploadBackgroundImage(file)
}
e.target.value = null
},
async uploadAsset(
file,
{isPublic = this.assetsUploadToPublic, notifySuccess = true} = {}
) {
const formData = new FormData()
formData.append('file', file)
try {
await LNbits.api.request(
const {data} = await LNbits.api.request(
'POST',
`/api/v1/assets?public_asset=${this.assetsUploadToPublic}`,
`/api/v1/assets?public_asset=${isPublic}`,
null,
formData,
{
headers: {'Content-Type': 'multipart/form-data'}
}
)
this.$q.notify({
type: 'positive',
message: 'Upload successful!',
icon: null
})
if (notifySuccess) {
this.$q.notify({
type: 'positive',
message: 'Upload successful!',
icon: null
})
}
await this.getUserAssets()
return data
} catch (e) {
console.warn(e)
LNbits.utils.notifyApiError(e)
}
},
async uploadBackgroundImage(file) {
const asset = await this.uploadAsset(file, {
isPublic: false,
notifySuccess: false
})
if (!asset) {
return
}
const assetUrl = `${window.location.origin}/api/v1/assets/${asset.id}/thumbnail`
await this.siteCustomisationChanged({bgimageChoice: assetUrl})
},
async deleteAsset(asset) {
LNbits.utils
.confirmDialog('Are you sure you want to delete this asset?')
+40
View File
@@ -160,6 +160,46 @@ window._lnbitsUtils = {
return null
}
},
isValidBech32(value) {
if (typeof value !== 'string') {
return false
}
const candidate = value.trim()
if (
!candidate ||
(candidate !== candidate.toLowerCase() &&
candidate !== candidate.toUpperCase())
) {
return false
}
const normalized = candidate.toLowerCase()
const splitPosition = normalized.lastIndexOf('1')
if (splitPosition <= 0) {
return false
}
const humanReadablePart = normalized.substring(0, splitPosition)
const data = normalized.substring(splitPosition + 1)
if (data.length < 6) {
return false
}
if (
typeof bech32ToFiveBitArray !== 'function' ||
typeof verify_checksum !== 'function'
) {
return false
}
const words = bech32ToFiveBitArray(data)
if (words.some(word => word < 0)) {
return false
}
return verify_checksum(humanReadablePart, words)
},
async notifyApiError(error) {
if (!error.response) {
return console.error(error)
+294 -153
View File
File diff suppressed because it is too large Load Diff
+11 -35
View File
@@ -5,12 +5,8 @@ from collections.abc import Callable, Coroutine
from loguru import logger
from lnbits.core.crud import (
get_standalone_payment,
update_payment,
)
from lnbits.core.models import Payment, PaymentState
from lnbits.core.services.fiat_providers import handle_fiat_payment_confirmation
from lnbits.core.models import Payment
from lnbits.core.services.payments import update_invoice_callback
from lnbits.settings import settings
from lnbits.wallets import get_funding_source
@@ -116,7 +112,10 @@ async def internal_invoice_listener() -> None:
while settings.lnbits_running:
checking_id = await internal_invoice_queue.get()
logger.info(f"got an internal payment notification {checking_id}")
await invoice_callback_dispatcher(checking_id, is_internal=True)
payment = await update_invoice_callback(checking_id)
if payment:
logger.success(f"internal invoice {checking_id} settled")
await invoice_callback_dispatcher(payment)
async def invoice_listener() -> None:
@@ -129,7 +128,10 @@ async def invoice_listener() -> None:
funding_source = get_funding_source()
async for checking_id in funding_source.paid_invoices_stream():
logger.info(f"got a payment notification {checking_id}")
await invoice_callback_dispatcher(checking_id)
payment = await update_invoice_callback(checking_id)
if payment:
logger.success(f"fundingsource invoice {checking_id} settled")
await invoice_callback_dispatcher(payment)
def wait_for_paid_invoices(
@@ -165,33 +167,7 @@ def run_interval(
return wrapper
async def invoice_callback_dispatcher(checking_id: str, is_internal: bool = False):
"""
Takes an incoming payment, checks its status, and dispatches it to
invoice_listeners from core and extensions.
"""
payment = await get_standalone_payment(checking_id, incoming=True)
if not payment:
logger.warning(f"No payment found for '{checking_id}'.")
return
if not payment.is_in:
logger.warning(f"Payment '{checking_id}' is not incoming, skipping.")
return
from lnbits.core.services.payments import check_payment_status
status = await check_payment_status(
payment, skip_internal_payment_notifications=True
)
payment.fee = status.fee_msat or payment.fee
# only overwrite preimage if status.preimage provides it
payment.preimage = status.preimage or payment.preimage
payment.status = PaymentState.SUCCESS
await update_payment(payment)
if payment.fiat_provider:
await handle_fiat_payment_confirmation(payment)
internal = "internal" if is_internal else ""
logger.success(f"{internal} invoice {checking_id} settled")
async def invoice_callback_dispatcher(payment: Payment):
for name, send_chan in invoice_listeners.items():
logger.trace(f"invoice listeners: sending to `{name}`")
await send_chan.put(payment)
+29 -29
View File
@@ -75,7 +75,35 @@
</p>
</div>
</div>
<div class="row q-col-gutter-md">
<div v-if="isSuperUser">
<lnbits-admin-funding-sources
:form-data="formData"
:allowed-funding-sources="settings.lnbits_allowed_funding_sources"
/>
<div class="row q-col-gutter-md q-my-md">
<div class="col-12 col-sm-8">
<q-item tag="div">
<q-item-section>
<q-item-label
v-text="$t('funding_source_retries')"
></q-item-label>
<q-item-label
caption
v-text="$t('funding_source_retries_desc')"
></q-item-label>
</q-item-section>
<q-item-section>
<q-input
filled
v-model="formData.funding_source_max_retries"
type="number"
/>
</q-item-section>
</q-item>
</div>
</div>
</div>
<div class="row q-col-gutter-md q-mt-lg">
<div class="col-12">
<h6 class="q-my-none">
<span v-text="$t('routing_fee_reserve_calculations')"></span>
@@ -182,34 +210,6 @@
></q-input>
</div>
</div>
<div v-if="isSuperUser">
<lnbits-admin-funding-sources
:form-data="formData"
:allowed-funding-sources="settings.lnbits_allowed_funding_sources"
/>
<div class="row q-col-gutter-md q-my-md">
<div class="col-12 col-sm-8">
<q-item tag="div">
<q-item-section>
<q-item-label
v-text="$t('funding_source_retries')"
></q-item-label>
<q-item-label
caption
v-text="$t('funding_source_retries_desc')"
></q-item-label>
</q-item-section>
<q-item-section>
<q-input
filled
v-model="formData.funding_source_max_retries"
type="number"
/>
</q-item-section>
</q-item>
</div>
</div>
</div>
<q-separator></q-separator>
<h6 class="q-mt-lg q-mb-sm">
<p v-text="$t('watchdog')"></p>
@@ -52,6 +52,7 @@
:label="prop.label"
:hint="prop.hint"
:value="prop.value"
:readonly="prop.readonly || false"
>
<q-btn
v-if="prop.copy"
@@ -73,6 +74,15 @@
></q-btn>
</q-input>
</div>
<p
v-if="fund === 'PhoenixdWallet' && key === 'phoenixd_mnemonic'"
class="col-12 q-my-md"
>
<span>
<q-icon name="warning" color="orange" size="xs"></q-icon>
<span v-text="$t('phoenixd_warning')"></span>
</span>
</p>
</div>
<q-expansion-item
v-if="
@@ -251,10 +251,27 @@
type="text"
v-model="formData.lnbits_default_bgimage"
label="Background Image"
@update:model-value="applyGlobalBgimage"
hint="This must be a trusted source. It can change the content and it can log your IP address."
>
<template v-slot:append>
<q-btn
dense
flat
round
icon="upload"
@click="$refs.adminBackgroundImageInput.click()"
>
<q-tooltip>Upload background image</q-tooltip>
</q-btn>
</template>
</q-input>
<input
type="file"
ref="adminBackgroundImageInput"
accept="image/*"
style="display: none"
@change="onBackgroundImageInput"
/>
</div>
</div>
<div class="row q-col-gutter-md q-mb-md">
@@ -12,7 +12,7 @@
>
<qrcode-vue
ref="qrCode"
:value="value"
:value="optimizedValue"
:margin="margin"
:size="size"
level="Q"
+136 -70
View File
@@ -492,10 +492,28 @@
siteCustomisationChanged({bgimageChoice: $event})
"
>
<template v-slot:append>
<q-btn
dense
flat
round
icon="upload"
@click="$refs.backgroundImageInput.click()"
>
<q-tooltip>Upload background image</q-tooltip>
</q-btn>
</template>
<q-tooltip
><span v-text="$t('background_image')"></span
></q-tooltip>
</q-input>
<input
type="file"
ref="backgroundImageInput"
accept="image/*"
style="display: none"
@change="onBackgroundImageInput"
/>
</div>
</div>
<div class="row q-mb-md">
@@ -1108,82 +1126,130 @@
<q-separator></q-separator>
<q-card-section>
<q-btn-dropdown
color="grey"
dense
outline
no-caps
:label="props.row.name"
:icon="props.row.is_public ? 'public' : ''"
<div
class="row items-center no-wrap q-col-gutter-sm"
>
<q-list>
<q-item
clickable
v-close-popup
@click="copyAssetLinkToClipboard(props.row)"
<div class="col">
<q-btn-dropdown
color="grey"
dense
outline
no-caps
class="full-width"
:label="props.row.name"
:icon="props.row.is_public ? 'public' : ''"
>
<q-item-section avatar>
<q-avatar icon="content_copy" />
</q-item-section>
<q-item-section>
<q-item-label>Copy Link</q-item-label>
<q-item-label caption
>Copy asset link to
clipboard</q-item-label
>
</q-item-section>
</q-item>
<q-item
clickable
v-close-popup
@click="toggleAssetPublicAccess(props.row)"
>
<q-item-section avatar>
<q-avatar
:icon="
props.row.is_public
? 'public_off'
: 'public'
<q-list>
<q-item
clickable
v-close-popup
@click="
copyAssetLinkToClipboard(props.row)
"
text-color="primary"
/>
</q-item-section>
<q-item-section v-if="props.row.is_public">
<q-item-label>Unpublish</q-item-label>
<q-item-label caption
>Make this asset private</q-item-label
>
</q-item-section>
<q-item-section v-else>
<q-item-label>Publish</q-item-label>
<q-item-label caption
>Make this asset public</q-item-label
>
</q-item-section>
</q-item>
<q-item-section avatar>
<q-avatar icon="content_copy" />
</q-item-section>
<q-item-section>
<q-item-label>Copy Link</q-item-label>
<q-item-label caption
>Copy asset link to
clipboard</q-item-label
>
</q-item-section>
</q-item>
<q-item
clickable
v-close-popup
@click="deleteAsset(props.row)"
>
<q-item-section avatar>
<q-avatar
icon="delete"
text-color="negative"
/>
</q-item-section>
<q-item-section>
<q-item-label>Delete</q-item-label>
<q-item-label caption
>Permanently delete this
asset</q-item-label
<q-item
clickable
v-close-popup
@click="
toggleAssetPublicAccess(props.row)
"
>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
<q-item-section avatar>
<q-avatar
:icon="
props.row.is_public
? 'public_off'
: 'public'
"
text-color="primary"
/>
</q-item-section>
<q-item-section
v-if="props.row.is_public"
>
<q-item-label>Unpublish</q-item-label>
<q-item-label caption
>Make this asset
private</q-item-label
>
</q-item-section>
<q-item-section v-else>
<q-item-label>Publish</q-item-label>
<q-item-label caption
>Make this asset
public</q-item-label
>
</q-item-section>
</q-item>
<q-item
clickable
v-close-popup
@click="deleteAsset(props.row)"
>
<q-item-section avatar>
<q-avatar
icon="delete"
text-color="negative"
/>
</q-item-section>
<q-item-section>
<q-item-label>Delete</q-item-label>
<q-item-label caption
>Permanently delete this
asset</q-item-label
>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</div>
<div class="col-auto">
<q-btn
type="a"
target="_blank"
rel="noopener noreferrer"
color="primary"
dense
flat
round
icon="image"
:href="`/api/v1/assets/${props.row.id}/data`"
>
<q-tooltip>Full image</q-tooltip>
</q-btn>
</div>
<div
class="col-auto"
v-if="props.row.thumbnail_base64"
>
<q-btn
type="a"
target="_blank"
rel="noopener noreferrer"
color="secondary"
dense
flat
round
icon="photo_size_select_small"
:href="`/api/v1/assets/${props.row.id}/thumbnail`"
>
<q-tooltip>Thumbnail</q-tooltip>
</q-btn>
</div>
</div>
</q-card-section>
</q-card>
</div>
+5 -1
View File
@@ -15,7 +15,11 @@ from lnbits.settings import settings
def log_server_info():
logger.info("LNbits Info")
if settings.first_install:
logger.success("This is a fresh install of LNbits.")
if settings.has_first_install_token_changed():
logger.success("This is a first install token reset.")
else:
logger.success("This is a fresh install of LNbits.")
if settings.first_install_token:
logger.success(
f"FIRST_INSTALL_TOKEN: `{settings.first_install_token}`. "
+52 -2
View File
@@ -4,9 +4,11 @@ import hashlib
import json
import urllib.parse
from collections.abc import AsyncGenerator
from pathlib import Path
from typing import Any
import httpx
from embit.bip39 import mnemonic_is_valid
from httpx import RequestError, TimeoutException
from loguru import logger
from websockets import connect
@@ -61,6 +63,8 @@ class PhoenixdWallet(Wallet):
}
self.client = httpx.AsyncClient(base_url=self.endpoint, headers=self.headers)
self._seed_mnemonic_to_persist: str | None = None
self._load_mnemonic_from_seed_file()
async def cleanup(self):
try:
@@ -69,6 +73,7 @@ class PhoenixdWallet(Wallet):
logger.warning(f"Error closing wallet connection: {e}")
async def status(self) -> StatusResponse:
await self._persist_loaded_mnemonic()
try:
r = await self.client.get("/getinfo", timeout=10)
r.raise_for_status()
@@ -101,7 +106,6 @@ class PhoenixdWallet(Wallet):
unhashed_description: bytes | None = None,
**kwargs,
) -> InvoiceResponse:
try:
msats_amount = amount
data: dict[str, Any] = {
@@ -309,7 +313,7 @@ class PhoenixdWallet(Wallet):
and message_json.get("type") == "payment_received"
):
logger.info(
f'payment-received: {message_json["paymentHash"]}'
f"payment-received: {message_json['paymentHash']}"
)
yield message_json["paymentHash"]
@@ -319,3 +323,49 @@ class PhoenixdWallet(Wallet):
"retrying in 5 seconds"
)
await asyncio.sleep(5)
def _load_mnemonic_from_seed_file(self):
data_dir = settings.phoenixd_data_dir
if not data_dir:
return
seed_path = Path(data_dir).expanduser() / "seed.dat"
if not seed_path.is_file():
return
try:
mnemonic = seed_path.read_text(encoding="utf-8").strip()
if mnemonic == settings.phoenixd_mnemonic:
return
except OSError as exc:
logger.warning(f"Failed to read Phoenixd seed file '{seed_path}': {exc}")
return
if not mnemonic:
logger.warning(f"Phoenixd seed file '{seed_path}' is empty.")
return
if not mnemonic_is_valid(mnemonic):
logger.warning(
f"Phoenixd seed file '{seed_path}' does not contain a valid "
"BIP39 mnemonic."
)
return
settings.phoenixd_mnemonic = mnemonic
self._seed_mnemonic_to_persist = mnemonic
async def _persist_loaded_mnemonic(self):
if not self._seed_mnemonic_to_persist:
return
logger.info("Updating 'PHOENIXD_MNEMONIC' mnemonic settings.")
try:
from lnbits.core.crud.settings import set_settings_field
await set_settings_field(
"phoenixd_mnemonic", self._seed_mnemonic_to_persist
)
self._seed_mnemonic_to_persist = None
except Exception as exc:
logger.warning(f"Failed to persist Phoenixd mnemonic: {exc}")
+15 -11
View File
@@ -6,7 +6,7 @@
"": {
"name": "lnbits",
"dependencies": {
"axios": "^1.13.5",
"axios": "^1.15.0",
"chart.js": "^4.5.1",
"moment": "^2.30.1",
"nostr-tools": "^2.18.2",
@@ -734,14 +734,14 @@
"license": "MIT"
},
"node_modules/axios": {
"version": "1.13.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
"integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^1.1.0"
"proxy-from-env": "^2.1.0"
}
},
"node_modules/balanced-match": {
@@ -1498,9 +1498,9 @@
"license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1555,9 +1555,13 @@
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/pyright": {
"version": "1.1.289",
+1 -1
View File
@@ -21,7 +21,7 @@
"terser": "^5.44.1"
},
"dependencies": {
"axios": "^1.13.5",
"axios": "^1.15.0",
"chart.js": "^4.5.1",
"moment": "^2.30.1",
"nostr-tools": "^2.18.2",
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "lnbits"
version = "1.5.2"
version = "1.5.4-rc1"
requires-python = ">=3.10,<3.13"
description = "LNbits, free and open-source Lightning wallet and accounts system."
authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
+42
View File
@@ -135,6 +135,8 @@ async def test_auth_api_first_install_success_and_validation(
)
assert success.status_code == 200
assert success.json()["access_token"]
assert "cookie_access_token=" in success.headers["set-cookie"]
assert "Secure" not in success.headers["set-cookie"]
updated_superuser = await get_account(settings.super_user, active_only=False)
assert updated_superuser is not None
@@ -159,3 +161,43 @@ async def test_auth_api_first_install_success_and_validation(
await update_account(restored_superuser)
settings.first_install = original_first_install
settings.first_install_token = original_first_install_token
@pytest.mark.anyio
async def test_auth_api_first_install_uses_secure_cookie_when_enabled(
http_client: AsyncClient, settings: Settings
):
superuser = await get_account(settings.super_user, active_only=False)
assert superuser is not None
original_username = superuser.username
original_password_hash = superuser.password_hash
original_first_install = settings.first_install
original_auth_https_only = settings.auth_https_only
new_username = f"secure_{uuid4().hex[:8]}"
try:
settings.first_install = True
settings.auth_https_only = True
success = await http_client.put(
"/api/v1/auth/first_install",
json={
"username": new_username,
"password": "secret1234",
"password_repeat": "secret1234",
},
)
assert success.status_code == 200
assert "cookie_access_token=" in success.headers["set-cookie"]
assert "Secure" in success.headers["set-cookie"]
finally:
restored_superuser = await get_account(settings.super_user, active_only=False)
assert restored_superuser is not None
restored_superuser.username = original_username
restored_superuser.password_hash = original_password_hash
await update_account(restored_superuser)
settings.first_install = original_first_install
settings.auth_https_only = original_auth_https_only
+40 -1
View File
@@ -1,4 +1,6 @@
import asyncio
import copy
import inspect
from datetime import datetime, timezone
from uuid import uuid4
@@ -23,7 +25,12 @@ from lnbits.core.services import create_user_account, update_wallet_balance
from lnbits.core.services.payments import create_wallet_invoice
from lnbits.core.views.auth_api import first_install
from lnbits.db import DB_TYPE, SQLITE, Database
from lnbits.settings import AuthMethods, FiatProviderLimits, Settings
from lnbits.settings import (
AuthMethods,
EditableSettings,
FiatProviderLimits,
Settings,
)
from lnbits.settings import settings as lnbits_settings
from lnbits.wallets.fake import FakeWallet
from tests.helpers import (
@@ -33,6 +40,23 @@ from tests.helpers import (
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
ADMIN_USER_ID = uuid4().hex
# Snapshot the initialized module settings instead of a fresh Settings() instance.
# The module settings include runtime-populated values like `version`.
_PURE_SETTINGS = copy.deepcopy(lnbits_settings)
_PURE_SETTINGS_FIELDS = tuple(
sorted(
{
field_name
for field_name in Settings.readonly_fields()
if field_name != "super_user"
}
| {
name
for name in inspect.signature(EditableSettings).parameters
if not name.startswith("_")
}
)
)
@pytest.fixture(scope="session")
@@ -72,6 +96,7 @@ async def app(settings: Settings):
username="superadmin",
password="secret1234",
password_repeat="secret1234",
first_install_token=settings.first_install_token,
)
)
@@ -327,7 +352,21 @@ async def new_user(username: str | None = None) -> User:
return user
def _restore_pure_settings(settings: Settings):
for field_name in _PURE_SETTINGS_FIELDS:
setattr(
settings, field_name, copy.deepcopy(getattr(_PURE_SETTINGS, field_name))
)
def _settings_cleanup(settings: Settings):
_restore_pure_settings(settings)
settings.auth_https_only = False
settings.lnbits_data_folder = "./tests/data"
settings.bundle_assets = True
settings.lnbits_admin_ui = True
settings.lnbits_extensions_default_install = []
settings.lnbits_extensions_deactivate_all = True
settings.lnbits_allow_new_accounts = True
settings.lnbits_allowed_users = []
settings.auth_allowed_methods = AuthMethods.all()
+3 -1
View File
@@ -28,7 +28,7 @@ from lnbits.core.models.extensions_builder import (
PublicPageFields,
SettingsFields,
)
from lnbits.wallets import get_funding_source
from lnbits.wallets import get_funding_source, set_funding_source
class DbTestModel(BaseModel):
@@ -182,6 +182,8 @@ def make_lnurl_pay_response(
)
set_funding_source()
funding_source = get_funding_source()
is_fake: bool = funding_source.__class__.__name__ == "FakeWallet"
is_regtest: bool = not is_fake
+1 -9
View File
@@ -9,15 +9,7 @@ from loguru import logger
from lnbits.wallets import get_funding_source
funding_source = get_funding_source()
def is_boltz_wallet():
print(
"### funding_source.__class__.__name__ 2",
get_funding_source().__class__.__name__,
)
return get_funding_source().__class__.__name__ == "BoltzWallet"
is_boltz_wallet = funding_source.__class__.__name__ == "BoltzWallet"
docker_lightning_cli = [
"docker",
@@ -21,8 +21,7 @@ async def test_create_invoice(from_wallet):
)
# we cannot know the preimage of the swap yet
funding_source = get_funding_source()
if not is_boltz_wallet():
if not is_boltz_wallet:
assert payment.preimage
invoice = decode(payment.bolt11)
@@ -43,8 +42,7 @@ async def test_create_internal_invoice(from_wallet):
)
# we cannot know the preimage of the swap yet
funding_source = get_funding_source()
if not is_boltz_wallet():
if not is_boltz_wallet:
assert payment.preimage
invoice = decode(payment.bolt11)
+1 -1
View File
@@ -20,7 +20,7 @@ async def test_services_pay_invoice(to_wallet, real_invoice):
)
assert payment
assert payment.memo == description
if not is_boltz_wallet():
if not is_boltz_wallet:
assert payment.status == PaymentState.SUCCESS
assert payment.preimage
else:
+140
View File
@@ -0,0 +1,140 @@
from pathlib import Path
from uuid import uuid4
import pytest
from lnbits.core.crud import create_account, delete_account, get_account
from lnbits.core.crud.settings import get_settings_field, set_settings_field
from lnbits.core.db import db
from lnbits.core.models import Account, UpdateSuperuserPassword, UserExtra
from lnbits.core.services.users import check_admin_settings
from lnbits.core.views.auth_api import first_install
from lnbits.settings import settings
async def _restore_setting_field(field_name: str, original_row) -> None:
if original_row is None:
await db.execute(
"DELETE FROM system_settings WHERE id = :id AND tag = :tag",
{"id": field_name, "tag": "core"},
)
return
await set_settings_field(field_name, original_row.value, original_row.tag)
def test_has_first_install_token_changed_requires_a_confirmed_mismatch():
original_token = settings.first_install_token
original_confirmed = settings.first_install_token_confirmed
try:
settings.first_install_token = "new-token"
settings.first_install_token_confirmed = "old-token"
assert settings.has_first_install_token_changed() is True
settings.first_install_token_confirmed = "new-token"
assert settings.has_first_install_token_changed() is False
settings.first_install_token_confirmed = None
assert settings.has_first_install_token_changed() is False
settings.first_install_token = None
assert settings.has_first_install_token_changed() is False
finally:
settings.first_install_token = original_token
settings.first_install_token_confirmed = original_confirmed
@pytest.mark.anyio
async def test_first_install_confirms_first_install_token(app):
temp_super_user = uuid4().hex
username = f"super_{temp_super_user[:8]}"
original_super_user = settings.super_user
original_first_install = settings.first_install
original_first_install_token = settings.first_install_token
original_first_install_token_confirmed = settings.first_install_token_confirmed
original_confirmed_row = await get_settings_field("first_install_token_confirmed")
await create_account(Account(id=temp_super_user, extra=UserExtra(provider="env")))
try:
settings.super_user = temp_super_user
settings.first_install = True
settings.first_install_token = "expected-token"
settings.first_install_token_confirmed = None
response = await first_install(
UpdateSuperuserPassword(
username=username,
password="secret1234",
password_repeat="secret1234",
first_install_token="expected-token",
)
)
assert response.status_code == 200
assert settings.first_install is False
assert settings.first_install_token_confirmed == "expected-token"
confirmed_row = await get_settings_field("first_install_token_confirmed")
assert confirmed_row is not None
assert confirmed_row.value == "expected-token"
account = await get_account(temp_super_user)
assert account is not None
assert account.username == username
assert account.extra.provider == "lnbits"
assert account.verify_password("secret1234")
finally:
await _restore_setting_field(
"first_install_token_confirmed", original_confirmed_row
)
settings.super_user = original_super_user
settings.first_install = original_first_install
settings.first_install_token = original_first_install_token
settings.first_install_token_confirmed = original_first_install_token_confirmed
await delete_account(temp_super_user)
@pytest.mark.anyio
async def test_check_admin_settings_clears_persisted_super_user_when_token_changes(app):
temp_super_user = uuid4().hex
original_super_user = settings.super_user
original_first_install = settings.first_install
original_first_install_token = settings.first_install_token
original_first_install_token_confirmed = settings.first_install_token_confirmed
original_super_user_row = await get_settings_field("super_user")
original_confirmed_row = await get_settings_field("first_install_token_confirmed")
super_user_file = Path(settings.lnbits_data_folder) / ".super_user"
await create_account(
Account(id=temp_super_user, extra=UserExtra(provider="lnbits"))
)
try:
await set_settings_field("super_user", temp_super_user)
await set_settings_field("first_install_token_confirmed", "old-token")
settings.lnbits_admin_ui = True
settings.super_user = temp_super_user
settings.first_install = False
settings.first_install_token = "new-token"
settings.first_install_token_confirmed = "old-token"
await check_admin_settings()
super_user_row = await get_settings_field("super_user")
assert super_user_row is not None
assert super_user_row.value
assert settings.first_install is True
finally:
await _restore_setting_field("super_user", original_super_user_row)
await _restore_setting_field(
"first_install_token_confirmed", original_confirmed_row
)
settings.super_user = original_super_user
settings.first_install = original_first_install
settings.first_install_token = original_first_install_token
settings.first_install_token_confirmed = original_first_install_token_confirmed
super_user_file.write_text(original_super_user)
await delete_account(temp_super_user)
+6 -1
View File
@@ -252,7 +252,12 @@ async def test_notification_for_internal_payment(
assert _payment.bolt11 == payment.bolt11
assert _payment.amount == 123_000
updated_payment = await get_payment(_payment.checking_id)
assert updated_payment.webhook_status == "404"
assert (
updated_payment.webhook_status is not None
), "Webhook should have been called."
assert (
int(updated_payment.webhook_status) >= 400
), "Webhook should have been called and failed."
@pytest.mark.anyio
+1 -1
View File
@@ -197,7 +197,7 @@ async def test_update_wallet_balance_validates_credit_and_debit(
settings.lnbits_wallet_limit_max_balance = 0
queue_mock = mocker.patch(
"lnbits.core.services.payments.internal_invoice_queue_put",
"lnbits.tasks.internal_invoice_queue_put",
mocker.AsyncMock(),
)
File diff suppressed because it is too large Load Diff
Generated
+49 -49
View File
@@ -671,48 +671,48 @@ toml = [
[[package]]
name = "cryptography"
version = "46.0.5"
version = "46.0.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" }
sdist = { url = "https://files.pythonhosted.org/packages/a4/ba/04b1bd4218cbc58dc90ce967106d51582371b898690f3ae0402876cc4f34/cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759", size = 750542, upload-time = "2026-03-25T23:34:53.396Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" },
{ url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" },
{ url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" },
{ url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" },
{ url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" },
{ url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" },
{ url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" },
{ url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" },
{ url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" },
{ url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" },
{ url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" },
{ url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" },
{ url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" },
{ url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" },
{ url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" },
{ url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" },
{ url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" },
{ url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" },
{ url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" },
{ url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" },
{ url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" },
{ url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" },
{ url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" },
{ url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" },
{ url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" },
{ url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" },
{ url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" },
{ url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" },
{ url = "https://files.pythonhosted.org/packages/eb/dd/2d9fdb07cebdf3d51179730afb7d5e576153c6744c3ff8fded23030c204e/cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c", size = 3476964, upload-time = "2026-02-10T19:18:20.687Z" },
{ url = "https://files.pythonhosted.org/packages/e9/6f/6cc6cc9955caa6eaf83660b0da2b077c7fe8ff9950a3c5e45d605038d439/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", size = 4218321, upload-time = "2026-02-10T19:18:22.349Z" },
{ url = "https://files.pythonhosted.org/packages/3e/5d/c4da701939eeee699566a6c1367427ab91a8b7088cc2328c09dbee940415/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", size = 4381786, upload-time = "2026-02-10T19:18:24.529Z" },
{ url = "https://files.pythonhosted.org/packages/ac/97/a538654732974a94ff96c1db621fa464f455c02d4bb7d2652f4edc21d600/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", size = 4217990, upload-time = "2026-02-10T19:18:25.957Z" },
{ url = "https://files.pythonhosted.org/packages/ae/11/7e500d2dd3ba891197b9efd2da5454b74336d64a7cc419aa7327ab74e5f6/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", size = 4381252, upload-time = "2026-02-10T19:18:27.496Z" },
{ url = "https://files.pythonhosted.org/packages/bc/58/6b3d24e6b9bc474a2dcdee65dfd1f008867015408a271562e4b690561a4d/cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", size = 3407605, upload-time = "2026-02-10T19:18:29.233Z" },
{ url = "https://files.pythonhosted.org/packages/47/23/9285e15e3bc57325b0a72e592921983a701efc1ee8f91c06c5f0235d86d9/cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8", size = 7176401, upload-time = "2026-03-25T23:33:22.096Z" },
{ url = "https://files.pythonhosted.org/packages/60/f8/e61f8f13950ab6195b31913b42d39f0f9afc7d93f76710f299b5ec286ae6/cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30", size = 4275275, upload-time = "2026-03-25T23:33:23.844Z" },
{ url = "https://files.pythonhosted.org/packages/19/69/732a736d12c2631e140be2348b4ad3d226302df63ef64d30dfdb8db7ad1c/cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a", size = 4425320, upload-time = "2026-03-25T23:33:25.703Z" },
{ url = "https://files.pythonhosted.org/packages/d4/12/123be7292674abf76b21ac1fc0e1af50661f0e5b8f0ec8285faac18eb99e/cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175", size = 4278082, upload-time = "2026-03-25T23:33:27.423Z" },
{ url = "https://files.pythonhosted.org/packages/5b/ba/d5e27f8d68c24951b0a484924a84c7cdaed7502bac9f18601cd357f8b1d2/cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463", size = 4926514, upload-time = "2026-03-25T23:33:29.206Z" },
{ url = "https://files.pythonhosted.org/packages/34/71/1ea5a7352ae516d5512d17babe7e1b87d9db5150b21f794b1377eac1edc0/cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97", size = 4457766, upload-time = "2026-03-25T23:33:30.834Z" },
{ url = "https://files.pythonhosted.org/packages/01/59/562be1e653accee4fdad92c7a2e88fced26b3fdfce144047519bbebc299e/cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c", size = 3986535, upload-time = "2026-03-25T23:33:33.02Z" },
{ url = "https://files.pythonhosted.org/packages/d6/8b/b1ebfeb788bf4624d36e45ed2662b8bd43a05ff62157093c1539c1288a18/cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507", size = 4277618, upload-time = "2026-03-25T23:33:34.567Z" },
{ url = "https://files.pythonhosted.org/packages/dd/52/a005f8eabdb28df57c20f84c44d397a755782d6ff6d455f05baa2785bd91/cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19", size = 4890802, upload-time = "2026-03-25T23:33:37.034Z" },
{ url = "https://files.pythonhosted.org/packages/ec/4d/8e7d7245c79c617d08724e2efa397737715ca0ec830ecb3c91e547302555/cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738", size = 4457425, upload-time = "2026-03-25T23:33:38.904Z" },
{ url = "https://files.pythonhosted.org/packages/1d/5c/f6c3596a1430cec6f949085f0e1a970638d76f81c3ea56d93d564d04c340/cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c", size = 4405530, upload-time = "2026-03-25T23:33:40.842Z" },
{ url = "https://files.pythonhosted.org/packages/7e/c9/9f9cea13ee2dbde070424e0c4f621c091a91ffcc504ffea5e74f0e1daeff/cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f", size = 4667896, upload-time = "2026-03-25T23:33:42.781Z" },
{ url = "https://files.pythonhosted.org/packages/ad/b5/1895bc0821226f129bc74d00eccfc6a5969e2028f8617c09790bf89c185e/cryptography-46.0.6-cp311-abi3-win32.whl", hash = "sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2", size = 3026348, upload-time = "2026-03-25T23:33:45.021Z" },
{ url = "https://files.pythonhosted.org/packages/c3/f8/c9bcbf0d3e6ad288b9d9aa0b1dee04b063d19e8c4f871855a03ab3a297ab/cryptography-46.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124", size = 3483896, upload-time = "2026-03-25T23:33:46.649Z" },
{ url = "https://files.pythonhosted.org/packages/c4/cc/f330e982852403da79008552de9906804568ae9230da8432f7496ce02b71/cryptography-46.0.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a", size = 7162776, upload-time = "2026-03-25T23:34:13.308Z" },
{ url = "https://files.pythonhosted.org/packages/49/b3/dc27efd8dcc4bff583b3f01d4a3943cd8b5821777a58b3a6a5f054d61b79/cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8", size = 4270529, upload-time = "2026-03-25T23:34:15.019Z" },
{ url = "https://files.pythonhosted.org/packages/e6/05/e8d0e6eb4f0d83365b3cb0e00eb3c484f7348db0266652ccd84632a3d58d/cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77", size = 4414827, upload-time = "2026-03-25T23:34:16.604Z" },
{ url = "https://files.pythonhosted.org/packages/2f/97/daba0f5d2dc6d855e2dcb70733c812558a7977a55dd4a6722756628c44d1/cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290", size = 4271265, upload-time = "2026-03-25T23:34:18.586Z" },
{ url = "https://files.pythonhosted.org/packages/89/06/fe1fce39a37ac452e58d04b43b0855261dac320a2ebf8f5260dd55b201a9/cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410", size = 4916800, upload-time = "2026-03-25T23:34:20.561Z" },
{ url = "https://files.pythonhosted.org/packages/ff/8a/b14f3101fe9c3592603339eb5d94046c3ce5f7fc76d6512a2d40efd9724e/cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d", size = 4448771, upload-time = "2026-03-25T23:34:22.406Z" },
{ url = "https://files.pythonhosted.org/packages/01/b3/0796998056a66d1973fd52ee89dc1bb3b6581960a91ad4ac705f182d398f/cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70", size = 3978333, upload-time = "2026-03-25T23:34:24.281Z" },
{ url = "https://files.pythonhosted.org/packages/c5/3d/db200af5a4ffd08918cd55c08399dc6c9c50b0bc72c00a3246e099d3a849/cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d", size = 4271069, upload-time = "2026-03-25T23:34:25.895Z" },
{ url = "https://files.pythonhosted.org/packages/d7/18/61acfd5b414309d74ee838be321c636fe71815436f53c9f0334bf19064fa/cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa", size = 4878358, upload-time = "2026-03-25T23:34:27.67Z" },
{ url = "https://files.pythonhosted.org/packages/8b/65/5bf43286d566f8171917cae23ac6add941654ccf085d739195a4eacf1674/cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58", size = 4448061, upload-time = "2026-03-25T23:34:29.375Z" },
{ url = "https://files.pythonhosted.org/packages/e0/25/7e49c0fa7205cf3597e525d156a6bce5b5c9de1fd7e8cb01120e459f205a/cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb", size = 4399103, upload-time = "2026-03-25T23:34:32.036Z" },
{ url = "https://files.pythonhosted.org/packages/44/46/466269e833f1c4718d6cd496ffe20c56c9c8d013486ff66b4f69c302a68d/cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72", size = 4659255, upload-time = "2026-03-25T23:34:33.679Z" },
{ url = "https://files.pythonhosted.org/packages/0a/09/ddc5f630cc32287d2c953fc5d32705e63ec73e37308e5120955316f53827/cryptography-46.0.6-cp38-abi3-win32.whl", hash = "sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c", size = 3010660, upload-time = "2026-03-25T23:34:35.418Z" },
{ url = "https://files.pythonhosted.org/packages/1b/82/ca4893968aeb2709aacfb57a30dec6fa2ab25b10fa9f064b8882ce33f599/cryptography-46.0.6-cp38-abi3-win_amd64.whl", hash = "sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f", size = 3471160, upload-time = "2026-03-25T23:34:37.191Z" },
{ url = "https://files.pythonhosted.org/packages/2e/84/7ccff00ced5bac74b775ce0beb7d1be4e8637536b522b5df9b73ada42da2/cryptography-46.0.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:2ea0f37e9a9cf0df2952893ad145fd9627d326a59daec9b0802480fa3bcd2ead", size = 3475444, upload-time = "2026-03-25T23:34:38.944Z" },
{ url = "https://files.pythonhosted.org/packages/bc/1f/4c926f50df7749f000f20eede0c896769509895e2648db5da0ed55db711d/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a3e84d5ec9ba01f8fd03802b2147ba77f0c8f2617b2aff254cedd551844209c8", size = 4218227, upload-time = "2026-03-25T23:34:40.871Z" },
{ url = "https://files.pythonhosted.org/packages/c6/65/707be3ffbd5f786028665c3223e86e11c4cda86023adbc56bd72b1b6bab5/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:12f0fa16cc247b13c43d56d7b35287ff1569b5b1f4c5e87e92cc4fcc00cd10c0", size = 4381399, upload-time = "2026-03-25T23:34:42.609Z" },
{ url = "https://files.pythonhosted.org/packages/f3/6d/73557ed0ef7d73d04d9aba745d2c8e95218213687ee5e76b7d236a5030fc/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:50575a76e2951fe7dbd1f56d181f8c5ceeeb075e9ff88e7ad997d2f42af06e7b", size = 4217595, upload-time = "2026-03-25T23:34:44.205Z" },
{ url = "https://files.pythonhosted.org/packages/9e/c5/e1594c4eec66a567c3ac4400008108a415808be2ce13dcb9a9045c92f1a0/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:90e5f0a7b3be5f40c3a0a0eafb32c681d8d2c181fc2a1bdabe9b3f611d9f6b1a", size = 4380912, upload-time = "2026-03-25T23:34:46.328Z" },
{ url = "https://files.pythonhosted.org/packages/1a/89/843b53614b47f97fe1abc13f9a86efa5ec9e275292c457af1d4a60dc80e0/cryptography-46.0.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6728c49e3b2c180ef26f8e9f0a883a2c585638db64cf265b49c9ba10652d430e", size = 3409955, upload-time = "2026-03-25T23:34:48.465Z" },
]
[[package]]
@@ -1263,7 +1263,7 @@ wheels = [
[[package]]
name = "lnbits"
version = "1.5.2"
version = "1.5.4rc1"
source = { editable = "." }
dependencies = [
{ name = "aiosqlite" },
@@ -2022,11 +2022,11 @@ email = [
[[package]]
name = "pygments"
version = "2.19.2"
version = "2.20.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
]
[[package]]
@@ -2114,7 +2114,7 @@ wheels = [
[[package]]
name = "pytest"
version = "9.0.2"
version = "9.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
@@ -2125,9 +2125,9 @@ dependencies = [
{ name = "pygments" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
]
[[package]]
@@ -2213,11 +2213,11 @@ wheels = [
[[package]]
name = "python-multipart"
version = "0.0.22"
version = "0.0.26"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" }
sdist = { url = "https://files.pythonhosted.org/packages/88/71/b145a380824a960ebd60e1014256dbb7d2253f2316ff2d73dfd8928ec2c3/python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17", size = 43501, upload-time = "2026-04-10T14:09:59.473Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" },
{ url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" },
]
[[package]]
@@ -2312,7 +2312,7 @@ wheels = [
[[package]]
name = "requests"
version = "2.32.5"
version = "2.33.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
@@ -2320,9 +2320,9 @@ dependencies = [
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
sdist = { url = "https://files.pythonhosted.org/packages/34/64/8860370b167a9721e8956ae116825caff829224fbca0ca6e7bf8ddef8430/requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652", size = 134232, upload-time = "2026-03-25T15:10:41.586Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
{ url = "https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b", size = 65017, upload-time = "2026-03-25T15:10:40.382Z" },
]
[[package]]