Context
Rust panics that escape app-facing UniFFI/runtime/callback boundaries can crash the Android JVM with no means of avoiding these crashes. PR #97 addressed the immediate AccountInfoError blocking-task path from #96, but the same safety goal needs to apply across the codebase, not just account info.
This issue tracks hardening all app-facing error boundaries so panic/join/lock/data failures are converted into typed errors, watcher events, or safe fallbacks instead of unwinding through Android/iOS bindings.
Scope
FFI async task joins
Replace rt.spawn(...).await.unwrap() / .unwrap()? in exported functions with typed join-error handling:
src/lib.rs: decode
src/lib.rs: get_lnurl_invoice
src/lib.rs: lnurl_auth
src/lib.rs: check_sweepable_balances
src/lib.rs: scan_legacy_rn_native_segwit_recovery_funds
src/lib.rs: prepare_legacy_rn_native_segwit_recovery_sweep
src/lib.rs: prepare_sweep_transaction
src/lib.rs: broadcast_sweep_transaction
src/lib.rs: blocktank_remove_all_orders
src/lib.rs: blocktank_remove_all_cjit_entries
src/lib.rs: blocktank_wipe_all
src/lib.rs: wipe_all_databases
Prefer a small shared helper per error shape, rather than repeating ad-hoc unwrap_or_else blocks.
Blocking task panic boundaries
The new run_account_info_blocking pattern should be generalized to other onchain blocking tasks:
src/modules/onchain/implementation.rs: legacy RN recovery scan/sweep blocking work
src/modules/onchain/implementation.rs: sweep balance sync
src/modules/onchain/implementation.rs: sweep transaction sync/client setup
src/modules/onchain/implementation.rs: sweep broadcast pre-sync and broadcast
src/modules/onchain/implementation.rs: broadcast_raw_tx
These should catch panic payloads inside the blocking closure and map them to SweepError / BroadcastError instead of only mapping JoinError.
Watcher thread and event callbacks
Harden the long-lived watcher path:
- Wrap the
std::thread::Builder::spawn watcher body in catch_unwind.
- Make post-init watcher panics visible via logging and/or
WatcherEvent::Error where possible.
- Route every
listener.on_event(...) call through a safe callback wrapper.
- Replace
expect("blockchain set above") with a typed AccountInfoError.
Native Trezor callbacks
Native callback calls should not be able to unwind through Rust/app boundaries:
TransportCallback adapter methods in src/modules/trezor/implementation.rs
UiCallbackAdapter methods in src/modules/trezor/implementation.rs
- Direct mobile callback calls such as
enumerate_devices, close_device, and save_thp_credential
Map callback panics/failures into TrezorError or the callback result error shape used by trezor-connect-rs.
Locks and data parsing
Convert non-test panic sources into typed errors:
src/lib.rs: poisoned DB mutex unwraps
src/lib.rs: init_db global state unwraps where practical
src/lib.rs: wipe_all_databases lock unwrap
src/modules/blocktank/db.rs: state.parse().unwrap() / state2.parse().unwrap() should become database conversion errors
Acceptance Criteria
- No app-facing exported function panics because a spawned Tokio task panicked or was cancelled.
- Onchain blocking closures catch Rust panics and return typed errors with useful messages.
- Watcher thread panics and watcher/native callback panics cannot crash the Android JVM.
- DB lock poisoning and invalid stored enum values return typed errors instead of panicking.
- Regression tests cover at least the shared async join helper and each new blocking panic helper.
cargo fmt --check, focused regression tests, and cargo test --lib -- --skip modules::blocktank pass.
- If public/generated bindings change, bump the crate/package version, regenerate mobile bindings, and create a new release.
- AI agents rules should ensure future changes are reviewed to avoid panics propagating to android bindings.
Context
Rust panics that escape app-facing UniFFI/runtime/callback boundaries can crash the Android JVM with no means of avoiding these crashes. PR #97 addressed the immediate
AccountInfoErrorblocking-task path from #96, but the same safety goal needs to apply across the codebase, not just account info.This issue tracks hardening all app-facing error boundaries so panic/join/lock/data failures are converted into typed errors, watcher events, or safe fallbacks instead of unwinding through Android/iOS bindings.
Scope
FFI async task joins
Replace
rt.spawn(...).await.unwrap()/.unwrap()?in exported functions with typed join-error handling:src/lib.rs:decodesrc/lib.rs:get_lnurl_invoicesrc/lib.rs:lnurl_authsrc/lib.rs:check_sweepable_balancessrc/lib.rs:scan_legacy_rn_native_segwit_recovery_fundssrc/lib.rs:prepare_legacy_rn_native_segwit_recovery_sweepsrc/lib.rs:prepare_sweep_transactionsrc/lib.rs:broadcast_sweep_transactionsrc/lib.rs:blocktank_remove_all_orderssrc/lib.rs:blocktank_remove_all_cjit_entriessrc/lib.rs:blocktank_wipe_allsrc/lib.rs:wipe_all_databasesPrefer a small shared helper per error shape, rather than repeating ad-hoc
unwrap_or_elseblocks.Blocking task panic boundaries
The new
run_account_info_blockingpattern should be generalized to other onchain blocking tasks:src/modules/onchain/implementation.rs: legacy RN recovery scan/sweep blocking worksrc/modules/onchain/implementation.rs: sweep balance syncsrc/modules/onchain/implementation.rs: sweep transaction sync/client setupsrc/modules/onchain/implementation.rs: sweep broadcast pre-sync and broadcastsrc/modules/onchain/implementation.rs:broadcast_raw_txThese should catch panic payloads inside the blocking closure and map them to
SweepError/BroadcastErrorinstead of only mappingJoinError.Watcher thread and event callbacks
Harden the long-lived watcher path:
std::thread::Builder::spawnwatcher body incatch_unwind.WatcherEvent::Errorwhere possible.listener.on_event(...)call through a safe callback wrapper.expect("blockchain set above")with a typedAccountInfoError.Native Trezor callbacks
Native callback calls should not be able to unwind through Rust/app boundaries:
TransportCallbackadapter methods insrc/modules/trezor/implementation.rsUiCallbackAdaptermethods insrc/modules/trezor/implementation.rsenumerate_devices,close_device, andsave_thp_credentialMap callback panics/failures into
TrezorErroror the callback result error shape used bytrezor-connect-rs.Locks and data parsing
Convert non-test panic sources into typed errors:
src/lib.rs: poisonedDBmutex unwrapssrc/lib.rs:init_dbglobal state unwraps where practicalsrc/lib.rs:wipe_all_databaseslock unwrapsrc/modules/blocktank/db.rs:state.parse().unwrap()/state2.parse().unwrap()should become database conversion errorsAcceptance Criteria
cargo fmt --check, focused regression tests, andcargo test --lib -- --skip modules::blocktankpass.