feat(kotlin-sdk): bind the ordered wallet bring-up (startWalletSubsystems) over JNI - #4658
Conversation
…tems) over JNI Android could not call platform_wallet_manager_start_wallet_subsystems at all — the C export and the Swift binding existed, but no JNI export and no Kotlin surface — so every Android consumer (dash-wallet, the example app) starts the L1 scan before the DIP-15 receival accounts exist and depends on the after-the-fact rescan, whose in-session rewind loses a race against the filter pipeline's forward-only synced-height advance (see MO-1012, 2026-09-03 instrumented restore). The JNI wrapper returns the outcome as a fixed 57-byte big-endian blob; WalletStartupOutcome.decode is the Kotlin half of that contract and WalletStartupTest pins it, along with the status-helper semantics (discoveryWorthRetrying / identityIsSettled) mirrored from the Swift binding. PlatformWalletManager.startWalletSubsystems follows the drain's per-call key-material contract: resolver and signer built for the call, closed when it returns. Call it once per wallet load, immediately before startSpv. (cherry picked from commit 783b58a)
|
Warning Review limit reachedNext included review available in 31 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (5)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
✅ Final review complete — no blockers (commit 1fe99cb) · triage: normal · Phase 2 only (queue backlog) |
thepastaclaw
left a comment
There was a problem hiding this comment.
Final validation — Phase 2 only (queue backlog)
The JNI bridge and Kotlin outcome decoding are consistent, and the native startup failure path is handled as intended. One resource-lifecycle defect remains: if signer construction fails after the resolver is created, the resolver's native handle is never closed, so repeated wallet-start attempts can leak handles.
🟡 1 suggestion(s)
Review provenance
Source: reviewer 1: gpt-6-astra (agent: phase2-reviewer, role: general); reviewer 2: gpt-6-astra (agent: phase2-reviewer, role: ffi-engineer); reviewer 3: gpt-6-astra (agent: phase2-reviewer, role: rust-quality); reviewer 4: gpt-6-astra (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6-astra (agent: astra-verifier, role: final-verifier)
- Triage:
normalbygpt-6-astra(effort low) — This is a substantial but well-contained Kotlin/JNI API addition coordinating wallet startup, native error/status mapping, and key-related signer resolution, yet it does not itself change consensus, funds movement, cryptographic primitives, or storage migrations. - Phase 1 reviewers: not run (skipped for throughput: 16 PRs queued, above the 10 limit)
- Fresh verifier:
gpt-6-astra— final-verifier; agentastra-verifier - Phase 2 reviewers:
gpt-6-astra— general (completed, effort high); agentphase2-reviewer,gpt-6-astra— ffi-engineer (completed, effort high); agentphase2-reviewer,gpt-6-astra— rust-quality (completed, effort high); agentphase2-reviewer,gpt-6-astra— security-auditor (completed, effort high); agentphase2-reviewer
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/PlatformWalletManager.kt`:
- [SUGGESTION] packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/PlatformWalletManager.kt:2390-2392: Close the resolver when signer construction fails
`startupResolver` is constructed before `KeystoreSigner`, but the `try/finally` begins only after both constructors return. If `KeystoreSigner(...)` throws during native signer creation or dependent keystore/database initialization, control exits without calling `startupResolver.close()`, leaving its native handle allocated. Because this method can be retried during wallet loading, repeated signer-construction failures can accumulate leaked resolver handles. Place signer construction inside a nested cleanup scope or use nullable locals so every successfully-created resource is closed.
| val startupResolver = MnemonicResolverAndPersister(walletStorage) | ||
| val startupSigner = | ||
| KeystoreSigner(walletStorage, network, biometricGate, database.platformAddressDao()) |
There was a problem hiding this comment.
🟡 Suggestion: Close the resolver when signer construction fails
startupResolver is constructed before KeystoreSigner, but the try/finally begins only after both constructors return. If KeystoreSigner(...) throws during native signer creation or dependent keystore/database initialization, control exits without calling startupResolver.close(), leaving its native handle allocated. Because this method can be retried during wallet loading, repeated signer-construction failures can accumulate leaked resolver handles. Place signer construction inside a nested cleanup scope or use nullable locals so every successfully-created resource is closed.
| val startupResolver = MnemonicResolverAndPersister(walletStorage) | |
| val startupSigner = | |
| KeystoreSigner(walletStorage, network, biometricGate, database.platformAddressDao()) | |
| var startupResolver: MnemonicResolverAndPersister? = null | |
| var startupSigner: KeystoreSigner? = null | |
| try { | |
| startupResolver = MnemonicResolverAndPersister(walletStorage) | |
| startupSigner = KeystoreSigner(walletStorage, network, biometricGate, database.platformAddressDao()) | |
| val blob = mapNativeErrors { | |
| WalletManagerNative.startWalletSubsystems( | |
| managerHandle, | |
| walletId, | |
| startupResolver!!.nativeHandle, | |
| startupSigner!!.nativeHandle, | |
| budgetSecs, | |
| gapLimit, | |
| ) | |
| } | |
| WalletStartupOutcome.decode(blob) | |
| } finally { | |
| runCatching { startupSigner?.close() } | |
| runCatching { startupResolver?.close() } | |
| } |
source: gpt-6-astra (phase2-reviewer: general, rust-quality)
Issue being fixed
platform-walletalready owns the DashPay startup ordering (#4359, gated on seed ownership by #4368) andSwift already drives it. Kotlin has no binding, so Android clients cannot run the ordered bring-up at all.
The consequence on a restored wallet is missed money. DashPay contact accounts only start watching their
addresses once they are registered. If Core SPV starts first, the scan passes the heights that fund those
addresses while nothing is watching them, and the coins are never seen. A later rescan does not help: the
addresses were not in the filter query when those blocks were tested.
Measured on a restored CoinJoin-heavy testnet wallet before this binding existed: 12 receiving-chain coins
worth 0.0836 DASH simply absent, plus a class of contact payments whose records were never built.
What was done
rs-unified-sdk-jni/src/wallet_manager.rs: JNI entry point that resolves the mnemonic and signer, callsPlatformWalletManager::start_wallet_subsystems, and returns a packed outcome blob.WalletStartup.kt:WalletStartupStatus(the status codes the Rust side reports) andWalletStartupOutcome(status, discovery count, whether DashPay sync ran, drained and pending counts,elapsed ms).
PlatformWalletManager.startWalletSubsystems(walletId, budgetSecs = 0, gapLimit = 0): suspending, runs onthe IO dispatcher behind the teardown gate, validates its arguments, and maps native errors. The budget is
never unbounded because this call gates Core SPV.
WalletManagerNative: the native declaration.WalletStartupTest: 6 tests over the outcome decoding and the status mapping.No behaviour change for any existing caller: this only adds a binding to an API that already exists.
How this was tested
kotlin-sdk :sdk:testDebugUnitTest— 435 tests, 0 failures (6 of them the new class).cargo check -p rs-unified-sdk-jniclean;cargo fmt --checkandclippyclean for the touched file.startSpv,over eight successive test builds on two large restored testnet wallets:
zero coins missing, 14 contact accounts registered before the scan began.
sessions before the ordered bring-up is now present in every run.