android: ask for the wallet passphrase, and stop printing RPC errors #150
Loading…
Reference in a new issue
No description provided.
Delete branch "onionwire/t_9ef8561c-wallet-manager-ask-for-the-passphrase-an"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Stacked on #149 (
onionwire/t_67879b98-wallet-actually-open-the-wallet-file-and, still open at the time of writing:git rev-list --count origin/main..origin/onionwire/t_67879b98-...= 1).baseis that branch, notmain— the passphrase-taking open does not exist onmain.The bug
Tapping a saved wallet looked like it did nothing. The child was restarted onto the file with no wallet open, so every read answered the daemon's
-13 No wallet file, and the screen printed that string at the user. The parent card (#149) makes the core actually open the file and seal the passphrase; this is the phone side for a wallet that predates it, which has no stored passphrase and needs one typed once.Correction in
b4f8bb8(second pass)An earlier revision of this PR claimed the failure filter ran "at every site that renders a wallet failure in both wallet screens". That was wrong, and the reviewer reproduced it. The filter sat on
WalletChildCard's sticky error line, but the same field of the same record is rendered 185 lines above:WalletScreen.kt'stoInput()copied the SDK'slastErrorverbatim,Text(walletChildLine(input))printed it (WalletChildStatusText.kt:293-320appends it;shortSendReasononly cuts at": "), andwalletChildStatusDescriptionput it in the screen-reader sentence too. In exactly the state this card exists for — a live child with no wallet file open — the user was shownNo wallet file.Fixed at the producer instead of the render sites:
WalletChildStatus.toInput()now runslastErrorthroughwalletFailureText— the one seam where the SDK record becomes the Kotlin model, upstream ofWalletChildStatusText.kt(outside this card's file list). It is idempotent on its own replacement texts, so the error card's second pass is stable.What changed
ui/WalletManagerScreen.ktWalletPassphraseCardis shown when the child reportsneeds_passphraseand no wallet is open. Masked field (PasswordVisualTransformation), no copy action at all — same rule and same reason asSeedBackupCard. Submit calls the parent'sWire::open_wallet_with_passphraseon the fileactive_wallet_filename()already reports, so there is no second idea of which wallet is open. It is wiped on success and inonDispose, and the card is notrememberSaveable.FLAG_SECUREis set while the composition is up and cleared when it goes. Disclosure: no screen in this app set that flag before this change (grep -rn FLAG_SECURE android/was empty on the parent tip), so "like the other key-bearing screens" did not hold as written. I added it here because this screen renders a spend key and asks for the passphrase that opens a wallet file; the same gap exists on the wallet screen's seed card, and a follow-up card should decide it app-wide rather than me doing it here.walletFailureText(raw)— the display filter. The wallet client keeps onlyerror.messageout of the daemon's JSON, soNo wallet fileis what actually arrives; a failure that never got that far can still arrive as the envelope. Both are replaced with a plain sentence. A bare-13is matched with lookarounds (-135is not a code). The core's own refusals (no saved wallet in onionwire-9,node stopped: …,wallet file X is not open — the RPC refused its passphrase (…)) pass through verbatim — they are written for a person and carry the reason. Over-matching only downgrades to a plainer sentence, so the filter fails closed.listError,actionError,keysErrorand the passphrase error here, and toerror/daemonErrorin the wallet screen's action paths.ui/WalletScreen.kttoInput()filterslastError. This is the fix for the leak above: the status line and its screen-reader sentence are both built from this oneWalletChildInput, so nothing the SDK reports can reach either unfiltered. The sticky error line's existingwalletFailureText(input?.lastError)stays as the second pass and is stable under it.error,daemonError, the wallet-child error flow).walletChildOpenHint): it says whether a wallet is open and where to fix it — "Open it on the Saved wallets screen, where the prompt is" when it needs a passphrase, otherwise "open a saved wallet… or create or import one here". The twowalletOpen/needsPassphraseflags are read off the sameWalletChildStatusthe status line is drawn from; no new state.WireViewModel.ktopenWalletWithPassphrase(filename, passphrase). One liverefreshWalletChild()probe on success — a one-shot read, not a second poller; the wallet screen still owns the only loop.readWalletChild,startWalletChild,stopWalletChild) now stores the filtered text.Tests
WalletManagerTest(JVM): 31 new tests, 46 total in the file. Covers the new decision —needs_passphrase && !walletOpen→ prompt, open wallet → no prompt (an open wallet wins over a stale flag) — plus the submit gate and its two distinct reasons, the failure-text filter, the open hint, and — new in this pass, the point of the correction — the render path itself: the tests build the realWalletChildStatusrecord withlastError = "No wallet file", run it throughtoInput()and assert onwalletChildLine(...)/walletChildStatusDescription(...), not onwalletFailureTextin isolation. Assertions are on pure functions, not on composition.Mutation-checked: reverting
lastError = walletFailureText(lastError)tolastError = lastErrorfails exactly three of them (the_status_line_never_renders_the_daemons_own_words,a_raw_rpc_envelope_on_the_status_line_is_replaced_too,filtering_the_sdk_record_twice_changes_nothing) and209 tests completed, 3 failed. With the fix in place the same probe the reviewer ran prints:Full suite on this machine:
No instrumentation or device test is claimed: there is no emulator or device on this machine. A wallet created before #149 opening with a typed passphrase, and the balance / address / seed / keys working afterwards, is NOT verified on a device by this PR. That is the acceptance criterion a reviewer with a device has to run: install, create a wallet (or take one from before), restart, tap it in Saved wallets, type the passphrase, then check Recovery phrase and Private keys on the same screen.
Toolchain check on this machine (not a substitute for CI):
Scope
git diff --statagainsted29131(the parent tip this is based on) — only the four allowed files:No
src/**, nocrates/**, no manifest, no*.gradle.kts, no new permission, no build-file change, no new dependency. The UniFFI bindings were regenerated locally only to check the surface:Wire.openWalletWithPassphrase(filename, passphrase)andWalletChildStatus.needsPassphrase/.walletOpenare what the parent exposes, and they are what this uses.No release, no tag.
Second pass: the status-line leak (
b4f8bb8)Fixed the defect the review reproduced. The filter now runs at the producer,
WalletChildStatus.toInput()inWalletScreen.kt— the one seam where the SDK record becomes the Kotlin model, upstream ofWalletChildStatusText.kt, which is outside this card's file list.walletFailureTextis idempotent on its own replacement texts, so the sticky error card's existing pass is stable.Runtime probe, the same one the review ran, on the fixed build:
Six new JVM tests in
WalletManagerTestbuild the realWalletChildStatusrecord and assert onwalletChildLine(...)/walletChildStatusDescription(...)— the render path, not the filter in isolation. Mutation check: revertinglastError = walletFailureText(lastError)tolastError = lastErrorfails three of them (209 tests completed, 3 failed); with the fix the full suite is209 tests, 0 failures, 0 errors(XML 2026-09-22T18:07:33Z, run at 18:07:44Z).The PR body's earlier claim that the filter ran at every render site in both wallet screens was wrong and has been corrected.
CI:
android-cirun 585 onb4f8bb8, success —HEAD is now at b4f8bb8,BUILD SUCCESSFUL in 10m 1s,dist/onionwire-0.1.0-android-arm64-v8a.apk34,276,918 bytes sha25608b07f16ff059860a5b4372989f1d6dbb79e2bb2a70d3daff990a751c399017e,apksignerverify OK.