Skip to content

feat(kotlin-sdk): expose core_wallet_next_receive_address / next_change_address (Swift parity) - #4260

Open
HashEngineering wants to merge 3 commits into
dashpay:v4.2-devfrom
HashEngineering:feat/kotlin-sdk-next-receive-address
Open

feat(kotlin-sdk): expose core_wallet_next_receive_address / next_change_address (Swift parity)#4260
HashEngineering wants to merge 3 commits into
dashpay:v4.2-devfrom
HashEngineering:feat/kotlin-sdk-next-receive-address

Conversation

@HashEngineering

@HashEngineering HashEngineering commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

What

Kotlin parity for the existing address-derivation FFI. rs-platform-wallet-ffi has shipped core_wallet_next_receive_address / core_wallet_next_change_address for some time, and iOS binds the C ABI directly (SwiftDashSDKReceiveAddressReadercoreWallet().nextReceiveAddress(accountIndex:) — the DashWallet-iOS Receive screen's source). No rs-unified-sdk-jni/Kotlin plumbing existed, so Android callers (including the SDK's own KotlinExampleApp ReceiveAddressSheet) have been reading the Room core_addresses mirror instead of asking the engine.

  • rs-unified-sdk-jni/src/wallet_manager.rs: coreWalletNextReceiveAddress / coreWalletNextChangeAddress — same guard / take_pwffi_error / core_wallet_free_address pattern as the neighboring core-wallet JNI calls.
  • WalletManagerNative.kt: the two external fun declarations.
  • ManagedCoreWallet.kt: public nextReceiveAddress(accountIndex: Int = 0) / nextChangeAddress(accountIndex: Int = 0), named to mirror the Swift surface.

Semantics (documented on the Kotlin surface)

Engine-authoritative over the in-memory used-set — "unused" means never seen on-chain. There is no issued-marker, so repeated calls return the same address until it receives funds (current-address semantics, matching iOS exactly, including the documented cold-start caveat: index 0 until SPV replay populates the used-set). A per-invoice fresh_address with an engine-side issued-marker is a separate, future ask — deliberately out of scope here.

Why now

dash-wallet's BIP70 cutover work (dashpay/dash-wallet#1531) sources Payment.refund_to from the Room pool as a stopgap; once this lands in a published AAR, that Room read swaps for coreWallet().nextReceiveAddress() (one function body, no callers change), and the same call becomes the Receive screen's post-dashj source in Phase 2 — identical to how iOS already works.

Verified

cargo check -p rs-unified-sdk-jni clean; :sdk Kotlin compile clean. Purely additive — no existing surface touched. Not yet exercised on-device (needs an AAR rebuild via build_android.sh); the FFI itself is already proven in production by iOS.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added support for retrieving the next unused receive address for a wallet account.
    • Added support for retrieving the next unused change address for a wallet account.
    • Account selection is supported, defaulting to the first account.
    • Invalid account indices and address retrieval errors are reported clearly through the SDK.

…ge_address

Kotlin parity for the existing address-derivation FFI: iOS binds
core_wallet_next_receive_address directly (SwiftDashSDKReceiveAddressReader
-> coreWallet().nextReceiveAddress), but no rs-unified-sdk-jni/Kotlin
plumbing existed - Android callers had to read the Room core_addresses
mirror instead. Adds the two JNI wrappers (same guard/take_pwffi_error/
core_wallet_free_address pattern as the neighboring core-wallet calls)
and ManagedCoreWallet.nextReceiveAddress/nextChangeAddress(accountIndex).

Engine-authoritative (in-memory used-set); same semantics and cold-start
caveat as the Swift binding, documented on the Kotlin surface: no
issued-marker, so repeated calls return the same address until it is
seen on-chain.

cargo check -p rs-unified-sdk-jni clean; :sdk:compileReleaseKotlin clean.

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

coderabbitai Bot commented Aug 1, 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: b2af4dc0-d43a-4d24-8605-ab8b4aa99578

📥 Commits

Reviewing files that changed from the base of the PR and between 70a4192 and 10db4ad.

📒 Files selected for processing (1)
  • packages/kotlin-sdk/sdk/src/test/kotlin/org/dashfoundation/dashsdk/wallet/ManagedCoreWalletAddressTest.kt

📝 Walkthrough

Walkthrough

The Kotlin SDK adds methods for the next unused receive and change addresses. The JNI layer validates inputs, calls platform-wallet FFI functions, converts native strings, maps errors, and releases native buffers.

Changes

Wallet next-address retrieval

Layer / File(s) Summary
JNI address retrieval bridge
packages/rs-unified-sdk-jni/src/wallet_manager.rs
JNI exports validate wallet handles and account indices, call the corresponding platform-wallet FFI functions, handle errors and null results, convert addresses to Java strings, and free native buffers.
Kotlin wallet address API
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/kotlin-sdk/sdk/src/test/kotlin/org/dashfoundation/dashsdk/wallet/ManagedCoreWalletAddressTest.kt
The SDK adds native declarations and managed methods for receive and change addresses. Methods reject negative account indices, default to account index 0, map native errors, and include host-JVM coverage for validation and lifecycle behavior.

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

Suggested reviewers: quantumexplorer

Sequence Diagram(s)

sequenceDiagram
  participant ManagedCoreWallet
  participant WalletManagerNative
  participant JNIWalletManager
  participant PlatformWalletFFI
  ManagedCoreWallet->>WalletManagerNative: request next receive or change address
  WalletManagerNative->>JNIWalletManager: invoke native binding
  JNIWalletManager->>PlatformWalletFFI: call address retrieval function
  PlatformWalletFFI-->>JNIWalletManager: return native address or error
  JNIWalletManager-->>WalletManagerNative: return Java string or exception
Loading
🚥 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 describes the Kotlin SDK additions for core wallet receive and change address accessors and accurately notes Swift parity.
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 unit tests (beta)
  • Create PR with unit tests

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.

@thepastaclaw

thepastaclaw commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

ℹ️ Review skipped (commit 10db4ad)
Last checked: 2026-08-01 22:00 UTC

@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

🧹 Nitpick comments (1)
packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/ManagedCoreWallet.kt (1)

58-84: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cite the iOS source file in the KDoc.

Lines 60-62 name Swift symbols but do not cite the corresponding Swift source file. Add the repository-relative Swift file reference for the receive behavior and the change behavior.

As per coding guidelines, “When porting behavior from iOS, cite the corresponding Swift source file in KDoc.”

🤖 Prompt for 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.

In
`@packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/ManagedCoreWallet.kt`
around lines 58 - 84, Update the KDoc for nextReceiveAddress and its
corresponding change-address accessor to cite the repository-relative Swift
source file that defines the iOS receive and change behaviors. Keep the existing
Swift symbol references and behavioral documentation, adding only the
source-file references required by the portability guideline.

Source: Coding guidelines

🤖 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/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/ManagedCoreWallet.kt`:
- Around line 73-88: Add automated coverage for
ManagedCoreWallet.nextReceiveAddress and nextChangeAddress using the packaged
native library; verify both return the current address on repeated calls before
the used-set changes, and assert that negative accountIndex values are rejected.

---

Nitpick comments:
In
`@packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/ManagedCoreWallet.kt`:
- Around line 58-84: Update the KDoc for nextReceiveAddress and its
corresponding change-address accessor to cite the repository-relative Swift
source file that defines the iOS receive and change behaviors. Keep the existing
Swift symbol references and behavioral documentation, adding only the
source-file references required by the portability guideline.
🪄 Autofix (Beta)

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: 95f84cd8-e4d3-4e68-9165-9b6199a7bb3b

📥 Commits

Reviewing files that changed from the base of the PR and between ed4116b and 73d9ada.

📒 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 on lines +73 to +88
fun nextReceiveAddress(accountIndex: Int = 0): String {
require(accountIndex >= 0) { "accountIndex must be non-negative, got $accountIndex" }
return WalletManagerNative.coreWalletNextReceiveAddress(handle, accountIndex)
}

/**
* The engine's next unused BIP-44 INTERNAL (change) address for
* [accountIndex], base58-encoded — the change-side twin of
* [nextReceiveAddress]; same used-set semantics and cold-start
* caveat. Builds pick change themselves (`setFunding`); this
* accessor exists for callers that must NAME a change address
* up front (e.g. `CoreTransactionBuilder.setChangeAddress`).
*/
fun nextChangeAddress(accountIndex: Int = 0): String {
require(accountIndex >= 0) { "accountIndex must be non-negative, got $accountIndex" }
return WalletManagerNative.coreWalletNextChangeAddress(handle, accountIndex)

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.

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Add automated coverage for both address APIs.

No test change accompanies these public JNI-backed methods, and the PR objective states that on-device testing is pending. Add coverage that calls both methods through the packaged native library, rejects negative account indices, and verifies repeated calls return the current address before the used-set changes.

As per coding guidelines, “Keep pull requests focused, link related issues, include tests, and complete the pull request template.”

🤖 Prompt for 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.

In
`@packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/ManagedCoreWallet.kt`
around lines 73 - 88, Add automated coverage for
ManagedCoreWallet.nextReceiveAddress and nextChangeAddress using the packaged
native library; verify both return the current address on repeated calls before
the used-set changes, and assert that negative accountIndex values are rejected.

Source: Coding guidelines

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

Preliminary review — Codex only

The JNI bridge's handle/index validation and C-string copy/free path are sound. The public Kotlin methods nevertheless bypass the SDK's required native-error mapping, allowing an internal exception type to escape; the class documentation is also stale, and the new JNI paths lack automated device coverage. Request changes for the public error-contract violation, with documentation and tests as non-blocking follow-ups.

Validated blockers were found in the Codex precheck. Sonnet is deferred until a fresh Codex revalidation clears the blocker gate.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — security-auditor (completed), gpt-5.6-sol — ffi-engineer (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet: not run (deferred by blocker gate)

🔴 1 blocking | 🟡 1 suggestion(s) | 💬 1 nitpick(s)

1 additional finding(s) omitted (not in diff).

🤖 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/ManagedCoreWallet.kt`:
- [BLOCKING] packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/ManagedCoreWallet.kt:73-88: Map native address errors into the public SDK hierarchy
  Both new public methods call an `external fun` directly. The JNI functions use `take_pwffi_error` and `throw_sdk_exception`, so failures such as a nonexistent account or invalid native handle are raised as the internal `DashSDKException`. `DashSdkError.kt` explicitly requires every public SDK entry point that calls native code to use `mapNativeErrors`; without it, callers expecting `DashSdkError.NotFound` or `DashSdkError.PlatformWallet` receive an implementation-level exception instead. Wrap both native calls at this public boundary.
- [SUGGESTION] packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/ManagedCoreWallet.kt:73-88: Add device coverage for both address APIs
  No automated test exercises either new Kotlin-to-JNI path. This repository already builds the packaged native library and runs `connectedDebugAndroidTest`, while `WalletManagerRoundTripTest` creates a default wallet offline, so coverage can verify the actual exported symbols without network access. Add an instrumented test that calls both methods, confirms repeated calls return the same unused address, and confirms negative account indices are rejected; this protects the JNI signatures, handle plumbing, and documented current-address semantics.

Comment on lines +73 to +88
fun nextReceiveAddress(accountIndex: Int = 0): String {
require(accountIndex >= 0) { "accountIndex must be non-negative, got $accountIndex" }
return WalletManagerNative.coreWalletNextReceiveAddress(handle, accountIndex)
}

/**
* The engine's next unused BIP-44 INTERNAL (change) address for
* [accountIndex], base58-encoded — the change-side twin of
* [nextReceiveAddress]; same used-set semantics and cold-start
* caveat. Builds pick change themselves (`setFunding`); this
* accessor exists for callers that must NAME a change address
* up front (e.g. `CoreTransactionBuilder.setChangeAddress`).
*/
fun nextChangeAddress(accountIndex: Int = 0): String {
require(accountIndex >= 0) { "accountIndex must be non-negative, got $accountIndex" }
return WalletManagerNative.coreWalletNextChangeAddress(handle, accountIndex)

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.

🔴 Blocking: Map native address errors into the public SDK hierarchy

Both new public methods call an external fun directly. The JNI functions use take_pwffi_error and throw_sdk_exception, so failures such as a nonexistent account or invalid native handle are raised as the internal DashSDKException. DashSdkError.kt explicitly requires every public SDK entry point that calls native code to use mapNativeErrors; without it, callers expecting DashSdkError.NotFound or DashSdkError.PlatformWallet receive an implementation-level exception instead. Wrap both native calls at this public boundary.

Suggested change
fun nextReceiveAddress(accountIndex: Int = 0): String {
require(accountIndex >= 0) { "accountIndex must be non-negative, got $accountIndex" }
return WalletManagerNative.coreWalletNextReceiveAddress(handle, accountIndex)
}
/**
* The engine's next unused BIP-44 INTERNAL (change) address for
* [accountIndex], base58-encoded — the change-side twin of
* [nextReceiveAddress]; same used-set semantics and cold-start
* caveat. Builds pick change themselves (`setFunding`); this
* accessor exists for callers that must NAME a change address
* up front (e.g. `CoreTransactionBuilder.setChangeAddress`).
*/
fun nextChangeAddress(accountIndex: Int = 0): String {
require(accountIndex >= 0) { "accountIndex must be non-negative, got $accountIndex" }
return WalletManagerNative.coreWalletNextChangeAddress(handle, accountIndex)
fun nextReceiveAddress(accountIndex: Int = 0): String {
require(accountIndex >= 0) { "accountIndex must be non-negative, got $accountIndex" }
return org.dashfoundation.dashsdk.errors.mapNativeErrors {
WalletManagerNative.coreWalletNextReceiveAddress(handle, accountIndex)
}
}
/**
* The engine's next unused BIP-44 INTERNAL (change) address for
* [accountIndex], base58-encoded — the change-side twin of
* [nextReceiveAddress]; same used-set semantics and cold-start
* caveat. Builds pick change themselves (`setFunding`); this
* accessor exists for callers that must NAME a change address
* up front (e.g. `CoreTransactionBuilder.setChangeAddress`).
*/
fun nextChangeAddress(accountIndex: Int = 0): String {
require(accountIndex >= 0) { "accountIndex must be non-negative, got $accountIndex" }
return org.dashfoundation.dashsdk.errors.mapNativeErrors {
WalletManagerNative.coreWalletNextChangeAddress(handle, accountIndex)
}
}

source: ['codex']

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 70a4192 — both accessors now wrap their native call in mapNativeErrors, and the stale class KDoc is refreshed. 10db4ad adds host-JVM coverage: negative-index and closed-handle guards, the DashSDKException(7) → DashSdkError.NotFound mapping, and a source-scanning lint test (GateCoverageLintTest pattern) that structurally enforces the mapNativeErrors wrapper on both accessors so this cannot regress. Instrumented device coverage remains the documented follow-up. Ready for revalidation.

🤖 Generated with Claude Code

Comment on lines +73 to +88
fun nextReceiveAddress(accountIndex: Int = 0): String {
require(accountIndex >= 0) { "accountIndex must be non-negative, got $accountIndex" }
return WalletManagerNative.coreWalletNextReceiveAddress(handle, accountIndex)
}

/**
* The engine's next unused BIP-44 INTERNAL (change) address for
* [accountIndex], base58-encoded — the change-side twin of
* [nextReceiveAddress]; same used-set semantics and cold-start
* caveat. Builds pick change themselves (`setFunding`); this
* accessor exists for callers that must NAME a change address
* up front (e.g. `CoreTransactionBuilder.setChangeAddress`).
*/
fun nextChangeAddress(accountIndex: Int = 0): String {
require(accountIndex >= 0) { "accountIndex must be non-negative, got $accountIndex" }
return WalletManagerNative.coreWalletNextChangeAddress(handle, accountIndex)

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 device coverage for both address APIs

No automated test exercises either new Kotlin-to-JNI path. This repository already builds the packaged native library and runs connectedDebugAndroidTest, while WalletManagerRoundTripTest creates a default wallet offline, so coverage can verify the actual exported symbols without network access. Add an instrumented test that calls both methods, confirms repeated calls return the same unused address, and confirms negative account indices are rejected; this protects the JNI signatures, handle plumbing, and documented current-address semantics.

source: ['coderabbit']

HashEngineering and others added 2 commits August 1, 2026 08:40
…ndary

Review blocker (thepastaclaw): nextReceiveAddress/nextChangeAddress
called the external fun directly, letting the JNI layer's internal
DashSDKException escape a public SDK entry point. Wrap both calls in
mapNativeErrors per the DashSdkError.kt contract, and refresh the stale
ManagedCoreWallet class KDoc (the class now carries address accessors,
not just broadcast entry points).

:sdk:compileReleaseKotlin clean.

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

Four host-JVM tests for nextReceiveAddress/nextChangeAddress covering
everything on the Kotlin side of the JNI boundary (the native lib cannot
load on the host): negative accountIndex rejected before any native
call, the closed-handle guard fires first, DashSDKException(7) maps to
DashSdkError.NotFound through mapNativeErrors, and - in the
GateCoverageLintTest tradition - a source-scanning guard that both
public accessors wrap their WalletManagerNative call in mapNativeErrors,
making the review blocker structurally non-regressable.

Real JNI linkage, derivation-vector, and used-set semantics remain the
instrumented follow-up (connectedDebugAndroidTest).

4/4 pass via :sdk:testDebugUnitTest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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