Skip to content

feat(kotlin-sdk): expose core_wallet_set_gap_limit to Kotlin (migrated-wallet address-window heal) - #4377

Open
bfoss765 wants to merge 2 commits into
v4.2-devfrom
feat/kotlin-sdk-gap-limit-jni
Open

feat(kotlin-sdk): expose core_wallet_set_gap_limit to Kotlin (migrated-wallet address-window heal)#4377
bfoss765 wants to merge 2 commits into
v4.2-devfrom
feat/kotlin-sdk-gap-limit-jni

Conversation

@bfoss765

@bfoss765 bfoss765 commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Issue being fixed or feature implemented

A wallet migrated from dashj can be silently blind to its own funds: if a same-seed client (the shipped dashj app, still run in parallel by migrated users) spends past the SDK's derived address window, the change output — and every descendant transaction — never enters the SDK's filter-scan query. The wallet reports synced with the wrong balance and missing history.

Field case (mainnet, long-lived ~150-contact wallet): a 70 DASH payment's 37.17 change went to an address one step past the watched window. The SDK saw the 70 leave and never saw the change return — confirmed balance collapsed from 70.6 to 0.6 at the spend height, and everything downstream of that change stayed invisible.

The Rust seam to fix this existed end-to-end (AddressPool::set_gap_limitManagedCoreFundsAccountCoreWallet::set_gap_limit → the core_wallet_set_gap_limit C export, in-tree since #3970) but stopped at the C boundary — no JNI trampoline, so no Kotlin host could reach it.

What was done?

~40 lines of plumbing, no engine changes:

  • rs-unified-sdk-jni: Java_…_WalletManagerNative_coreWalletSetGapLimit (account-type mapping via the existing core_account_type, same guard shape as the sibling exports).
  • kotlin-sdk: the external fun declaration and a ManagedCoreWallet.setGapLimit(accountType, accountIndex, gapLimit) wrapper under mapNativeErrors. Rust clamps to MAX_GAP_LIMIT = 1000; mark_used keeps rolling the window forward during the subsequent re-scan, so frontiers deeper than one window still recover.

The intended host usage is: widen the standard families once (BIP44/BIP32/CoinJoin), rewind the SPV filter watermark (rescanSpvFilters, already exposed), and let the scan re-match history against the widened script set. The Android wallet implements this as a one-shot per-version migration heal.

How Has This Been Tested?

  • cargo check -p rs-unified-sdk-jni clean on this branch.
  • Controlled testnet reproduction (Android host, arm64 device): restored a seed on the SDK wallet, then funded index 45 on BOTH the external and change chains (past the default gap of 30) from a second client. The wallet showed 0.01/"synced" while 0.045 sat confirmed on-chain — the field bug, reproduced. After an in-place upgrade to a build with the heal (widen to 1000 + watermark rewind through this API): full recovery — 0.045, all transactions, one-shot latch verified across restarts, and a no-regression pass on a healthy wallet (history preserved, identity intact).
  • Mainnet field verification: the affected wallet above took the heal build as an in-place upgrade and completed one full re-scan — fully synced, correct balance, the previously-invisible payment and its history now shown. The address-window blindness did not recur across restarts.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added support for configuring an account’s address gap limit.
    • Gap limits can be set per account type and account index.
    • Invalid values are rejected, and an enforced maximum helps protect wallet discovery and scanning performance.

The Rust seam existed end-to-end (AddressPool::set_gap_limit ->
ManagedCoreFundsAccount -> CoreWallet::set_gap_limit -> the
core_wallet_set_gap_limit C export, in-tree since #3970) but stopped at
the C boundary: WalletManagerNative had no trampoline, so no Kotlin host
could widen an address window. Adds the JNI export (account-type mapping
via the existing core_account_type; from-height guard mirrors the
sibling exports), the external fun, and a ManagedCoreWallet.setGapLimit
wrapper under mapNativeErrors.

Motivation: a migrated wallet whose OTHER same-seed client (dashj) kept
deriving past the SDK's watched window goes silently blind to the change
output and every descendant — the wallet reports synced with the wrong
balance. Widening the gap limit (Rust caps at 1000) and re-scanning
recovers the history; the Android app's one-shot migration heal is the
first consumer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added this to the v4.2.0 milestone Aug 11, 2026
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a1eac3f7-9843-4dc2-8a48-c2060bae0a3b

📥 Commits

Reviewing files that changed from the base of the PR and between d035604 and 69cacb6.

📒 Files selected for processing (1)
  • packages/rs-unified-sdk-jni/src/wallet_manager.rs

📝 Walkthrough

Walkthrough

The change adds an account-specific setGapLimit API to the Kotlin wallet SDK. The JNI layer validates its parameters and forwards valid requests to the native wallet FFI, which enforces the maximum gap limit.

Changes

Wallet gap-limit API

Layer / File(s) Summary
Kotlin gap-limit contract
packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/WalletManagerNative.kt, packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/ManagedCoreWallet.kt
Adds the native coreWalletSetGapLimit declaration and the public setGapLimit method for a selected account. Native errors are mapped to Kotlin errors.
JNI validation and forwarding
packages/rs-unified-sdk-jni/src/wallet_manager.rs
Documents and adds the JNI bridge. It validates account type, account index, and gap limit, then forwards valid requests to the native wallet FFI.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ManagedCoreWallet
  participant WalletManagerNative
  participant JNIWalletManager
  participant PlatformWalletFFI
  ManagedCoreWallet->>WalletManagerNative: set gap limit for account
  WalletManagerNative->>JNIWalletManager: call coreWalletSetGapLimit
  JNIWalletManager->>PlatformWalletFFI: validate and forward request
  PlatformWalletFFI-->>JNIWalletManager: return result or error
  JNIWalletManager-->>ManagedCoreWallet: map native error
Loading

Possibly related PRs

Suggested reviewers: quantumexplorer, bezibalazs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes exposing the existing gap-limit functionality to the Kotlin SDK through JNI.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/kotlin-sdk-gap-limit-jni

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

@thepastaclaw

thepastaclaw commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

✅ Final review complete — no blockers (commit 69cacb6)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/rs-unified-sdk-jni/src/wallet_manager.rs`:
- Around line 645-648: Restrict the account type handling around
core_account_type to accept only BIP44, BIP32, and CoinJoin for this per-account
bridge operation. Explicitly reject AllSpendable (account_type 3) before
forwarding the value, while preserving the existing out-of-range exception
behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 76b15fb1-440b-4305-a885-ecb512cb0c64

📥 Commits

Reviewing files that changed from the base of the PR and between 480271e and d035604.

📒 Files selected for processing (3)
  • packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/WalletManagerNative.kt
  • packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/ManagedCoreWallet.kt
  • packages/rs-unified-sdk-jni/src/wallet_manager.rs

Comment thread packages/rs-unified-sdk-jni/src/wallet_manager.rs Outdated
A gap limit belongs to one account's address pools; the aggregate (3)
has none, so refuse it at the boundary with a clear message instead of
forwarding it to a per-account FFI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.63%. Comparing base (480271e) to head (69cacb6).

Additional details and impacted files
@@             Coverage Diff              @@
##           v4.2-dev    #4377      +/-   ##
============================================
- Coverage     87.80%   87.63%   -0.18%     
============================================
  Files          2641     2670      +29     
  Lines        336510   339447    +2937     
============================================
+ Hits         295468   297465    +1997     
- Misses        41042    41982     +940     
Components Coverage Δ
dpp 88.86% <ø> (ø)
drive 86.25% <ø> (ø)
drive-abci 89.66% <ø> (ø)
sdk ∅ <ø> (∅)
dapi-client ∅ <ø> (∅)
platform-version ∅ <ø> (∅)
platform-value 92.88% <ø> (ø)
platform-wallet ∅ <ø> (∅)
drive-proof-verifier 48.02% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@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 — Codex/Sol only (Phase 2 disabled)

The Kotlin declaration, public wrapper, and JNI trampoline correctly validate signed inputs, reject the AllSpendable aggregate, map concrete account types, and forward native errors. The remaining in-scope issue is the absence of automated Android binding coverage for the new cross-language symbol and validation branches.

Source: codex-general reviewer backend gpt-5.6-sol; codex-ffi-engineer reviewer backend gpt-5.6-sol; final verifier backend gpt-5.6-sol. openclaw-agent/cliproxy/gpt-5.6-sol is orchestration-only and not reviewer evidence.

Validated zero-blocker Codex/Sol precheck evidence was promoted to final because Phase 2 (Sonnet/Opus) is temporarily disabled. This is Codex/Sol-only final validation, not Codex + Sonnet/Opus coverage.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — ffi-engineer (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet/Opus: not run (Phase 2 disabled — temporary Codex/Sol-only final)
  • Secondary pass: disabled (temporary_phase2_sonnet_disable)

🟡 1 suggestion(s)

🤖 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/rs-unified-sdk-jni/src/wallet_manager.rs`:
- [SUGGESTION] packages/rs-unified-sdk-jni/src/wallet_manager.rs:648-675: Add an instrumented binding test for the new JNI export
  No repository test invokes `coreWalletSetGapLimit`, so the Kotlin external declaration, generated JNI symbol, parameter descriptor, and the new validation branches can regress without detection. `cargo check -p rs-unified-sdk-jni` verifies only the Rust side and cannot detect a Kotlin/JNI naming or signature mismatch that would produce `UnsatisfiedLinkError` on Android. The existing Android instrumented suite already uses invalid handles to pin JNI bindings without requiring a funded wallet; add equivalent coverage that loads the native library, verifies that account type `3`, a negative account index, and a non-positive gap limit are rejected as invalid parameters, then calls a concrete account type with handle `0` and verifies that execution reaches the underlying FFI's invalid-handle path.

Comment on lines +648 to +675
let account_type = match core_account_type(account_type) {
Some(platform_wallet_ffi::CoreAccountTypeFFI::AllSpendable) | None => {
throw_sdk_exception(
env,
1,
"accountType must be a concrete account (0=BIP44, 1=BIP32, 2=CoinJoin)",
);
return;
}
Some(concrete) => concrete,
};
if account_index < 0 {
throw_sdk_exception(env, 1, "accountIndex must be non-negative");
return;
}
if gap_limit <= 0 {
throw_sdk_exception(env, 1, "gapLimit must be positive");
return;
}
let result = unsafe {
platform_wallet_ffi::core_wallet_set_gap_limit(
wallet_handle as Handle,
account_type,
account_index as u32,
gap_limit as u32,
)
};
let _ = take_pwffi_error(env, result);

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: Add an instrumented binding test for the new JNI export

No repository test invokes coreWalletSetGapLimit, so the Kotlin external declaration, generated JNI symbol, parameter descriptor, and the new validation branches can regress without detection. cargo check -p rs-unified-sdk-jni verifies only the Rust side and cannot detect a Kotlin/JNI naming or signature mismatch that would produce UnsatisfiedLinkError on Android. The existing Android instrumented suite already uses invalid handles to pin JNI bindings without requiring a funded wallet; add equivalent coverage that loads the native library, verifies that account type 3, a negative account index, and a non-positive gap limit are rejected as invalid parameters, then calls a concrete account type with handle 0 and verifies that execution reaches the underlying FFI's invalid-handle path.

source: ['codex']

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