Commit graph

51 commits

Author SHA1 Message Date
fusion44
6e3238a5a7
feat(api): implement GET /system/health readiness endpoint (#145)
Make /system/health unauthenticated, compute real readiness from the
shared startup state via build_health_info, and return 503 (body still a
SystemHealthInfo) when a subsystem is not ready. Drop the now-dead
per-backend get_system_health delegation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 18:41:18 +02:00
fusion44
0e77b6a924
feat(api): add pure build_health_info + rename subsystem field to healthy
build_health_info maps the startup state to SystemHealthInfo (platform
independent, pure, synchronous). Rename SubSystemHealthInfo.health ->
healthy to match the top level, and switch the two health models from
the misused Query() to Field().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 18:41:18 +02:00
fusion44
6b29239cc0
refactor(api): hoist api_startup_status into a shared leaf module
Move the in-process startup-state singleton out of app.main into
app.api.startup_status so app.system can read it without a circular
import. No behavior change - main mutates the same shared object.

Also lands the design spec and implementation plan for the
system/health endpoint work (#145).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 18:41:18 +02:00
fusion44
6b4f3abc20
feat(api): normalize all error responses to the ErrorMessage shape
Route HTTPException (app- and framework-raised), RequestValidationError
and uncaught exceptions through build_error_response so every error body
is {detail, error_code, report?, trace?}. Fixes the bare-string body that
string HTTPException details produced, and registers the handler on the
Starlette base HTTPException so framework 404/405/415 also carry the full
envelope.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 18:41:18 +02:00
fusion44
71e7cb5632
feat(api): add gated build_error_response for consistent errors
Introduces the flat ErrorMessage envelope builder with BAPI_SEND_REPORT /
BAPI_SEND_TRACE gating (report/trace off by default), plus the design
spec and implementation plan for the consistent-error-responses work.

Fixes #148
Fixes #123

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 18:41:18 +02:00
fusion44
a921686b4a
fix(lightning): clear error for undecodable/wrong-network invoices
Decoding a malformed or wrong-network invoice (e.g. a regtest lnbcrt...
invoice on a mainnet node) made each backend fail with a cryptic,
leaked error: LND with the Go 'strconv.ParseUint: parsing "rt500":
invalid syntax' surfaced as a 500, and CLN with 'Invalid bolt11: ...'.

Add a shared helper raise_for_pay_req_decode_error() in lightning/utils
that recognizes the decode-failure signatures of both backends and
raises one clear 400 pointing at the likely cause (malformed or wrong
network). Wire LND (lnd_grpc), CLN gRPC and CLN JSON-RPC through it so
they behave identically; genuine backend errors still return 500.

Fixes #225

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 18:41:18 +02:00
fusion44
340d862102
chore(api): remove SSE transport and unused SSE stream endpoints
Replaced by the /ws WebSocket channel. Deletes the SSE connection manager,
the vendored sse_starlette, and the unused /bitcoin/block-sub and
/system/hardware-info-sub streaming endpoints. Also removes the
test_block_sub_error.py regression test, which exclusively covered the
now-deleted handle_block_sub per-request generator (handle_block_sub_redis,
which broadcasts over /ws, is retained and unaffected).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 18:41:18 +02:00
fusion44
5089925c88
feat(api): serve realtime updates over /ws WebSocket
Replace the /sse/subscribe endpoint with a /ws WebSocket endpoint backed
by ws_mgr, and port the warmup helper (_send_sse_event -> _send_ws_event,
SSE -> Event, broadcast_sse_msg -> broadcast_msg). Also fixes two tests
left over from the prior SSE->Event/broadcast_msg rename that still
referenced the old names and were failing collection/execution.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 18:41:18 +02:00
fusion44
e53d70d390
fix(api): harden WebSocket auth handshake against bad/disconnecting clients
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 18:41:18 +02:00
fusion44
375c5a9469
feat(api): add WebSocketManager with first-message auth handshake
First commit of the SSE -> WebSocket migration (#252): the WebSocket
connection manager and its {type:auth,token} JWT handshake. Also lands
the migration's design spec and implementation plan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 18:41:18 +02:00
fusion44
8646d966dc
fix(lnd): return a retryable status while the RPC server starts up
While LND is up but its RPC server is 'in the process of starting up, but
not yet ready to accept calls', every LND method returned a bare 500 -
the caller (e.g. the WebUI polling list-all-tx) got a generic server
error with no useful signal.

_check_if_locked only mapped the 'wallet locked' case; generalize it to
_check_transient_ln_error and also map the startup message to
425 TOO_EARLY (matching how bitcoind warmup is reported), with a clear
'try again shortly' detail. This covers all LND methods that share the
same error-handling path.

Fixes #247

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 10:02:20 +02:00
fusion44
b8677cbbf1
fix(system): tolerate empty hardware Redis values on VM setups
Some checks failed
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
On VM installs (e.g. Proxmox) the RaspiBlitz monitor scripts don't
populate keys like system_cpu_load / system_ram_mb / system_temp_celsius,
so redis returns empty strings. get_hardware_info did int('')/float('')
and raised ValueError, which killed the _handle_gather_hardware_info
background task ('Task exception was never retrieved') - hardware SSE
updates then stopped until restart.

- parse the hardware Redis values via _safe_int/_safe_float, returning
  zero values instead of crashing (and guard the vram-percent division
  by zero)
- wrap the gatherer loop body in try/except so a single failure can no
  longer kill the task

Fixes #271

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 14:45:21 +02:00
fusion44
6444769dc4
fix(bitcoind): guard block-sub handlers against errored RPC replies
Same #277 root cause in the block-subscription path: Bitcoin Core 28+
uses strict JSON-RPC 2.0, so an errored getblock/getbestblockhash reply
has no 'result' key. handle_block_sub and handle_block_sub_redis accessed
r['result'] unguarded, so a transient RPC error would raise
KeyError: 'result' and kill the block stream / block-update task.

Skip and log the block when the reply has no result instead of crashing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 14:33:59 +02:00
fusion44
54fc16e627
test(bitcoind): lock JSON-RPC 2.0 success handling (no error key)
Bitcoin Core 28+ replies to a JSON-RPC 2.0 request with a strict 2.0
response: a success has a 'result' key but no 'error' key. The v1.11 code
did 'if result["error"] is not None' and crashed with KeyError: 'error'.
The getters are already guarded on dev with 'if "error" in result and
...'; add regression tests for get_network_info / get_blockchain_info so
that guard can't be dropped again (verified they fail against the old
unguarded form).

Fixes #277

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 13:47:11 +02:00
fusion44
3980ce0c6b
fix(bitcoind): handle JSON-RPC errors returned with HTTP 200
Bitcoin Core returns the -28 warmup error ('Loading block index',
'Verifying blocks', 'Starting network threads') as an HTTP 200 response
with a JSON-RPC error body. _process_response only classified errors on
the non-200 path and passed a 200 body through unchanged, so the result
had an 'error' key but no 'status' key. Callers doing
'raise HTTPException(result["status"], ...)' then crashed with
KeyError: 'status' instead of HTTPException(425), so the startup
warmup-retry loop in initialize_bitcoin_repo never engaged and the API
(and login) failed while bitcoind was still warming up.

Same root cause, different -28 warmup message.

Fixes #287
Fixes #285

Normalize any JSON-RPC error to an {error, status} dict regardless of the
HTTP status, mapping the warmup messages to 425 TOO_EARLY as before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 13:30:04 +02:00
fusion44
63b136d5ca
fix(apps): report electrs installed/configured/online in advanced status
_do_electrs_status_advanced only handled the negative cases: each guard
set installed/configured/status to its false-y value and returned early.
On the happy path (electrs installed, configured and running) it fell
through, populated the ports and sync details, but never set
s.installed/s.configured/s.status - so they kept their AppStatus
defaults (False/False/offline).

The /apps/status_advanced/electrs endpoint therefore reported electrs as
not installed and offline while simultaneously returning its ports and
sync details, disagreeing with the app_state_update_message status
(fusion44/blitz_api#286). Set the positive values when each guard passes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 13:07:52 +02:00
fusion44
a9bbc9a27c
fix(api): release Redis connections in channel listeners/notifiers
Some checks failed
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
BaseChannelListener.listen() only called pubsub.unsubscribe() in its
finally block: the pubsub connection and the listener's own Redis client
were never released, leaking a connection for every install/uninstall
(_watcher) and every recreation of the app-status listener. The Celery
task notifiers were likewise never closed, and used the deprecated
Redis.close() instead of aclose().

- add aclose() to BaseChannelNotifier and BaseChannelListener
- listen() now closes the pubsub and its Redis client in finally
- close the notifiers in the app_manage / app_status_update tasks and
  switch to aclose()
- back off in the app-status watch loop so a Redis outage no longer
  spins, and drop a leftover debug print

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 22:40:14 +02:00
fusion44
868457ffa5
fix(api): replace deprecated asyncio.get_event_loop()
get_event_loop() is deprecated on Python 3.11+ when there is no running
loop and is slated to change behaviour further.

- SSEManager.setup() ran at import time (app.api.utils) via
  get_event_loop(); this only worked because uvicorn imports the app
  inside its loop and would break when imported without a running loop
  (e.g. a Celery worker). Start the broadcast consumer lazily from
  within a running loop instead.
- everywhere else the pattern was get_event_loop().create_task(x)
  inside a coroutine; replace with asyncio.create_task(x).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 22:34:44 +02:00
fusion44
df62098cd1
refactor(lightning): migrate SendCoinsInput validator to Pydantic v2
Replace the deprecated Pydantic v1 @validator('amount', pre=True,
always=True) with a v2 @model_validator(mode='after'). The model
validator always runs and can see both amount and send_all, preserving
the cross-field rule (and the always=True semantics that reject the
empty/default case). Removes the last Pydantic v1 deprecation warning.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 22:22:05 +02:00
fusion44
cf423f52c1
fix(system): stop native_python impl swallowing errors into None
@logger.catch defaults to reraise=False, so change_password and
get_debug_logs_raw - which raise NotImplementedError - silently
returned None. The service layer's 'except NotImplementedError -> 501'
never fired, yielding '200 null' or a response-model 500 instead.
login likewise turned unexpected errors into a None result.

Drop the pointless decorator from the two methods that only raise, and
let login reraise so failures surface.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 21:44:25 +02:00
fusion44
c4afca50e9
fix(system): stop leaking passwords via change-password endpoint
The change-password endpoint accepted old_password/new_password as bare
str parameters, i.e. query parameters, so the passwords ended up in
access logs, proxy logs and browser history. Accept them in a
ChangePasswordInput request body instead.

Also mark the RaspiBlitz blitz.passwords.sh check/set invocations
sensitive=True so the plaintext passwords are not written to the debug
log, and guard against a missing password type (was an AttributeError
-> 500).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 21:42:25 +02:00
fusion44
9912ff0bfa
fix(apps): honour keep_data on uninstall and fix install log name
- uninstall_app dropped input.keep_data; _manage_app always ran the
  bonus script with a bare 'off', so the RaspiBlitz script fell back to
  an interactive whiptail prompt that hangs the non-interactive API.
  Thread keep_data through and pass the explicit
  --keep-data/--delete-data flag the scripts expect.
- install.{app_id}.log rendered as install.AppId.MEMPOOL.log because
  str-enum formatting includes the class name on Python 3.11+; use
  app_id.value here and in the CLN-incompatibility message.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 20:58:35 +02:00
fusion44
d5a568da3e
fix(lightning): include payments in CLN list_all_tx
Two bugs in the CLN JSON-RPC list_all_tx:
- 'if pay is not Payment' compared each item to the Payment class rather
  than its type, so it was always true and every payment was skipped -
  payments never showed up in the transaction list.
- the successful_only filter appended the item inside the match branch
  and then again unconditionally, so it never filtered anything.

Use isinstance for the type check and skip non-matching items when
successful_only is set. Also guard the source lists against None so a
failed sub-query no longer crashes the loop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 20:45:21 +02:00
fusion44
dccaa17a8e
fix(auth): correct JWT expiry unit and use standard exp claim
sign_jwt added JWT_EXPIRY_TIME (seconds) to a milliseconds epoch and
stored it in a custom 'expires' claim, while register_cookie_updater
slept JWT_EXPIRY_TIME as seconds. With the code default (300) tokens
effectively expired almost immediately; with the sampled 3600000 the
cookie-refresh loop slept ~41 days, so the local .cookie held an
expired token nearly always. The custom claim also meant PyJWT never
validated expiry itself.

- issue standard 'iat'/'exp' claims (seconds) and let PyJWT validate,
  requiring 'exp' on decode
- derive the cookie refresh interval from the same unit, guarded
  against tiny/negative values
- default BAPI_JWT_EXPIRY_TIME to 3600s and fix .env_sample (was
  3600000 'milliseconds')

Existing tokens and the local .cookie are invalidated by this change;
clients re-login and the cookie regenerates at startup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 20:35:34 +02:00
fusion44
5aa79acb10
fix(apps): don't release another task's app-manage lock
app_manage_task_impl released APP_MANAGE_LOCK_KEY and broadcast a
FINISHED message from its finally block unconditionally, including on
the early-return path where acquire_lock reported the lock as already
held by a running install. A duplicate install/uninstall request would
therefore delete the running task's lock (allowing concurrent
management of the same app) and send FINISHED, which stopped that
task's AppManageListener before it was done.

Track whether this task actually acquired the lock and only
release + finish when it did.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 20:01:54 +02:00
fusion44
ff54f93397
fix(api): catch subprocess timeouts in exec_bash_command
'from redis.asyncio import ... TimeoutError' shadowed the builtin
TimeoutError. redis's TimeoutError is a RedisError subclass, not a
builtin subclass, so the 'except TimeoutError' guarding
asyncio.wait_for never matched: timed-out commands fell through to the
generic handler, the child process was never terminated (leak), and
the caller got a misleading 'unable to execute' error instead of a
timeout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 19:59:57 +02:00
fusion44
4355c8eabd
fix(lightning): prevent shell injection in CLN local calls
Two authenticated code paths interpolated user-controlled input into a
shell command:

- decode_pay_request passed the bolt11 string into _make_local_call,
  which ran it via create_subprocess_shell; a crafted /lightning/
  decode-pay-req request could execute arbitrary commands. Switch
  _make_local_call to create_subprocess_exec with a discrete argv list.
- blitz_cln_unlock interpolated the wallet password into a
  cl.hsmtool.sh invocation run through a shell, and logged it in the
  clear. shlex.quote the interpolated values and mark the call
  sensitive=True.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 19:58:41 +02:00
fusion44
49d5fcb9bf
fix: send bitcoin-only warmup app status as app_state_update_message
The WebUI only listens for the app_state_update_message SSE event to
populate the Apps tab. In bitcoin-only mode the warmup data was sent
under installed_app_status, which no client listens to, so the Apps
tab was stuck on the loading screen whenever the app status cache was
warm. Installing LND made it work again because the lightning warmup
path already used the correct event (raspiblitz#3608, raspiblitz#5141).

Also hardens the warmup path:
- reset the warmup_running flag on errors so a single failure no
  longer starves all future SSE clients of warmup data
- convert per-source exceptions in the bitcoin-only warmup gather
  instead of discarding the whole data set
- don't fall through to the partial-data branches when the API is
  fully initialized with lightning disabled
- remove the now-unused INSTALLED_APP_STATUS event and the dead
  cached_status_raw variable

Adds regression tests plus a conftest.py providing test env defaults
so the suite runs without a developer .env file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 15:30:17 +02:00
fusion44
74059b9256
tests: add better integration tests 2026-02-03 11:06:06 +01:00
Christoph Stenglein
cf4ab3963c remove ln_info_lite 2024-10-06 14:18:01 +02:00
fusion44
62d3f3da9b
fix: code changes for pydantic 2.4 2024-03-10 07:57:38 +01:00
fusion44
adf472ac8b feat: implement Ruff as a linter; fix given errors 2023-06-25 07:17:00 +02:00
fusion44
f06d84c29f
fix: return auth token as plain text on login 2022-10-22 11:45:59 +02:00
fusion44
ba453e7bdb
refactor: switch to domain driven directory layout 2022-10-03 20:22:00 +02:00
fusion44
0ad597cd90 refactor: move login to platform specific files
refs #143
2022-08-24 20:56:40 +02:00
fusion44
304bbc8c74 fix(tests): test setup 2022-08-24 19:19:58 +02:00
fusion44
516550bd9f
chore: format source using the pre_commit command 2022-03-20 17:20:56 +01:00
Stefan Stammberger
78db9f3d71
feat: implement basic fee revenue endpoint
closes #48
2022-01-09 20:20:21 +01:00
Stefan Stammberger
782464b61d
feat: implement handling of locked wallets
* new endpoint: /lightning/unlock-wallet
* ln endpoints return HTTP_423_LOCKED status if wallet is locked
* bootup sequence defers ln listeners setup if wallet is locked

refs #34
2021-11-24 11:12:10 +01:00
Stefan Stammberger
3c7a22e417
feat: implement /lightning/new-address endpoint
closes #43
2021-11-22 21:54:42 +01:00
Stefan Stammberger
7b94aac3d8
feat: add "/system/refresh-token" endpoint
closes #41
2021-11-19 18:32:30 +01:00
Stefan Stammberger
bd6d293259
fix: tests and simplify the test code
refs# 21
2021-11-19 17:28:39 +01:00
Stefan Stammberger
ae59ef9181
feat: implement /lightning/list-all-tx endpoint
This call currently fetches all invoices, payments and transactions each
time the endpoint is hit. TODO: Find a better way to cache this.
Possibly it is a good idea to build this list once on startup and
keep it updatedwhen new transactions roll in.

refs #30
2021-11-02 19:02:08 +01:00
Stefan Stammberger
be1f1f6dde
feat: implement /lightning/list-payments endpoint
Currently implemented for LND only.

Low level list-payments endpoint in preparation to implement #30.
2021-10-31 19:33:35 +01:00
Stefan Stammberger
4a6c6e9cf4
feat: implement /lightning/list-invoices endpoint
Currently implemented for LND only.

Low level list-invoices endpoint in preparation to implement #30.
2021-10-31 19:04:39 +01:00
Stefan Stammberger
5e068f0cb1
feat: implement list-onchain-tx endpoint for LND
Low level on-chain API in preparation to implement #30.
2021-10-31 16:49:15 +01:00
Stefan Stammberger
4d5ffbc03e
refactor: Shorten the url path of get ln info lite
old:   /lightning/get-ln-info-lite
new: /lightning/get-info-lite
2021-10-05 19:50:32 +02:00
Stefan Stammberger
2d18e5a33f
refactor: Rename poorly named classes and events 2021-10-05 19:34:06 +02:00
Stefan Stammberger
a8355687d4
feat: implement send coins (on-chain) endpoint
closes #15
2021-10-03 20:55:28 +02:00
Stefan Stammberger
f7e1cf206d
test: improve ln test setup and coverage 2021-10-03 10:37:51 +02:00