Skip to content

feat(kotlin-sdk): bind the ordered wallet bring-up (startWalletSubsystems) over JNI - #4658

Open
HashEngineering wants to merge 1 commit into
dashpay:v4.2-devfrom
HashEngineering:feat/kotlin-start-wallet-subsystems-v42dev
Open

feat(kotlin-sdk): bind the ordered wallet bring-up (startWalletSubsystems) over JNI#4658
HashEngineering wants to merge 1 commit into
dashpay:v4.2-devfrom
HashEngineering:feat/kotlin-start-wallet-subsystems-v42dev

Conversation

@HashEngineering

Copy link
Copy Markdown
Collaborator

Issue being fixed

platform-wallet already owns the DashPay startup ordering (#4359, gated on seed ownership by #4368) and
Swift 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, calls
    PlatformWalletManager::start_wallet_subsystems, and returns a packed outcome blob.
  • WalletStartup.kt: WalletStartupStatus (the status codes the Rust side reports) and
    WalletStartupOutcome (status, discovery count, whether DashPay sync ran, drained and pending counts,
    elapsed ms).
  • PlatformWalletManager.startWalletSubsystems(walletId, budgetSecs = 0, gapLimit = 0): suspending, runs on
    the 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:testDebugUnitTest435 tests, 0 failures (6 of them the new class).
  • cargo check -p rs-unified-sdk-jni clean; cargo fmt --check and clippy clean for the touched file.
  • Device-verified through a downstream Android wallet build that calls this binding before startSpv,
    over eight successive test builds on two large restored testnet wallets:
    • 7,324-transaction CoinJoin wallet: balance exact to the duff against a dashj wallet of the same seed,
      zero coins missing, 14 contact accounts registered before the scan began.
    • 3,269-transaction wallet: balance exact, and a contact payment that had gone missing in 4 of 6 restore
      sessions before the ordered bring-up is now present in every run.

…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)
@coderabbitai

coderabbitai Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Next included review available in 31 minutes.

Check out review usage here.

View limit details

Limit 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.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: d6ff5a9c-2f70-48ba-ac78-caeece395606

📥 Commits

Reviewing files that changed from the base of the PR and between c5363e7 and 1fe99cb.

📒 Files selected for processing (5)
  • packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/WalletManagerNative.kt
  • packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/PlatformWalletManager.kt
  • packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/WalletStartup.kt
  • packages/kotlin-sdk/sdk/src/test/kotlin/org/dashfoundation/dashsdk/wallet/WalletStartupTest.kt
  • packages/rs-unified-sdk-jni/src/wallet_manager.rs

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added this to the v4.2.0 milestone Sep 11, 2026
@thepastaclaw

thepastaclaw commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

✅ Final review complete — no blockers (commit 1fe99cb) · triage: normal · Phase 2 only (queue backlog)

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: normal by gpt-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; agent astra-verifier
  • Phase 2 reviewers: gpt-6-astra — general (completed, effort high); agent phase2-reviewer, gpt-6-astra — ffi-engineer (completed, effort high); agent phase2-reviewer, gpt-6-astra — rust-quality (completed, effort high); agent phase2-reviewer, gpt-6-astra — security-auditor (completed, effort high); agent phase2-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.

Comment on lines +2390 to +2392
val startupResolver = MnemonicResolverAndPersister(walletStorage)
val startupSigner =
KeystoreSigner(walletStorage, network, biometricGate, database.platformAddressDao())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Suggested change
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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants