Compare commits

..
Author SHA1 Message Date
Vlad Stan 4744ef9bba Merge branch 'more_unit_tests' of https://github.com/lnbits/lnbits into more_unit_tests 2026-03-30 16:00:12 +03:00
Vlad Stan fdf061c08d chore: make regtest wait 2026-03-30 15:32:42 +03:00
Vlad Stan d348234cad fix: aaaaa 2026-03-30 15:32:42 +03:00
Vlad Stan fc5261132e fix: regtests 2026-03-30 15:32:42 +03:00
Vlad Stan 4e18c423de chore: check log 2026-03-30 15:32:42 +03:00
Vlad Stan 37e5601207 fix: try clean-up 2026-03-30 15:32:42 +03:00
Vlad Stan 231f6fc01c fix: is boltz wallet 2026-03-30 15:32:42 +03:00
Vlad Stan 811957f5e1 chore: logs 2026-03-30 15:32:42 +03:00
Vlad Stan c63a53cb14 chore: some logs 2026-03-30 15:32:42 +03:00
Vlad Stan 78b8f7f16e fix: hope for regtest to work 2026-03-30 15:32:42 +03:00
Vlad Stan 4642878c05 fix: test 2026-03-30 15:32:42 +03:00
Vlad Stan d7c850ccbb fix: test funding source 2026-03-30 15:32:42 +03:00
Vlad Stan 4fbb4b5eef chore: fix lint 2026-03-30 15:32:42 +03:00
Vlad Stan db2829a5f6 chore: postpone test 2026-03-30 15:31:59 +03:00
Vlad Stan 13ce1ac172 chore: fix lint 2026-03-30 15:08:42 +03:00
Vlad Stan 512697ae53 test: more tests 2026-03-30 14:58:04 +03:00
Vlad Stan dfa89ca063 test: some tests 2026-03-30 13:18:36 +03:00
Vlad Stan 182694d2e5 fix: bad config 2026-03-30 13:12:47 +03:00
Vlad Stan 2f10c57a10 fix: timeout 2026-03-30 13:12:46 +03:00
Vlad Stan d685824596 fix: test funding source 2026-03-30 13:12:46 +03:00
Vlad Stan e816eb2dac fix: date 2026-03-30 13:12:46 +03:00
Vlad Stan d3c099350a chore: fix lint 2026-03-30 13:12:46 +03:00
Vlad Stan 691ac969dc chore: postpone test 2026-03-30 13:12:46 +03:00
Vlad Stan 85e8ac6057 refactor: extract helpers, move private functions to the bottom 2026-03-30 13:12:46 +03:00
Vlad Stan 99da300f6d chore: code lint 2026-03-30 13:12:46 +03:00
Vlad Stan cd413d85ed fix: tests 2026-03-30 13:12:46 +03:00
Vlad Stan 9245e2c268 fix: lint 2026-03-30 13:12:46 +03:00
Vlad Stan 0b5c8cdee2 test: more api tests 2026-03-30 13:12:46 +03:00
Vlad Stan c94cef07c2 chore: fix lint 2026-03-30 13:12:46 +03:00
Vlad Stan 22b4ab779f test: more tests 2026-03-30 13:12:46 +03:00
Vlad Stan 42061d0cc8 test: some tests 2026-03-30 13:12:46 +03:00
Vlad Stan d0ea42fd70 chore: make regtest wait 2026-03-26 12:28:12 +02:00
Vlad Stan cd58a0dc9b fix: aaaaa 2026-03-26 12:12:13 +02:00
Vlad Stan cac4e309f4 fix: regtests 2026-03-26 11:57:25 +02:00
Vlad Stan 632cd257d7 chore: check log 2026-03-26 11:50:30 +02:00
Vlad Stan a11274593c fix: try clean-up 2026-03-26 11:49:57 +02:00
Vlad Stan ae92965b69 fix: is boltz wallet 2026-03-26 11:45:16 +02:00
Vlad Stan 3e1f75c4cc chore: logs 2026-03-26 11:32:02 +02:00
Vlad Stan d668747ca2 chore: some logs 2026-03-26 11:24:18 +02:00
Vlad Stan dedfcd4818 fix: hope for regtest to work 2026-03-25 11:37:21 +02:00
Vlad Stan f66f2fe6c2 fix: test 2026-03-25 11:24:25 +02:00
Vlad Stan f3ca37e846 Merge branch 'more_unit_tests' of https://github.com/lnbits/lnbits into more_unit_tests 2026-03-25 10:54:23 +02:00
Vlad Stan d45709a80f fix: bad config 2026-03-25 10:53:25 +02:00
Vlad Stan e4b6dc84b5 fix: timeout 2026-03-25 10:49:52 +02:00
Vlad Stan 67d1b4be3e fix: test funding source 2026-03-25 10:49:52 +02:00
Vlad Stan bd514f1a46 fix: date 2026-03-25 10:49:52 +02:00
Vlad Stan 03b302da69 chore: fix lint 2026-03-25 10:49:52 +02:00
Vlad Stan b373e35915 chore: postpone test 2026-03-25 10:49:52 +02:00
Vlad Stan 0fd116e7ee refactor: extract helpers, move private functions to the bottom 2026-03-25 10:49:52 +02:00
Vlad Stan 2c84f9781d chore: code lint 2026-03-25 10:49:52 +02:00
Vlad Stan 9fcf3121ac fix: tests 2026-03-25 10:49:52 +02:00
Vlad Stan 1df9fa831d fix: lint 2026-03-25 10:49:52 +02:00
Vlad Stan 42039070b9 test: more api tests 2026-03-25 10:49:52 +02:00
Vlad Stan 2f9146fff3 chore: fix lint 2026-03-25 10:49:52 +02:00
Vlad Stan 5738795c8c test: more tests 2026-03-25 10:49:52 +02:00
Vlad Stan 3c77acf266 test: some tests 2026-03-25 10:49:52 +02:00
Vlad Stan a4f2f55279 fix: timeout 2026-03-23 14:01:55 +02:00
Vlad Stan 2ab7c17ceb fix: test funding source 2026-03-23 12:33:57 +02:00
Vlad Stan 7a2c509cd3 fix: date 2026-03-23 12:32:39 +02:00
Vlad Stan 8a51ea53c8 chore: fix lint 2026-03-23 12:32:39 +02:00
Vlad Stan 19d3e9e64e chore: postpone test 2026-03-23 12:32:39 +02:00
Vlad Stan 9f9f14ead0 refactor: extract helpers, move private functions to the bottom 2026-03-23 12:32:39 +02:00
Vlad Stan 0a093d82ae chore: code lint 2026-03-23 12:32:39 +02:00
Vlad Stan 17fd459fa7 fix: tests 2026-03-23 12:32:39 +02:00
Vlad Stan fe9aa4d8c9 fix: lint 2026-03-23 12:32:39 +02:00
Vlad Stan 1af3b62739 test: more api tests 2026-03-23 12:32:39 +02:00
Vlad Stan e9cbc3492f chore: fix lint 2026-03-23 12:32:39 +02:00
Vlad Stan ca2f78d25b test: more tests 2026-03-23 12:32:39 +02:00
Vlad Stan 9d9ab48345 test: some tests 2026-03-23 12:32:39 +02:00
61 changed files with 364 additions and 1512 deletions
+1 -11
View File
@@ -7,20 +7,14 @@
# They are NOT managed by the Admin UI and are not stored in the database.
# === First Install Token ===
# If set the user is required to enter this token on the /first_install page
# if set the user is required to enter this token on the /first_install page
# FIRST_INSTALL_TOKEN="myaccesstoken"
# === Security ===
# When enabled (recommended), auth cookies require HTTPS and SSO will reject insecure HTTP.
AUTH_HTTPS_ONLY=true
# === Logging and Development ===
DEBUG=False
DEBUG_DATABASE=False
BUNDLE_ASSETS=True
# add `?profiler=true` to the url to enable the profiler for that request
PROFILER=False
# logging into LNBITS_DATA_FOLDER/logs/
ENABLE_LOG_TO_FILE=true
@@ -30,10 +24,6 @@ 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,10 +69,7 @@ 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 -1
View File
@@ -65,7 +65,7 @@ jobs:
make: openapi
regtest:
needs: [ lint ]
needs: [ lint, test-api, test-wallets, test-unit, migration, openapi ]
uses: ./.github/workflows/regtest.yml
strategy:
matrix:
-1
View File
@@ -6,7 +6,6 @@ __pycache__
*$py.class
.mypy_cache
.vscode
.codex
*-lock.json
.python-version
-79
View File
@@ -1,79 +0,0 @@
# Feature Spec: [FEAT-XXX] - Short Descriptive Title
**Milestone:** [e.g. MVP Core / Performance & Polish / Extension Framework]
**Priority:** Must-have / Should-have / Nice-to-have
**Spec Owner:** [Your Name / AI Agent Name]
**Status:** Draft → Under Review → Approved → Implemented → Verified
## 1. Purpose & User Story
As a [user type], I want [goal] so that [benefit].
_(One clear sentence. Keep it concise.)_
## 2. Functional Requirements
- [ ] REQ-1: [Clear, testable description]
- [ ] REQ-2: ...
- [ ] REQ-3: ...
_(List what the feature must do. Make each item verifiable.)_
## 3. Non-Functional Requirements
- **Performance:** [e.g. Latency < 800ms at p95, max 5k tokens, etc.]
- **Security / Safety:** [e.g. Input validation, no raw errors to user, etc.]
- **Compatibility:** [e.g. Works with all existing wallet backends, no breaking changes for extensions]
- **UI/UX:** [e.g. Follows existing Quasar/Vue patterns in wallet.js]
- **Other:** [cost, scalability, accessibility, etc.]
## 4. Technical Approach (Optional recommended for complex features)
- Proposed solution: [e.g. Extend existing CRUD in lnbits/core/, new extension, middleware change, etc.]
- Key files to modify: [list expected files]
- New dependencies: [none / specific package + version]
- Migration / Database changes: [yes/no + description]
## 5. Success Criteria & Verification
**Must pass all of these to be accepted:**
- [ ] All functional requirements (REQ-\*) implemented and tested
- [ ] Non-functional requirements met (performance, security, etc.)
- [ ] Relevant tests pass: `make test-unit`, `make test-api`, `make test-regtest` (as applicable)
- [ ] `make check` passes (ruff, mypy, pyright, prettier, checkbundle)
- [ ] Constitution compliance: All changes respect CONSTITUTION.md
- [ ] Backward compatibility: No breakage for existing extensions or wallet backends
- [ ] Documentation updated (if applicable: README, OpenAPI, inline comments)
**Additional Tests / Edge Cases:**
- [ ] Test invalid inputs / error paths
- [ ] Test with FakeWallet and at least one real backend
- [ ] Test with multiple extensions installed
## 6. Safety & Risk Assessment
- Potential risks: [e.g. Payment flow impact, key exposure, extension conflicts]
- Mitigation: [how addressed]
- Security review needed: [yes/no]
## 7. Implementation Notes (for AI / Developer)
- Style to match: Existing code patterns in `lnbits/core/` and `wallet.js`
- Surgical changes only (per AGENTS.md)
- Any known gotchas or dependencies on other features:
## 8. Acceptance Checklist (Sign-off)
- [ ] Spec reviewed and approved by project owner
- [ ] Implementation completed
- [ ] Verification steps passed
- [ ] PR created with link to this spec
- [ ] Constitution & AGENTS.md compliance confirmed
---
**Created:** [Date]
**Last Updated:** [Date]
**Approved By:** [Name / "Approved"]
-122
View File
@@ -1,122 +0,0 @@
# AGENTS.md - Instructions for All AI Coding Agents
This file is the **master instruction manual** for any AI agent (Grok, Claude, Cursor, Aider, etc.) working on LNbits.
## 1. Core Rule (Never Break This)
**You MUST read and strictly follow `CONSTITUTION.md` before doing any planning, coding, refactoring, or suggesting changes.**
- Every single change, feature, extension, or fix **must comply** with the Constitution.
- If you detect a violation (in new code or existing code), you **must** flag it immediately and propose a fix or ask for clarification.
- Constitution > any other instruction (including this file, user prompts, or previous conversations).
## 2. Mandatory Development Workflow
For **any non-trivial task** (new feature, bug fix, refactor, extension change):
1. **Constitution Check** Re-read relevant sections of `CONSTITUTION.md`
2. **Feature Spec Check** If a spec exists in `.specify/`, follow it exactly. If none exists, ask the user for clarification or propose a minimal spec.
3. **Think Step-by-Step** Follow the "Think Before Coding" and "Simplicity First" guidelines below.
4. **Surgical Changes** Only touch what is necessary.
5. **Implement**
6. **Verify** Run relevant tests (`make test-unit`, `make test-api`, etc.), `make check`, and confirm compliance.
7. **Report** Always include a clear summary.
Use the following response format:
```markdown
## Constitution & Spec Compliance
- Relevant Constitution sections checked: [list or quote key rules]
- Feature Spec followed: [yes / no / proposed]
## Assumptions & Plan
- Assumptions: ...
- Plan:
1. ...
2. ...
- Tradeoffs considered: ...
## Changes Made
- Files changed: ...
- Summary of modifications:
## Verification
- [ ] Passes `make check`
- [ ] Relevant tests pass (`make test-xxx`)
- [ ] Complies with Constitution
- [ ] Surgical & minimal (no unrelated changes)
```
## 3. Behavioral Guidelines (Merged & Project-Specific)
**Think Before Coding**
- Don't assume. Don't hide confusion. Surface tradeoffs.
- State assumptions explicitly. If uncertain, ask.
- If multiple interpretations exist, present them — don't pick silently.
- If something is unclear (especially regarding wallets, extensions, or funding sources), stop and ask.
**Simplicity First**
- Minimum code that solves the problem. Nothing speculative.
- No features beyond what was asked.
- No abstractions for single-use code.
- Respect LNbits' lean core philosophy: new functionality should preferably go into an **extension** unless it truly belongs in core.
**Surgical Changes**
- Touch only what you must. Clean up only your own mess.
- Match existing style (Python: Black + Ruff rules; JS: Prettier).
- Do not "improve" or refactor adjacent code unless explicitly asked.
- When editing, remove only imports/variables/functions made unused **by your changes**.
- Never delete pre-existing dead code unless instructed.
**Goal-Driven Execution**
- Transform tasks into verifiable goals.
- For tests: Write or update tests first when fixing bugs or adding behavior.
- Always consider impact on existing extensions and multiple wallet backends (LND, CLN, Boltz, VoidWallet, etc.).
## 4. LNbits-Specific Rules
- **Extensions First**: Core should remain lean. Prefer implementing new features as extensions unless they are fundamental to wallets, security, or the API.
- **Testing**: Use `FakeWallet` for unit/API tests. Regtest tests for full Lightning flows. Never break existing test targets in the Makefile.
- **Dependencies**: Never add new dependencies without updating `pyproject.toml` and getting approval.
- **Frontend**: JS/Vue code (e.g. `wallet.js`) must follow existing patterns and pass `make checkbundle` when static files are affected.
- **Database / Migrations**: Do not make raw SQL changes. Use existing CRUD/services and migration tooling.
- **Security**: Be extremely cautious with anything touching payments, keys, LNURL, Bolt11, or admin routes.
- **Tools**: Use `uv run` for all commands. Prefer Makefile targets (`make format`, `make check`, `make test-xxx`).
- **Generated Files**: Never modify gRPC files or other generated code.
## 5. Forbidden Behaviors
- Ignoring Constitution rules to "be helpful"
- Large refactors without a spec or explicit request
- Adding features "for future use"
- Breaking backward compatibility for extensions or existing wallet backends
- Committing code that fails `make check` or relevant tests
- Exposing raw errors/stack traces to users
- Using synchronous code in hot async paths without justification
## 6. How to Handle This File + Constitution
When the user gives you a task, start your response with:
> Following LNbits CONSTITUTION.md and AGENTS.md...
Then proceed with the structured format above.
---
**These guidelines are working if:**
- Fewer unnecessary changes appear in diffs
- Clarifying questions come **before** implementation
- All changes respect the lean, extension-first, security-first nature of LNbits
- Tests and `make check` continue to pass
Last Updated: April 2026
-136
View File
@@ -1,136 +0,0 @@
# CONSTITUTION.md
This is the immutable constitution of the LNbits project.
Every feature spec, code change, refactor, extension, or decision by humans or AI agents **must comply** with this document.
Changes to this file require explicit approval from the project owner/maintainers.
## 1. Project Overview
**Project Name:** LNbits
**Core Purpose:** Free and open-source Lightning wallet and accounts system. A lightweight Python server that sits on top of any Lightning funding source, providing safe isolated wallets, a clean REST API, and a powerful extension system for adding features rapidly.
**Target Users:** Individuals, communities, merchants, developers, and enterprises building on Bitcoin/Lightning (self-hosted or as part of larger stacks).
**High-Level Success Criteria:**
- Reliable multi-wallet Lightning accounting with any backend (20+ supported funding sources)
- Secure, extensible via 60+ extensions without bloating core
- High code quality, test coverage, and backward compatibility for extensions
- Production-ready performance and security for real Bitcoin value
**Version:** 1.5.4
## 2. Technology Stack (Strict)
- **Language:** Python >=3.10, <3.13 (strictly enforced via `pyproject.toml`)
- **Framework:** FastAPI + Starlette (backend API)
- **Frontend:** Vue.js + Quasar framework, with bundled static assets
- **Database:** SQLite (aiosqlite) by default, PostgreSQL (asyncpg/psycopg2) supported via `LNBITS_DATABASE_URL`
- **Async Runtime:** uvloop preferred
- **Dependency Management:** uv + pyproject.toml (Hatchling build backend). Use `uv run` for all commands.
- **Wallet Backends:** Abstracted via `lnbits.wallets` support for LND, Core Lightning, Phoenixd, Boltz, Breez SDK, Liquid, VoidWallet fallback, etc. New backends must follow existing abstraction.
- **Other Key Libs:** SQLAlchemy, Pydantic (v1), Loguru, Jinja2, LNURL, Bolt11, etc. (see `pyproject.toml` for pinned versions)
- **Build/Frontend Tools:** npm for bundling (Quasar/Vue), Prettier for JS/CSS (check Makefile targets)
**Forbidden:**
- Adding new top-level dependencies without updating `pyproject.toml` **and** team approval
- Using synchronous blocking calls in async paths (except where explicitly justified)
- Direct database queries outside of CRUD layers or core services
- Modifying generated files (e.g., gRPC files in wallets/boltz_grpc_files or lnd_grpc_files)
## 3. Architecture & Code Organization (Mandatory Rules)
- **Core Principle:** Modular monolith with heavy emphasis on **extensions**. All non-core features must live in extensions (installed via `lnbits/extensions`). Core stays lean.
- **Backend Structure:**
- `lnbits/core/` for core models, CRUD, services, routers, tasks
- `lnbits/wallets/` for funding source abstractions
- `lnbits/extensions/` for installed/upgradeable extensions (do not commit large extensions to core repo)
- `lnbits/static/` for bundled frontend assets (managed via npm bundle)
- **Key Rules:**
- Use dependency injection and FastAPI routers properly
- Extensions register routes/tasks via `register_ext_routes` / `register_ext_tasks`
- Database migrations handled centrally (extension-specific migrations)
- All new endpoints must be under proper versioning/prefixing where applicable
- Frontend: Vue 2/Quasar components in `wallet.js` style (or updated) keep reactive, use LNbits.utils helpers
- No circular imports; respect existing middleware order (e.g., InstalledExtensionMiddleware before ExtensionsRedirectMiddleware)
**Exclusions** (do not lint/format these):
- `lnbits/extensions/`, `lnbits/upgrades/`, generated gRPC files, static/vendor bundles
## 4. Code Quality & Style
- **Formatting & Linting:**
- Python: Black (line-length 88), Ruff (with selected rules: F, E, W, I, A, C, N, UP, RUF, B, S), MyPy (strict where possible), Pyright
- JS/Frontend: Prettier
- Run via Makefile: `make format` and `make check`
- **Type Checking:** MyPy + Pyright enforced on `lnbits/`, `tests/`, `tools/`
- **Testing Requirements:**
- Unit, API, wallet, and regtest tests via pytest (see Makefile targets)
- New core code or critical paths: high coverage expected (`--cov=lnbits`)
- Extensions should include their own tests where possible
- **Error Handling & Logging:** Use Loguru with structured context. Never expose raw stack traces to end users. Graceful fallbacks (e.g., VoidWallet on funding source failure).
- **Pre-commit:** Strongly recommended (`make install-pre-commit-hook`)
- **Bundle Integrity:** Frontend bundles must pass `make checkbundle` before commits affecting static files.
## 5. AI / LLM Usage Standards (if any agents or future AI features are added)
- Any new AI-powered features (e.g., via extensions) must use structured outputs (Pydantic/JSON mode)
- Store prompts/templates versioned in the extension
- Prefer deterministic behavior for financial/security paths (low temperature)
- All AI outputs involving value/money must be validated server-side
- Safety: Never allow untrusted model output to influence payments, wallet balances, or admin actions without guardrails
## 6. Safety, Security & Ethics
- **Critical:** Handle real Bitcoin/Lightning value → security-first mindset
- Per-wallet isolation with separate admin/invoice/read keys
- Rate limiting (SlowAPI) and IP blocking middleware mandatory
- Sanitize all user inputs; validate LNURL, Bolt11, etc.
- PII: Minimal collection; respect privacy (no unnecessary logging of sensitive data)
- Funding source failures: Graceful degradation to VoidWallet + clear logging
- Extensions: Hash-verified installs for vetted extensions; careful with custom extension paths
- Audit logging via AuditMiddleware
- Forbidden: Hard-coded secrets, insecure subprocess calls without review, SQL injection risks (use SQLAlchemy properly)
## 7. Performance & Cost Budgets
- Keep core lightweight extensions handle heavy features
- Async-first (uvloop, asyncpg/aiosqlite)
- Reasonable retry logic for funding source connections (see `check_funding_source`)
- Frontend: Optimized bundles (checkbundle enforced)
- No unnecessary blocking operations in request paths
## 8. Development Workflow (Spec-Driven where possible)
- **All significant changes** should follow Spec-Driven Development:
- Create/update Feature Spec in `.specify/` folder (or equivalent)
- Reference this Constitution in every spec and PR
- Use Makefile targets for format/check/test
- Tests run with `FakeWallet` by default for unit/API; regtest for full flows
- PRs must:
- Pass `make check` and relevant tests
- Include Constitution compliance notes (via AGENTS.md/CLAUDE.md)
- Not break existing extensions or wallet backends
- Branching: Protect main; use feature branches
- Extensions: Develop separately; core repo focuses on framework stability
## 9. Decision Hierarchy (What Takes Precedence)
1. This Constitution
2. Approved Feature Spec / Milestone
3. Existing tests and backward compatibility (especially for extensions and wallet backends)
4. Project maintainers / owner decision
5. Everything else (including helpful AI suggestions)
If conflict: Stop, document the issue, and seek clarification from maintainers.
## 10. Amendment Process
- This Constitution can only be changed with explicit approval from project maintainers.
- All changes must be dated, versioned, and reflected in `AGENTS.md`.
- Minor clarifications can be proposed via PR with justification.
---
**Last Updated:** April 2026 (based on v1.5.4)
**Owner/Maintainers Approval:** LNbits Team
+11 -11
View File
@@ -1,12 +1,12 @@
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/logos/lnbits-full-inverse.svg">
<img src="docs/logos/lnbits-full.svg" alt="LNbits" style="width:300px">
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png">
<img src="https://i.imgur.com/fyKPgVT.png" alt="LNbits" style="width:300px">
</picture>
</a>
![phase: stable](https://img.shields.io/badge/phase-stable-2EA043) [![license-badge]](LICENSE) [![docs-badge]][docs] ![PRs: welcome](https://img.shields.io/badge/PRs-Welcome-yellow) [![explore: LNbits extensions](https://img.shields.io/badge/explore-LNbits%20extensions-10B981)](https://extensions.lnbits.com/) [![hardware: LNBitsShop](https://img.shields.io/badge/hardware-LNBitsShop-7C3AED)](https://shop.lnbits.com/) [<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
<img alt="lnbits_head" src="docs/assets/header.jpg" />
![phase: stable](https://img.shields.io/badge/phase-stable-2EA043) [![license-badge]](LICENSE) [![docs-badge]][docs] ![PRs: welcome](https://img.shields.io/badge/PRs-Welcome-yellow) [![explore: LNbits extensions](https://img.shields.io/badge/explore-LNbits%20extensions-10B981)](https://extensions.lnbits.com/) [![hardware: LNBitsShop](https://img.shields.io/badge/hardware-LNBitsShop-7C3AED)](https://shop.lnbits.com/) [<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits) [<img src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">](https://opensats.org)
<img width="2000" height="203" alt="lnbits_head" src="https://github.com/user-attachments/assets/77669718-ac10-43c7-ae95-6ce236c77401" />
[![tip-hero](https://img.shields.io/badge/TipJar-LNBits%20Hero-9b5cff?labelColor=6b7280&logo=lightning&logoColor=white)](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg)
# LNbits — The most powerful Bitcoin & Lightning toolkit
@@ -46,7 +46,7 @@ Get yourself familiar and test on our demo server [demo.lnbits.com](https://demo
LNbits is packaged with tools to help manage funds, such as a table of transactions, line chart of spending, export to csv. Each wallet also comes with its own API keys, to help partition the exposure of your funding source.
<img alt="lnbits_wallet" src="docs/assets/wallet.jpg" />
<img src="https://i.imgur.com/w8jdGpF.png" style="width:800px">
## LNbits extension universe
@@ -54,25 +54,25 @@ Extend YOUR LNbits to meet YOUR needs.
All non-core features are installed as extensions, reducing your code base and making your LNbits unique to you. Extend your LNbits install in any direction, and even create and share your own extensions.
<img alt="lnbits_extensions" src="docs/assets/extensions.jpg" />
<img src="https://i.imgur.com/aEBpwJF.png" style="width:800px">
## LNbits API
LNbits has a powerful API, many projects use LNbits to do the heavy lifting for their bitcoin/lightning services.
<img alt="lnbits_api" src="docs/assets/api.jpg" />
<img src="https://i.imgur.com/V742sb9.png" style="width:800px">
## LNbits node manager
LNbits comes packaged with a light node management UI, to make running your node that much easier.
<img alt="lnbits_api" src="docs/assets/lightning_node.jpg" />
<img src="https://i.imgur.com/TYqIK60.png" style="width:800px">
## LNbits merchant tools
## LNbits across all your devices
The LNbits stack can process both bitcoin and fiat payments, making it a turnkey, all-in-one solution for merchants. With orders and inventory shared across extensions, and built-in notifications for Nostr, Telegram, and email, LNbits keeps everything in sync, freeing merchants to focus on their business.
As well as working great in a browser, LNbits has native IoS and Android apps as well as a chrome extension. So you can enjoy the same UI across ALL your devices.
<img alt="lnbits_merchants" src="docs/assets/merchants_small.webp" />
<img src="https://i.imgur.com/J96EbRf.png" style="width:800px">
## Powered by LNbits
Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 305 KiB

+1
View File
@@ -14,6 +14,7 @@ nav_order: 1
![phase: stable](https://img.shields.io/badge/phase-stable-2EA043)
![PRs: welcome](https://img.shields.io/badge/PRs-Welcome-yellow)
[<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
[<img src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">](https://opensats.org)
# LNBits Admin UI
+1
View File
@@ -14,6 +14,7 @@ nav_order: 1
![phase: stable](https://img.shields.io/badge/phase-stable-2EA043)
![PRs: welcome](https://img.shields.io/badge/PRs-Welcome-yellow)
[<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
[<img src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">](https://opensats.org)
# Backend Wallet Comparison Table
+1 -1
View File
@@ -11,7 +11,7 @@ nav_order: 1
</picture>
</a>
![phase: stable](https://img.shields.io/badge/phase-stable-2EA043) ![License: MIT](https://img.shields.io/badge/License-MIT-blue) ![PRs: welcome](https://img.shields.io/badge/PRs-Welcome-yellow) [![explore: LNbits extensions](https://img.shields.io/badge/explore-LNbits%20extensions-10B981)](https://extensions.lnbits.com/) [<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
![phase: stable](https://img.shields.io/badge/phase-stable-2EA043) ![License: MIT](https://img.shields.io/badge/License-MIT-blue) ![PRs: welcome](https://img.shields.io/badge/PRs-Welcome-yellow) [![explore: LNbits extensions](https://img.shields.io/badge/explore-LNbits%20extensions-10B981)](https://extensions.lnbits.com/) [<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits) <img src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">
# Basic installation
+1
View File
@@ -14,6 +14,7 @@ nav_order: 1
![phase: stable](https://img.shields.io/badge/phase-stable-2EA043)
![PRs: welcome](https://img.shields.io/badge/PRs-Welcome-yellow)
[<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
[<img src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">](https://opensats.org)
# LNbits Super User (SU)
+1
View File
@@ -14,6 +14,7 @@ nav_order: 1
![phase: stable](https://img.shields.io/badge/phase-stable-2EA043)
![PRs: welcome](https://img.shields.io/badge/PRs-Welcome-yellow)
[<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
[<img src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">](https://opensats.org)
# LNbits Roles: A Quick Overview
+1
View File
@@ -14,6 +14,7 @@ nav_order: 3
![phase: stable](https://img.shields.io/badge/phase-stable-2EA043)
![PRs: welcome](https://img.shields.io/badge/PRs-Welcome-yellow)
[<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
[<img src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">](https://opensats.org)
# Backend wallets
-4
View File
@@ -65,7 +65,6 @@ from .middleware import (
InstalledExtensionMiddleware,
add_first_install_middleware,
add_ip_block_middleware,
add_profiler_middleware,
add_ratelimit_middleware,
)
from .tasks import internal_invoice_listener, invoice_listener, run_interval
@@ -197,9 +196,6 @@ def create_app() -> FastAPI:
register_exception_handlers(app)
if settings.profiler:
add_profiler_middleware(app)
return app
-4
View File
@@ -12,7 +12,6 @@ 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,
@@ -59,7 +58,6 @@ 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,
@@ -119,13 +117,11 @@ __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,13 +90,6 @@ 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:
-7
View File
@@ -37,13 +37,6 @@ 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,7 +9,6 @@ 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 (
@@ -39,8 +38,6 @@ 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()
@@ -66,15 +63,6 @@ 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)
-12
View File
@@ -23,7 +23,6 @@ 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,
@@ -56,8 +55,6 @@ 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(
@@ -98,15 +95,6 @@ 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()
+1 -31
View File
@@ -14,8 +14,6 @@ from lnbits.core.services.fiat_providers import (
verify_paypal_webhook,
)
from lnbits.core.services.payments import create_fiat_invoice
from lnbits.fiat import get_fiat_provider
from lnbits.fiat.paypal import PayPalWallet
from lnbits.fiat.base import FiatSubscriptionPaymentOptions
from lnbits.settings import settings
@@ -188,11 +186,7 @@ async def handle_paypal_event(event: dict):
resource = event.get("resource", {})
logger.info(f"Handling PayPal event: '{event_id}'. Type: '{event_type}'.")
if event_type == "CHECKOUT.ORDER.APPROVED":
await _handle_paypal_checkout_order_approved(resource)
return
if event_type == "PAYMENT.CAPTURE.COMPLETED":
if event_type in ("CHECKOUT.ORDER.APPROVED", "PAYMENT.CAPTURE.COMPLETED"):
payment_hash = _paypal_extract_payment_hash(resource)
if not payment_hash:
logger.warning("PayPal event missing payment hash.")
@@ -211,30 +205,6 @@ async def handle_paypal_event(event: dict):
logger.warning(f"Unhandled PayPal event type: '{event_type}'.")
async def _handle_paypal_checkout_order_approved(resource: dict):
payment_hash = _paypal_extract_payment_hash(resource)
if not payment_hash:
logger.warning("PayPal approved event missing payment hash.")
return
payment = await get_standalone_payment(payment_hash)
if not payment:
logger.warning(f"No payment found for hash: '{payment_hash}'.")
return
fiat_provider = await get_fiat_provider("paypal")
if not isinstance(fiat_provider, PayPalWallet):
logger.warning("PayPal provider unavailable for approved order capture.")
return
capture_status = await fiat_provider.capture_order(
payment.extra.get("fiat_checking_id") or payment.checking_id
)
if capture_status.failed:
logger.warning(f"PayPal order capture failed for hash: '{payment_hash}'.")
return
async def _handle_paypal_subscription_payment(resource: dict):
amount_info = resource.get("amount") or {}
currency = (amount_info.get("currency") or "").upper()
+2 -6
View File
@@ -98,16 +98,12 @@ async def api_install_extension(data: CreateExtension):
ext_info.clean_extension_files()
detail = (
str(exc)
if isinstance(exc, (AssertionError, ValueError))
if isinstance(exc, AssertionError)
else f"Failed to install extension '{ext_info.id}'."
f"({ext_info.installed_version})."
)
raise HTTPException(
status_code=(
HTTPStatus.BAD_REQUEST
if isinstance(exc, (AssertionError, ValueError))
else HTTPStatus.INTERNAL_SERVER_ERROR
),
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail=detail,
) from exc
+2 -4
View File
@@ -35,7 +35,7 @@ if settings.lnbits_database_url:
else:
if not database_uri.startswith("postgres://"):
raise ValueError(
"Please use the 'postgres://...' format for the database URL."
"Please use the 'postgres://...' " "format for the database URL."
)
DB_TYPE = POSTGRES
@@ -560,9 +560,7 @@ class Filters(BaseModel, Generic[TFilterModel]):
def pagination(self) -> str:
stmt = ""
if self.limit == 0:
self.limit = 1000
self.limit = 10 if self.limit is None else self.limit
self.limit = self.limit or 10
stmt += f"LIMIT {min(1000, self.limit)} "
if self.offset:
stmt += f"OFFSET {self.offset}"
+7 -25
View File
@@ -169,7 +169,7 @@ class PayPalWallet(FiatProvider):
return FiatInvoiceResponse(
ok=True,
checking_id=order_id,
checking_id=f"fiat_paypal_{order_id}",
payment_request=approval_url,
)
except Exception as exc:
@@ -285,25 +285,6 @@ class PayPalWallet(FiatProvider):
async def get_payment_status(self, checking_id: str) -> FiatPaymentStatus:
raise NotImplementedError("PayPal does not support outgoing payments.")
async def capture_order(self, checking_id: str) -> FiatPaymentStatus:
try:
await self._ensure_access_token()
paypal_id = self._normalize_paypal_id(checking_id)
if paypal_id.startswith("subscription_"):
logger.warning("PayPal subscriptions do not support order capture.")
return FiatPaymentPendingStatus()
r = await self.client.post(
f"/v2/checkout/orders/{paypal_id}/capture",
json={},
headers=self._auth_headers(),
)
r.raise_for_status()
return self._status_from_order(r.json())
except Exception as exc:
logger.warning(f"Error capturing PayPal order '{checking_id}': {exc}")
return await self.get_invoice_status(checking_id)
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
logger.warning(
"PayPal does not support paid invoices stream. Use webhooks instead."
@@ -315,7 +296,7 @@ class PayPalWallet(FiatProvider):
def _status_from_order(self, order: dict[str, Any]) -> FiatPaymentStatus:
status = (order.get("status") or "").upper()
if status == "COMPLETED":
if status in ["COMPLETED", "APPROVED"]:
return FiatPaymentSuccessStatus()
if status in ["VOIDED", "CANCELLED", "CANCELED"]:
return FiatPaymentFailedStatus()
@@ -330,10 +311,11 @@ class PayPalWallet(FiatProvider):
return FiatPaymentPendingStatus()
def _normalize_paypal_id(self, checking_id: str) -> str:
normalized = checking_id
while normalized.startswith("fiat_paypal_"):
normalized = normalized.replace("fiat_paypal_", "", 1)
return normalized
return (
checking_id.replace("fiat_paypal_", "", 1)
if checking_id.startswith("fiat_paypal_")
else checking_id
)
def _serialize_metadata(
self, payment_options: FiatSubscriptionPaymentOptions
-14
View File
@@ -7,7 +7,6 @@ from typing import Any
from fastapi import FastAPI, Request, Response
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
from loguru import logger
from pyinstrument import Profiler
from slowapi import _rate_limit_exceeded_handler
from slowapi.errors import RateLimitExceeded
from slowapi.middleware import SlowAPIMiddleware
@@ -247,16 +246,3 @@ def add_first_install_middleware(app: FastAPI):
):
return RedirectResponse("/first_install")
return await call_next(request)
def add_profiler_middleware(app: FastAPI):
@app.middleware("http")
async def profile_middleware(request: Request, call_next):
profiling = request.query_params.get("profiler", False)
if profiling:
profiler = Profiler(async_mode="enabled")
profiler.start()
_ = await call_next(request)
profiler.stop()
return HTMLResponse(profiler.output_html())
return await call_next(request)
+3 -8
View File
@@ -284,7 +284,7 @@ class ThemesSettings(LNbitsSettings):
lnbits_custom_image: str | None = Field(default="/static/images/logos/lnbits.svg")
lnbits_ad_space_title: str = Field(default="Supported by")
lnbits_ad_space: str = Field(
default="https://shop.lnbits.com/;/static/images/bitcoin-shop-banner.png;/static/images/bitcoin-shop-banner.png,https://affil.trezor.io/aff_c?offer_id=169&aff_id=33845;/static/images/bitcoin-hardware-wallet.png;/static/images/bitcoin-hardware-wallet.png,https://firefish.io/?ref=lnbits;/static/images/firefish.png;/static/images/firefish.png"
default="https://shop.lnbits.com/;/static/images/bitcoin-shop-banner.png;/static/images/bitcoin-shop-banner.png,https://affil.trezor.io/aff_c?offer_id=169&aff_id=33845;/static/images/bitcoin-hardware-wallet.png;/static/images/bitcoin-hardware-wallet.png,https://firefish.io/?ref=lnbits;/static/images/firefish.png;/static/images/firefish.png,https://opensats.org/;/static/images/open-sats.png;/static/images/open-sats.png"
) # sneaky sneaky
lnbits_ad_space_enabled: bool = Field(default=False)
lnbits_allowed_currencies: list[str] = Field(default=[])
@@ -324,8 +324,7 @@ class AssetSettings(LNbitsSettings):
"heif",
"heics",
"text/plain",
"text/json",
"text/xml",
"text/json" "text/xml",
"application/json",
"application/pdf",
]
@@ -589,8 +588,6 @@ 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):
@@ -1009,8 +1006,8 @@ class EnvSettings(LNbitsSettings):
debug: bool = Field(default=False)
debug_database: bool = Field(default=False)
bundle_assets: bool = Field(default=True)
profiler: bool = Field(default=False)
# 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)
@@ -1029,8 +1026,6 @@ 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:
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,8 +275,6 @@ 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',
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

@@ -202,18 +202,7 @@ window.app.component('lnbits-admin-funding-sources', {
'Phoenixd',
{
phoenixd_api_endpoint: 'Endpoint',
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
}
phoenixd_api_password: 'Key'
}
],
[
@@ -41,37 +41,5 @@ window.app.component('lnbits-admin-site-customisation', {
]
}
},
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)
}
}
}
methods: {}
})
+8 -33
View File
@@ -545,56 +545,31 @@ window.PageAccount = {
if (file) {
this.uploadAsset(file)
}
e.target.value = null
},
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} = {}
) {
async uploadAsset(file) {
const formData = new FormData()
formData.append('file', file)
try {
const {data} = await LNbits.api.request(
await LNbits.api.request(
'POST',
`/api/v1/assets?public_asset=${isPublic}`,
`/api/v1/assets?public_asset=${this.assetsUploadToPublic}`,
null,
formData,
{
headers: {'Content-Type': 'multipart/form-data'}
}
)
if (notifySuccess) {
this.$q.notify({
type: 'positive',
message: 'Upload successful!',
icon: null
})
}
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?')
+153 -294
View File
File diff suppressed because it is too large Load Diff
+29 -29
View File
@@ -75,35 +75,7 @@
</p>
</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>
<div class="row q-col-gutter-md q-mt-lg">
<div class="row q-col-gutter-md">
<div class="col-12">
<h6 class="q-my-none">
<span v-text="$t('routing_fee_reserve_calculations')"></span>
@@ -210,6 +182,34 @@
></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,7 +52,6 @@
:label="prop.label"
:hint="prop.hint"
:value="prop.value"
:readonly="prop.readonly || false"
>
<q-btn
v-if="prop.copy"
@@ -74,15 +73,6 @@
></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,27 +251,10 @@
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">
+68 -134
View File
@@ -492,28 +492,10 @@
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">
@@ -1126,130 +1108,82 @@
<q-separator></q-separator>
<q-card-section>
<div
class="row items-center no-wrap q-col-gutter-sm"
<q-btn-dropdown
color="grey"
dense
outline
no-caps
:label="props.row.name"
:icon="props.row.is_public ? 'public' : ''"
>
<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-list>
<q-item
clickable
v-close-popup
@click="copyAssetLinkToClipboard(props.row)"
>
<q-list>
<q-item
clickable
v-close-popup
@click="
copyAssetLinkToClipboard(props.row)
"
<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 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-section>
</q-item>
<q-item
clickable
v-close-popup
@click="
toggleAssetPublicAccess(props.row)
<q-item
clickable
v-close-popup
@click="toggleAssetPublicAccess(props.row)"
>
<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 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-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
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 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-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</q-card-section>
</q-card>
</div>
+2 -52
View File
@@ -4,11 +4,9 @@ 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
@@ -63,8 +61,6 @@ 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:
@@ -73,7 +69,6 @@ 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()
@@ -106,6 +101,7 @@ class PhoenixdWallet(Wallet):
unhashed_description: bytes | None = None,
**kwargs,
) -> InvoiceResponse:
try:
msats_amount = amount
data: dict[str, Any] = {
@@ -313,7 +309,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"]
@@ -323,49 +319,3 @@ 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}")
+8 -12
View File
@@ -6,7 +6,7 @@
"": {
"name": "lnbits",
"dependencies": {
"axios": "^1.15.0",
"axios": "^1.13.5",
"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.15.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
"integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
"version": "1.13.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^2.1.0"
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
@@ -1555,13 +1555,9 @@
}
},
"node_modules/proxy-from-env": {
"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"
}
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/pyright": {
"version": "1.1.289",
+1 -1
View File
@@ -21,7 +21,7 @@
"terser": "^5.44.1"
},
"dependencies": {
"axios": "^1.15.0",
"axios": "^1.13.5",
"chart.js": "^4.5.1",
"moment": "^2.30.1",
"nostr-tools": "^2.18.2",
Generated
+2 -84
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.4.0 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand.
[[package]]
name = "aiohappyeyeballs"
@@ -3388,88 +3388,6 @@ files = [
[package.extras]
windows-terminal = ["colorama (>=0.4.6)"]
[[package]]
name = "pyinstrument"
version = "5.1.2"
description = "Call stack profiler for Python. Shows you why your code is slow!"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "pyinstrument-5.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f224fe80ba288a00980af298d3808219f9d246fd95b4f91729c9c33a0dc54fe6"},
{file = "pyinstrument-5.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7df09fc0d5b72daf48b73cdf07738761bff7f656c81aff686b3ccdd7d2abe236"},
{file = "pyinstrument-5.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75a7e17377d4405666bbaf126b1fd7bbb7e206d7246e6db3d62864d3d4790ae3"},
{file = "pyinstrument-5.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5381cc6583d26e04d9298acded4242f4fe71986f1472c8aee6992c6816f0cac5"},
{file = "pyinstrument-5.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ec08a530bef8d3492d31d8b0b12d0cfde09539f2a1c4b9678662ebc3c843e478"},
{file = "pyinstrument-5.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d671168508129b472be570bc9aee361190ba917b997c703bd134bb4de445ce7"},
{file = "pyinstrument-5.1.2-cp310-cp310-win32.whl", hash = "sha256:5957a94f84564b374a7f856d1b322345d600964280b0d687b8ddcc483f21e576"},
{file = "pyinstrument-5.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:38a2180a7801c51610b50e5d423674b21872efd019ccf05a11b7f9016cb1dcfc"},
{file = "pyinstrument-5.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3739a05583ea6312c385eb59fe985cd20d9048e95f9eeeb6a2f6c35202e2d36e"},
{file = "pyinstrument-5.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c9ee05dc75ac5fb18498c311e624f77f7f321f7ff325b251aa09e52e46f1d6a"},
{file = "pyinstrument-5.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a49a55ca5b75218767e29cacbe515d0b66fc18cb48a937bca0f77b8dafc7202"},
{file = "pyinstrument-5.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c45c14974ff04b1bfdc6c2a448627c6da7409c7800d0eb7bd03fb435dcb41d7"},
{file = "pyinstrument-5.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:22b9c04b3982c41c04b1c5ed05d1bc3a2ba26533450084058119f6dc160e70a3"},
{file = "pyinstrument-5.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5c4995ee0774801790c138f0dfec17d4e7a7ef09a6d56d53cbcbf0578a711021"},
{file = "pyinstrument-5.1.2-cp311-cp311-win32.whl", hash = "sha256:fe449e4a8ee60a2a27cf509350a584670f4c3704649601be7937598f09dbe7ca"},
{file = "pyinstrument-5.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:3fb839429671a42bf349335af4c1ce5cf83386ac11f04df0bc40720d4cb7d77d"},
{file = "pyinstrument-5.1.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2519865d4bf58936f2506c1c46a82d29a20f3239aa50c941df1ca9618c7da5f0"},
{file = "pyinstrument-5.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:059442106b8b5de29ae5ac1bdc20d044fed4da534b8caba434b6ffb119037bf5"},
{file = "pyinstrument-5.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd51f2d54fc39a4cfd73ba6be27cd0187123132ce3f445b639bff5e1b23d7e26"},
{file = "pyinstrument-5.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12af1e83795b6c640d657d339014dd1ff718b182dec736d7d1f1d8a97534eb53"},
{file = "pyinstrument-5.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2565513658e742c5eb691a779cb29d19d01bc9ee951d0eb76482e9f343c38c2e"},
{file = "pyinstrument-5.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5afd0ba788a1d112da49fb77966918e01df1f9e7d62e72894d82f7acb0996c2d"},
{file = "pyinstrument-5.1.2-cp312-cp312-win32.whl", hash = "sha256:554077b031b278593cb2301f0057be771ea62a729878c69aaf29fcdfb7b71281"},
{file = "pyinstrument-5.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:55a905384ba43efc924b8863aa6cfd276f029e4aa70c4a0e3b7389e27b191e45"},
{file = "pyinstrument-5.1.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7b8bab2334bf1d4c9e92d61db574300b914b594588a6b6dd67c45450152dfc29"},
{file = "pyinstrument-5.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:13dcc138a61298ef4994b7aebff509d2c06db89dfd6e2021f0b9cd96aaa44ec3"},
{file = "pyinstrument-5.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8abd4a7ffa2e7f9e00039a5e549e8eebc80d7ca8d43f0fb51a50ff2b117ce4a"},
{file = "pyinstrument-5.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb3a05108edebc30f31e2c69c904576042f1158b2513ab80adc08f7848a7a8f0"},
{file = "pyinstrument-5.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f70d588b53f3f35829d1d1ddfa05e07fcebf1434b3b1509d542ca317d8e9a2a5"},
{file = "pyinstrument-5.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b007327e0d6a6a01d5064883dd27c19996f044ce7488d507826fee7884e6a32e"},
{file = "pyinstrument-5.1.2-cp313-cp313-win32.whl", hash = "sha256:9ba0e6b17a7e86c3dc02d208e4c25506e8f914d9964ae89449f1f37f0b70abc0"},
{file = "pyinstrument-5.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:660d7fc486a839814db0b2f716bc13d8b99b9c780aaeb47f74a70a34adc02a7b"},
{file = "pyinstrument-5.1.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0baed297beee2bb9897e737bbd89e3b9d45a2fbbea9f1ad4e809007d780a9b1e"},
{file = "pyinstrument-5.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ebb910a32a45bde6c3fc30c578efc28a54517990e11e94b5e48a0d5479728568"},
{file = "pyinstrument-5.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bad403c157f9c6dba7f731a6fca5bfcd8ca2701a39bcc717dcc6e0b10055ffc4"},
{file = "pyinstrument-5.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f456cabdb95fd343c798a7f2a56688b028f981522e283c5f59bd59195b66df5"},
{file = "pyinstrument-5.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4e9c4dcc1f2c4a0cd6b576e3604abc37496a7868243c9a1443ad3b9db69d590f"},
{file = "pyinstrument-5.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:acf93b128328c6d80fdb85431068ac17508f0f7845e89505b0ea6130dead5ca6"},
{file = "pyinstrument-5.1.2-cp314-cp314-win32.whl", hash = "sha256:9c7f0167903ecff8b1d744f7e37b2bd4918e05a69cca724cb112f5ed59d1e41b"},
{file = "pyinstrument-5.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:ce3f6b1f9a2b5d74819ecc07d631eadececf915f551474a75ad65ac580ec5a0e"},
{file = "pyinstrument-5.1.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:af8651b239049accbeecd389d35823233f649446f76f47fd005316b05d08cef2"},
{file = "pyinstrument-5.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c6082f1c3e43e1d22834e91ba8975f0080186df4018a04b4dd29f9623c59df1d"},
{file = "pyinstrument-5.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c031eb066ddc16425e1e2f56aad5c1ce1e27b2432a70329e5385b85e812decee"},
{file = "pyinstrument-5.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f447ec391cad30667ba412dce41607aaa20d4a2496a7ab867e0c199f0fe3ae3d"},
{file = "pyinstrument-5.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:50299bddfc1fe0039898f895b10ef12f9db08acffb4d85326fad589cda24d2ee"},
{file = "pyinstrument-5.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a193ff08825ece115ececa136832acb14c491c77ab1e6b6a361905df8753d5c6"},
{file = "pyinstrument-5.1.2-cp314-cp314t-win32.whl", hash = "sha256:de887ba19e1057bd2d86e6584f17788516a890ae6fe1b7eed9927873f416b4d8"},
{file = "pyinstrument-5.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:b6a71f5e7f53c86c9b476b30cf19509463a63581ef17ddbd8680fee37ae509db"},
{file = "pyinstrument-5.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:47f14f248108f1202d48f34903bddc053a47c62ce46908aee848c1f667a1925b"},
{file = "pyinstrument-5.1.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc3f2688981af764fa2c5f5a00d7040c0c12771ca5a026730f2d826f7a28d277"},
{file = "pyinstrument-5.1.2-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7afb24d11d24fb1762059240ed97a1a4607779d5f221f91d62b67ae089bd506d"},
{file = "pyinstrument-5.1.2-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d21221a29718d5dcdc1453dbbbdd100734525672ef1e34ff03d49bc5d688ca4"},
{file = "pyinstrument-5.1.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3b68aa9f0d5217c67370762c99a80750ce56f66e5e9281c31b5baa3e4b88894d"},
{file = "pyinstrument-5.1.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:755702f01800934c3bec3c0d30483f97b6ff1fc1d2afcf6d816584703c9f1235"},
{file = "pyinstrument-5.1.2-cp38-cp38-win32.whl", hash = "sha256:2bacb980c95d4c9ea6a253e5ccf3e99993082de29ff7e7fc397e9484355577b1"},
{file = "pyinstrument-5.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:2588bc34c25d50f29d3a117c7dfac06513ee59c507b65081e66ca9569bcf45e0"},
{file = "pyinstrument-5.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bea0687665c181c6e62677fb560739a473c4286816582a43f8eb0aa6094ed529"},
{file = "pyinstrument-5.1.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:64246d2bd475870b62ed5df4808bfb33328135e8dfbdff823f9cb7d1358eb40b"},
{file = "pyinstrument-5.1.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff6fd3c7907e57f082cdd405bec1b34768c1810a165538299d259ee1bff5d7b6"},
{file = "pyinstrument-5.1.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:af09af38ee8407ca273407e24a8e6470d2444561d01005ee6a8db5f2fd908c08"},
{file = "pyinstrument-5.1.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1249d2799cdd57151b4444167e5c7736e2c9b5e79bd781b0779d631338509553"},
{file = "pyinstrument-5.1.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2d653206f50260f20bc78339c3d7aa0f19f8cf9c9f71939fbf02e2ea30353487"},
{file = "pyinstrument-5.1.2-cp39-cp39-win32.whl", hash = "sha256:d0b0c6e289725f14d0ff73f8190c953bdcb98f21c5c29c3eafb0dca8025583cb"},
{file = "pyinstrument-5.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:db8243e602aca43dc7ce8e40ed7d0ca4820d024c3c03824870c5a9e98f84e953"},
{file = "pyinstrument-5.1.2.tar.gz", hash = "sha256:af149d672da9493fa37334a1cc68f7b80c3e6cb9fd99b9e426c447db5c650bf0"},
]
[package.extras]
bin = ["click", "nox"]
docs = ["furo (==2024.7.18)", "myst-parser (==3.0.1)", "sphinx (==7.4.7)", "sphinx-autobuild (==2024.4.16)", "sphinxcontrib-programoutput (==0.17)"]
examples = ["django", "litestar", "numpy"]
test = ["cffi (>=1.17.0)", "flaky", "greenlet (>=3)", "ipython", "pytest", "pytest-asyncio (==0.23.8)", "trio"]
types = ["typing_extensions"]
[[package]]
name = "pyjwt"
version = "2.12.1"
@@ -5110,4 +5028,4 @@ migration = ["psycopg2-binary"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.10,<3.13"
content-hash = "0879a6230bbc0d0e1d8d7d090e1572ba863078b18e0d78478baf2100aa245e08"
content-hash = "42751c05e1fa8250ff90981def5e0ce8d1b284eeb2acc2c7ac9ae5847cf99e17"
+2 -3
View File
@@ -1,6 +1,6 @@
[project]
name = "lnbits"
version = "1.5.4"
version = "1.5.3"
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" }]
@@ -82,8 +82,7 @@ dev = [
"pytest-mock~=3.15.1",
"types-mock~=5.2.0.20250924",
"mock~=5.2.0",
"grpcio-tools~=1.76.0",
"pyinstrument>=5.1.2",
"grpcio-tools~=1.76.0"
]
[tool.poetry]
-42
View File
@@ -135,8 +135,6 @@ 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
@@ -161,43 +159,3 @@ 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
+1 -10
View File
@@ -80,19 +80,10 @@ async def test_callback_api_handles_paid_events_with_real_payments(mocker):
},
}
)
await handle_paypal_event(
{
"id": "evt_paypal_approved",
"event_type": "CHECKOUT.ORDER.APPROVED",
"resource": {
"purchase_units": [{"invoice_id": payment.payment_hash}],
},
}
)
await handle_paypal_event(
{
"id": "evt_paypal",
"event_type": "PAYMENT.CAPTURE.COMPLETED",
"event_type": "CHECKOUT.ORDER.APPROVED",
"resource": {
"purchase_units": [{"invoice_id": payment.payment_hash}],
},
+1 -3
View File
@@ -40,9 +40,7 @@ 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 = Settings()
_PURE_SETTINGS_FIELDS = tuple(
sorted(
{
+1 -3
View File
@@ -28,7 +28,7 @@ from lnbits.core.models.extensions_builder import (
PublicPageFields,
SettingsFields,
)
from lnbits.wallets import get_funding_source, set_funding_source
from lnbits.wallets import get_funding_source
class DbTestModel(BaseModel):
@@ -182,8 +182,6 @@ 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
+9 -1
View File
@@ -9,7 +9,15 @@ from loguru import logger
from lnbits.wallets import get_funding_source
funding_source = get_funding_source()
is_boltz_wallet = funding_source.__class__.__name__ == "BoltzWallet"
def is_boltz_wallet():
print(
"### funding_source.__class__.__name__ 2",
get_funding_source().__class__.__name__,
)
return get_funding_source().__class__.__name__ == "BoltzWallet"
docker_lightning_cli = [
"docker",
@@ -21,7 +21,8 @@ async def test_create_invoice(from_wallet):
)
# we cannot know the preimage of the swap yet
if not is_boltz_wallet:
funding_source = get_funding_source()
if not is_boltz_wallet():
assert payment.preimage
invoice = decode(payment.bolt11)
@@ -42,7 +43,8 @@ async def test_create_internal_invoice(from_wallet):
)
# we cannot know the preimage of the swap yet
if not is_boltz_wallet:
funding_source = get_funding_source()
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:
+12 -66
View File
@@ -1,32 +1,7 @@
import pytest
from lnbits.db import Filters
from tests.helpers import DbTestModel
TEST_DB_FETCH_PAGE_ROWS: tuple[dict[str, str], ...] = (
{"id": "1", "name": "Alice", "value": "foo"},
{"id": "2", "name": "Bob", "value": "bar"},
{"id": "3", "name": "Carol", "value": "bar"},
{"id": "4", "name": "Dave", "value": "bar"},
{"id": "5", "name": "Dave", "value": "foo"},
{"id": "6", "name": "Eve", "value": "foo"},
{"id": "7", "name": "Frank", "value": "bar"},
{"id": "8", "name": "Grace", "value": "foo"},
{"id": "9", "name": "Heidi", "value": "bar"},
{"id": "10", "name": "Ivan", "value": "foo"},
{"id": "11", "name": "Judy", "value": "bar"},
{"id": "12", "name": "Mallory", "value": "foo"},
{"id": "13", "name": "Niaj", "value": "bar"},
{"id": "14", "name": "Olivia", "value": "foo"},
{"id": "15", "name": "Peggy", "value": "bar"},
{"id": "16", "name": "Rupert", "value": "foo"},
{"id": "17", "name": "Sybil", "value": "bar"},
{"id": "18", "name": "Trent", "value": "foo"},
{"id": "19", "name": "Victor", "value": "bar"},
{"id": "20", "name": "Walter", "value": "foo"},
{"id": "21", "name": "Zoe", "value": "bar"},
)
@pytest.fixture(scope="session")
async def fetch_page(db):
@@ -38,14 +13,14 @@ async def fetch_page(db):
name TEXT NOT NULL
)
""")
for row in TEST_DB_FETCH_PAGE_ROWS:
await db.execute(
"""
INSERT INTO test_db_fetch_page (id, name, value)
VALUES (:id, :name, :value)
""",
row,
)
await db.execute("""
INSERT INTO test_db_fetch_page (id, name, value) VALUES
('1', 'Alice', 'foo'),
('2', 'Bob', 'bar'),
('3', 'Carol', 'bar'),
('4', 'Dave', 'bar'),
('5', 'Dave', 'foo')
""")
yield
await db.execute("DROP TABLE test_db_fetch_page")
@@ -58,35 +33,8 @@ async def test_db_fetch_page_simple(fetch_page, db):
)
assert row
assert row.total == len(TEST_DB_FETCH_PAGE_ROWS)
assert len(row.data) == Filters().limit
@pytest.mark.anyio
async def test_db_fetch_page_limit_zero_returns_all(fetch_page, db):
row = await db.fetch_page(
query="select * from test_db_fetch_page",
filters=Filters(limit=0),
model=DbTestModel,
)
assert row
assert row.total == len(TEST_DB_FETCH_PAGE_ROWS)
assert len(row.data) == len(TEST_DB_FETCH_PAGE_ROWS)
@pytest.mark.anyio
async def test_db_fetch_page_limit(fetch_page, db):
limit = 5
row = await db.fetch_page(
query="select * from test_db_fetch_page",
filters=Filters(limit=limit),
model=DbTestModel,
)
assert row
assert row.total == len(TEST_DB_FETCH_PAGE_ROWS)
assert len(row.data) == limit
assert row.total == 5
assert len(row.data) == 5
@pytest.mark.anyio
@@ -97,7 +45,7 @@ async def test_db_fetch_page_group_by(fetch_page, db):
group_by=["name"],
)
assert row
assert row.total == len({test_row["name"] for test_row in TEST_DB_FETCH_PAGE_ROWS})
assert row.total == 4
@pytest.mark.anyio
@@ -108,9 +56,7 @@ async def test_db_fetch_page_group_by_multiple(fetch_page, db):
group_by=["value", "name"],
)
assert row
assert row.total == len(
{(test_row["value"], test_row["name"]) for test_row in TEST_DB_FETCH_PAGE_ROWS}
)
assert row.total == 5
@pytest.mark.anyio
+1 -105
View File
@@ -23,14 +23,7 @@ from lnbits.core.services.fiat_providers import (
test_connection as fiat_provider_connection,
)
from lnbits.core.services.users import create_user_account
from lnbits.core.views.callback_api import handle_paypal_event
from lnbits.fiat.paypal import PayPalWallet
from lnbits.fiat.base import (
FiatInvoiceResponse,
FiatPaymentStatus,
FiatPaymentSuccessStatus,
FiatStatusResponse,
)
from lnbits.fiat.base import FiatInvoiceResponse, FiatPaymentStatus, FiatStatusResponse
from lnbits.settings import Settings
from tests.helpers import get_random_string
@@ -287,42 +280,6 @@ async def test_create_wallet_fiat_invoice_success(
assert status.success is True
@pytest.mark.anyio
async def test_create_paypal_fiat_invoice_uses_raw_order_id(
to_wallet: Wallet, settings: Settings, mocker: MockerFixture
):
settings.paypal_enabled = True
settings.paypal_client_id = "client-id"
settings.paypal_client_secret = "client-secret"
settings.paypal_limits.service_min_amount_sats = 0
settings.paypal_limits.service_max_amount_sats = 0
settings.paypal_limits.service_faucet_wallet_id = None
invoice_data = CreateInvoice(
unit="USD", amount=1.0, memo="Test", fiat_provider="paypal"
)
fiat_mock_response = FiatInvoiceResponse(
ok=True,
checking_id="ORDER123",
payment_request="https://paypal.com/checkoutnow?token=ORDER123",
)
mocker.patch(
"lnbits.fiat.PayPalWallet.create_invoice",
AsyncMock(return_value=fiat_mock_response),
)
mocker.patch(
"lnbits.utils.exchange_rates.get_fiat_rate_satoshis",
AsyncMock(return_value=1000),
)
payment = await payments.create_fiat_invoice(to_wallet.id, invoice_data)
assert payment.fiat_provider == "paypal"
assert payment.extra.get("fiat_checking_id") == "ORDER123"
assert payment.checking_id == "fiat_paypal_ORDER123"
@pytest.mark.anyio
async def test_fiat_service_fee(settings: Settings):
# settings.stripe_limits.service_min_amount_sats = 0
@@ -680,67 +637,6 @@ async def test_verify_paypal_webhook_raises_on_failed_verification(
)
def test_paypal_order_status_approved_is_pending():
wallet = object.__new__(PayPalWallet)
approved = wallet._status_from_order({"status": "APPROVED"})
completed = wallet._status_from_order({"status": "COMPLETED"})
assert approved.pending is True
assert approved.success is False
assert completed.success is True
def test_paypal_normalize_id_removes_legacy_double_prefix():
wallet = object.__new__(PayPalWallet)
assert wallet._normalize_paypal_id("fiat_paypal_ORDER123") == "ORDER123"
assert wallet._normalize_paypal_id("fiat_paypal_fiat_paypal_ORDER123") == "ORDER123"
@pytest.mark.anyio
async def test_handle_paypal_approved_event_captures_order(mocker: MockerFixture):
payment = Payment(
checking_id="fiat_paypal_ORDER123",
payment_hash="hash_123",
wallet_id="wallet_id",
amount=1000,
fee=0,
bolt11="bolt11",
status=PaymentState.PENDING,
fiat_provider="paypal",
extra={"fiat_checking_id": "ORDER123"},
)
provider = mocker.Mock(spec=PayPalWallet)
provider.capture_order = AsyncMock(return_value=FiatPaymentSuccessStatus())
mocker.patch(
"lnbits.core.views.callback_api.get_standalone_payment",
AsyncMock(return_value=payment),
)
mocker.patch(
"lnbits.core.views.callback_api.get_fiat_provider",
AsyncMock(return_value=provider),
)
status_mock = mocker.patch(
"lnbits.core.views.callback_api.check_fiat_status",
AsyncMock(),
)
await handle_paypal_event(
{
"id": "evt_paypal_approved",
"event_type": "CHECKOUT.ORDER.APPROVED",
"resource": {
"purchase_units": [{"invoice_id": payment.payment_hash}],
},
}
)
provider.capture_order.assert_awaited_once_with("ORDER123")
status_mock.assert_not_awaited()
@pytest.mark.anyio
async def test_test_connection_reports_provider_status(mocker: MockerFixture):
mocker.patch(
+1 -6
View File
@@ -252,12 +252,7 @@ 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 is not None
), "Webhook should have been called."
assert (
int(updated_payment.webhook_status) >= 400
), "Webhook should have been called and failed."
assert updated_payment.webhook_status == "404"
@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.tasks.internal_invoice_queue_put",
"lnbits.core.services.payments.internal_invoice_queue_put",
mocker.AsyncMock(),
)
Generated
+10 -44
View File
@@ -1263,7 +1263,7 @@ wheels = [
[[package]]
name = "lnbits"
version = "1.5.4"
version = "1.5.3"
source = { editable = "." }
dependencies = [
{ name = "aiosqlite" },
@@ -1334,7 +1334,6 @@ dev = [
{ name = "openai" },
{ name = "openapi-spec-validator" },
{ name = "pre-commit" },
{ name = "pyinstrument" },
{ name = "pytest" },
{ name = "pytest-cov" },
{ name = "pytest-httpserver" },
@@ -1409,7 +1408,6 @@ dev = [
{ name = "openai", specifier = "~=2.14.0" },
{ name = "openapi-spec-validator", specifier = "~=0.7.2" },
{ name = "pre-commit", specifier = "~=4.5.1" },
{ name = "pyinstrument", specifier = ">=5.1.2" },
{ name = "pytest", specifier = "~=9.0.2" },
{ name = "pytest-cov", specifier = "~=7.0.0" },
{ name = "pytest-httpserver", specifier = "~=1.1.3" },
@@ -2024,43 +2022,11 @@ email = [
[[package]]
name = "pygments"
version = "2.20.0"
version = "2.19.2"
source = { registry = "https://pypi.org/simple" }
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" }
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" }
wheels = [
{ 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]]
name = "pyinstrument"
version = "5.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/32/7f/d3c4ef7c43f3294bd5a475dfa6f295a9fee5243c292d5c8122044fa83bcb/pyinstrument-5.1.2.tar.gz", hash = "sha256:af149d672da9493fa37334a1cc68f7b80c3e6cb9fd99b9e426c447db5c650bf0", size = 266889, upload-time = "2026-01-04T18:38:58.464Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/43/74/c66e1bf3565600d78f53195efb6f8fd31610f85a58aa3fee39c56bf71d1b/pyinstrument-5.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f224fe80ba288a00980af298d3808219f9d246fd95b4f91729c9c33a0dc54fe6", size = 131470, upload-time = "2026-01-04T18:37:22.536Z" },
{ url = "https://files.pythonhosted.org/packages/1a/6b/606c5bfa311b5be74f58ef505c678216dda2be3b76a2ac770c2b0fccff77/pyinstrument-5.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7df09fc0d5b72daf48b73cdf07738761bff7f656c81aff686b3ccdd7d2abe236", size = 124567, upload-time = "2026-01-04T18:37:24.161Z" },
{ url = "https://files.pythonhosted.org/packages/15/70/c8a88defb77873513971f590549c48ceb70f7ef10f30a689762ef36dd877/pyinstrument-5.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75a7e17377d4405666bbaf126b1fd7bbb7e206d7246e6db3d62864d3d4790ae3", size = 149205, upload-time = "2026-01-04T18:37:25.696Z" },
{ url = "https://files.pythonhosted.org/packages/8f/4b/0e64fefb939af472c3fbc63ab45224766447bde73f51579f3ecc335b0a49/pyinstrument-5.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5381cc6583d26e04d9298acded4242f4fe71986f1472c8aee6992c6816f0cac5", size = 147900, upload-time = "2026-01-04T18:37:27.343Z" },
{ url = "https://files.pythonhosted.org/packages/38/6e/b4209711c61176acfeb6c351e9f88a37ed3d3bc3b749c374c0a655ee8f50/pyinstrument-5.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ec08a530bef8d3492d31d8b0b12d0cfde09539f2a1c4b9678662ebc3c843e478", size = 148133, upload-time = "2026-01-04T18:37:29.047Z" },
{ url = "https://files.pythonhosted.org/packages/26/28/f323b70789833baf0628af7b9f797b8c1a13b695bd8aa582b1312f14b602/pyinstrument-5.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d671168508129b472be570bc9aee361190ba917b997c703bd134bb4de445ce7", size = 147652, upload-time = "2026-01-04T18:37:30.682Z" },
{ url = "https://files.pythonhosted.org/packages/16/cd/9b0af0307a3a2cffb48ca76275c50b8bec3f85ca6e7b996e2e6cfbda1207/pyinstrument-5.1.2-cp310-cp310-win32.whl", hash = "sha256:5957a94f84564b374a7f856d1b322345d600964280b0d687b8ddcc483f21e576", size = 125793, upload-time = "2026-01-04T18:37:31.906Z" },
{ url = "https://files.pythonhosted.org/packages/05/89/fe4c650c252aefb8064bfdff6c0a020d33d15c55dc22abfa1f352dcc2dd1/pyinstrument-5.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:38a2180a7801c51610b50e5d423674b21872efd019ccf05a11b7f9016cb1dcfc", size = 126679, upload-time = "2026-01-04T18:37:33.59Z" },
{ url = "https://files.pythonhosted.org/packages/79/ef/0288edd620fb0cf2074d8c8e3567007a6bac66307b839d99988563de4eb8/pyinstrument-5.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3739a05583ea6312c385eb59fe985cd20d9048e95f9eeeb6a2f6c35202e2d36e", size = 131284, upload-time = "2026-01-04T18:37:35.01Z" },
{ url = "https://files.pythonhosted.org/packages/0b/4e/2a90a6997d9f7a39a6998d56de72e52673ebf5a9169a1c39dbf173e95105/pyinstrument-5.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c9ee05dc75ac5fb18498c311e624f77f7f321f7ff325b251aa09e52e46f1d6a", size = 124468, upload-time = "2026-01-04T18:37:36.628Z" },
{ url = "https://files.pythonhosted.org/packages/04/74/7bfd403e81f9b5ec523f60cced8f516ee52312752bb2e0fafabfd90bbd78/pyinstrument-5.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a49a55ca5b75218767e29cacbe515d0b66fc18cb48a937bca0f77b8dafc7202", size = 148057, upload-time = "2026-01-04T18:37:37.998Z" },
{ url = "https://files.pythonhosted.org/packages/50/3a/7205d7c199947d18edcd013af4ddf4d3cca85c5488fbe493050035947f7c/pyinstrument-5.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c45c14974ff04b1bfdc6c2a448627c6da7409c7800d0eb7bd03fb435dcb41d7", size = 146526, upload-time = "2026-01-04T18:37:39.642Z" },
{ url = "https://files.pythonhosted.org/packages/24/e8/f6864172e7ebe4bc5209bafbc574a619b4c511b9506b941789b11441be7c/pyinstrument-5.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:22b9c04b3982c41c04b1c5ed05d1bc3a2ba26533450084058119f6dc160e70a3", size = 147179, upload-time = "2026-01-04T18:37:41.332Z" },
{ url = "https://files.pythonhosted.org/packages/6d/04/89ef2d1c34767bfdbcc74ab0c7e0d021d7fac5e79873239e4ca26e97d6da/pyinstrument-5.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5c4995ee0774801790c138f0dfec17d4e7a7ef09a6d56d53cbcbf0578a711021", size = 146354, upload-time = "2026-01-04T18:37:42.808Z" },
{ url = "https://files.pythonhosted.org/packages/2e/d4/64441547ec12391b92c739a3b0685059e7dfa088d928df8364676ef7abc7/pyinstrument-5.1.2-cp311-cp311-win32.whl", hash = "sha256:fe449e4a8ee60a2a27cf509350a584670f4c3704649601be7937598f09dbe7ca", size = 125790, upload-time = "2026-01-04T18:37:44.141Z" },
{ url = "https://files.pythonhosted.org/packages/4d/8b/0a5f6b239294decb0ecd932711f3470bfbd42fc2e08a94cd5c1f4f6da7f1/pyinstrument-5.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:3fb839429671a42bf349335af4c1ce5cf83386ac11f04df0bc40720d4cb7d77d", size = 126578, upload-time = "2026-01-04T18:37:45.423Z" },
{ url = "https://files.pythonhosted.org/packages/26/d9/8fa5571ddd21b2b7189bd8b0bb4e90be1659a54dda5af51c7f6bf2b5666f/pyinstrument-5.1.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2519865d4bf58936f2506c1c46a82d29a20f3239aa50c941df1ca9618c7da5f0", size = 131419, upload-time = "2026-01-04T18:37:46.843Z" },
{ url = "https://files.pythonhosted.org/packages/6f/50/0512adb83cadfeaa1a215dc9784defff5043c5aa052d15015e3d8013af75/pyinstrument-5.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:059442106b8b5de29ae5ac1bdc20d044fed4da534b8caba434b6ffb119037bf5", size = 124446, upload-time = "2026-01-04T18:37:48.572Z" },
{ url = "https://files.pythonhosted.org/packages/9b/78/c45f0b668fb3c8c0d32058a451a8e1d34737cd7586387982185e12df1977/pyinstrument-5.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd51f2d54fc39a4cfd73ba6be27cd0187123132ce3f445b639bff5e1b23d7e26", size = 149694, upload-time = "2026-01-04T18:37:49.876Z" },
{ url = "https://files.pythonhosted.org/packages/91/4d/2ca3ca9906ce6e05070f431c54d54ccbaf57a980cfa58032d35b0b0ac1f8/pyinstrument-5.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12af1e83795b6c640d657d339014dd1ff718b182dec736d7d1f1d8a97534eb53", size = 148461, upload-time = "2026-01-04T18:37:51.544Z" },
{ url = "https://files.pythonhosted.org/packages/18/d2/bfe84a4326172ef68655b65b49fd041eeb94c8e59ee47258589b8b79dd3b/pyinstrument-5.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2565513658e742c5eb691a779cb29d19d01bc9ee951d0eb76482e9f343c38c2e", size = 148560, upload-time = "2026-01-04T18:37:52.931Z" },
{ url = "https://files.pythonhosted.org/packages/d0/00/db7f5def351e869230b0165828c4edacbf3fdda8d66aff30dd73a62082c2/pyinstrument-5.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5afd0ba788a1d112da49fb77966918e01df1f9e7d62e72894d82f7acb0996c2d", size = 148178, upload-time = "2026-01-04T18:37:54.278Z" },
{ url = "https://files.pythonhosted.org/packages/5e/bc/aea3329576e20b987d205027b8e6442ece845d681b9f9d8682d5404f81f3/pyinstrument-5.1.2-cp312-cp312-win32.whl", hash = "sha256:554077b031b278593cb2301f0057be771ea62a729878c69aaf29fcdfb7b71281", size = 125927, upload-time = "2026-01-04T18:37:55.615Z" },
{ url = "https://files.pythonhosted.org/packages/14/e2/d928434ec3a840478e95fd0d73b0dfc0b8060a07b06f4b45e9df30444e9a/pyinstrument-5.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:55a905384ba43efc924b8863aa6cfd276f029e4aa70c4a0e3b7389e27b191e45", size = 126675, upload-time = "2026-01-04T18:37:57.278Z" },
{ 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" },
]
[[package]]
@@ -2148,7 +2114,7 @@ wheels = [
[[package]]
name = "pytest"
version = "9.0.3"
version = "9.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
@@ -2159,9 +2125,9 @@ dependencies = [
{ name = "pygments" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
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" }
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" }
wheels = [
{ 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" },
{ 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" },
]
[[package]]
@@ -2247,11 +2213,11 @@ wheels = [
[[package]]
name = "python-multipart"
version = "0.0.26"
version = "0.0.22"
source = { registry = "https://pypi.org/simple" }
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" }
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" }
wheels = [
{ 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" },
{ 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" },
]
[[package]]