Skip to content

feat(sdk): refresh protocol version on SDK init (FFI/WASM/JS)#3902

Open
Claudius-Maginificent wants to merge 2 commits into
v3.1-devfrom
feat/sdk-refresh-on-init
Open

feat(sdk): refresh protocol version on SDK init (FFI/WASM/JS)#3902
Claudius-Maginificent wants to merge 2 commits into
v3.1-devfrom
feat/sdk-refresh-on-init

Conversation

@Claudius-Maginificent

@Claudius-Maginificent Claudius-Maginificent commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

Why this PR exists

Problem. A freshly built SDK seeds its protocol version to the per-network min_protocol_version floor and only ratchets it upward once a proven query returns the network's real version. Until that first query lands, fee-sensitive flows (e.g. shielded pool shield/unshield/transfer/withdraw) reserve fees against the seed floor, not the network's actual protocol version.

What breaks without it. Callers that issue a fee-sensitive operation as their very first action — before any other query has taught the SDK the real version — compute reservations against the floor. This PR makes every real init path eagerly learn the network version up front.

Blocking relationship. Compiles independently of #3900: the wiring calls the existing Sdk::refresh_protocol_version() (present on v3.1-dev with the same signature), so it is based on v3.1-dev, not stacked atop #3900. It also adds a small Sdk::is_mock() predicate to rs-sdk that references only types already on v3.1-dev, so independence still holds. Semantically it pairs with #3900 — that PR collapses the floor to a flat PV10 baseline and drops the clamp; this PR makes that low floor safe by refreshing on init. The two share rs-sdk/src/sdk.rs, but edit non-overlapping regions (the is_mock insertion sits where #3900 makes no change), so they merge cleanly. Intended to land together.

Issue being fixed or feature implemented

Eagerly call the existing best-effort refresh_protocol_version() on every network-backed SDK init path so the SDK learns the network's real protocol version at construction instead of on the first downstream query.

What was done?

A best-effort refresh is wired into every real (network-backed) init path. Failures never abort SDK creation — they log at warn and proceed with the floor version. Pinned SDKs are unaffected: refresh_protocol_version() is a no-op when version updating is disabled.

Mock SDKs are skipped at the helper level. A new Sdk::is_mock(&self) -> bool predicate (added to rs-sdk; additive, non-breaking) lets best_effort_refresh detect a mock from the Sdk itself and return early — so the FFI init paths call refresh unconditionally without threading a per-branch flag, and mock SDKs never issue a guaranteed-failing proven query.

Init path Layer Behaviour
dash_sdk_create FFI Calls refresh; self-skips when the SDK is a mock (null/empty dapi_addresses)
dash_sdk_create_extended FFI Calls refresh; self-skips on mock
dash_sdk_create_trusted FFI Always real → refreshes
dash_sdk_create_with_callbacks FFI Covered indirectly — delegates to dash_sdk_create_extended
dash_sdk_create_handle_with_mock FFI Mock only — refresh self-skips (no network round-trip)
WasmSdkBuilder::build WASM Now async; awaits a best-effort refresh before returning (always network-real)
EvoSDK::connect JS/TS Awaits the now-async builder.build()

A private best_effort_refresh(sdk, runtime) helper centralizes the mock self-skip + FFI runtime.block_on(...) + log-and-proceed pattern, reusing the same mechanism as the existing dash_sdk_refresh_protocol_version FFI wrapper.

Note: native rs-sdk init (SdkBuilder::build) is not wired here — it stays synchronous and cannot await the async refresh. Native callers refresh explicitly or rely on the lazy first-query ratchet. Tracked as follow-up.

How Has This Been Tested?

  • cargo build -p dash-sdk and cargo build -p dash-sdk --no-default-features — clean in both configs (the no-default-features build exercises the #[cfg(not(feature = "mocks"))] arm of is_mock, since mocks is in dash-sdk's default set).
  • cargo check -p rs-sdk-ffi -p wasm-sdk — clean on v3.1-dev (this is what determined the base branch).
  • cargo clippy -p dash-sdk -p rs-sdk-ffi --all-targets -- -D warnings and cargo clippy -p wasm-sdk -- -D warnings — clean.
  • cargo fmt -p dash-sdk -p rs-sdk-ffi -p wasm-sdk -- --check — clean.
  • JS/TS consistency verified: WasmSdkBuilder::build is async, EvoSDK::connect is async and awaits it.
  • End-to-end (paloma devnet): validated as part of the combined floor + refresh-on-init branch before the floor work was split into refactor(sdk): per-network protocol-version floor + version_pinned unification #3900 — 5/5 identity flows passed (cr_003, id_001, id_002, id_003, id_005; ~102s; --test-threads=1). The SDK seeded at PV10 and auto-ratcheted to the network's active PV12 on the first proven response, confirming the refresh-on-init path lands the client on the correct version.

Breaking Changes

WasmSdkBuilder.build() is now async (returns a Promise<WasmSdk>). JS/TS callers must await it. The in-repo caller (EvoSDK::connect) and the wasm-sdk tests already await build(), so no internal breakage; downstream JS consumers calling build() synchronously must add await. The new Sdk::is_mock() is purely additive (non-breaking).

Related

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

🤖 Co-authored by Claudius the Magnificent AI Agent

Summary by CodeRabbit

  • New Features

    • Added the ability to detect when the SDK is running in mock mode.
  • Bug Fixes

    • Improved protocol version initialization handling during SDK setup; version refresh failures are now logged as warnings rather than blocking initialization.
  • Chores

    • Enhanced SDK initialization to use asynchronous patterns for better resource handling.

The SDK seeds its protocol version to the per-network min_protocol_version
floor at construction and only ratchets upward once a proven query returns.
Wire an eager, best-effort refresh_protocol_version() into every real
(network-backed) init path so fee-sensitive flows reserve against the
network's actual protocol version from the first request instead of the
seed floor.

Per init path:

  FFI (packages/rs-sdk-ffi/src/sdk.rs):
  - dash_sdk_create            -> refresh on the real (non-mock) branch
  - dash_sdk_create_extended   -> refresh on the real (non-mock) branch
  - dash_sdk_create_trusted    -> refresh always (path is always real)
  - dash_sdk_create_with_callbacks -> covered indirectly (delegates to
    dash_sdk_create_extended)
  - dash_sdk_create_handle_with_mock -> skipped (mock only, no network)

  WASM (packages/wasm-sdk/src/sdk.rs):
  - WasmSdkBuilder::build is now async and awaits a best-effort refresh
    before returning; mock/test constructors are unaffected.

  JS (packages/js-evo-sdk/src/sdk.ts):
  - EvoSDK::connect awaits the now-async builder.build().

Failures never abort SDK creation: a private best_effort_refresh helper
(FFI) and the WASM build path log at warn and proceed with the floor
version. Pinned SDKs are unaffected — refresh_protocol_version is a no-op
when version updating is disabled.

<sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub>

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds best-effort protocol version refresh at SDK initialization time across three SDK flavors. A new Sdk::is_mock() method gates refresh on non-mock instances. The Rust FFI layer gains a best_effort_refresh helper called from all three dash_sdk_create* entry points. WasmSdkBuilder::build becomes async with the same refresh step, and the JS SDK is updated to await the now-async build.

Changes

Protocol version refresh at SDK initialization

Layer / File(s) Summary
Sdk::is_mock() utility
packages/rs-sdk/src/sdk.rs
Adds a pub fn is_mock(&self) -> bool method to impl Sdk, feature-gated to return true only when compiled with mocks and the inner instance is SdkInstance::Mock.
FFI best_effort_refresh helper and wiring
packages/rs-sdk-ffi/src/sdk.rs
Introduces best_effort_refresh private helper that skips mock SDKs, calls refresh_protocol_version() via runtime.block_on, and logs on failure. Wires it into dash_sdk_create, dash_sdk_create_extended, and dash_sdk_create_trusted after each successful build.
Async Wasm build + JS await
packages/wasm-sdk/src/sdk.rs, packages/js-evo-sdk/src/sdk.ts
WasmSdkBuilder::build becomes async, awaiting refresh_protocol_version() on init with warn-on-failure. EvoSDK.connect() is updated to await builder.build().

Sequence Diagram(s)

sequenceDiagram
  participant EvoSDK
  participant WasmSdkBuilder
  participant WasmSdk
  participant refresh_protocol_version

  EvoSDK->>WasmSdkBuilder: builder.build() [awaited]
  WasmSdkBuilder->>WasmSdk: construct inner SDK
  WasmSdk-->>WasmSdkBuilder: sdk instance
  WasmSdkBuilder->>refresh_protocol_version: sdk.refresh_protocol_version().await
  alt success
    refresh_protocol_version-->>WasmSdkBuilder: updated version
  else failure
    refresh_protocol_version-->>WasmSdkBuilder: error
    WasmSdkBuilder-->>WasmSdkBuilder: tracing::warn!, use floor version
  end
  WasmSdkBuilder-->>EvoSDK: Ok(WasmSdk)
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • dashpay/platform#3886: Introduced refresh_protocol_version and its FFI entry point that the best_effort_refresh helper in this PR calls during SDK creation.
  • dashpay/platform#3893: Changed the internals of refresh_protocol_version to use getEpochsInfo, directly affecting the behavior called by this PR's init-time refresh.
  • dashpay/platform#3809: Updated the protocol version floor that refresh_protocol_version ratchets from, complementing this PR's best-effort refresh on startup.

Suggested labels

ready for final review

Suggested reviewers

  • shumkov
  • llbartekll
  • ZocoLini

Poem

🐇 Hop, hop — the version refreshes on start,
No mock shall pretend it's the real counterpart!
Async the builder, awaited with care,
Warnings on failure float soft through the air.
The protocol floor holds, no panic, no fright —
This rabbit ships SDKs that initialize right! 🌟

🚥 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 directly matches the main objective: making the SDK refresh protocol version during initialization across FFI, WASM, and JS layers. It is concise, specific, and accurately summarizes the core change.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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/sdk-refresh-on-init

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 and usage tips.

…eal bool

best_effort_refresh now checks Sdk::is_mock() and skips the proven refresh
on mock SDKs, so the FFI init paths no longer thread a redundant is_real
flag. Behaviour is unchanged: real SDKs refresh, mocks skip.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@lklimek lklimek marked this pull request as ready for review June 15, 2026 15:01
@lklimek lklimek requested a review from Copilot June 15, 2026 15:01
@thepastaclaw

thepastaclaw commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

✅ Review complete (commit 1742883)

Copilot AI 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.

Pull request overview

This PR makes network-backed SDK initialization eagerly attempt a protocol-version refresh so fee-sensitive flows don’t reserve fees against the seeded floor version before the first downstream proven query updates the SDK.

Changes:

  • Make WasmSdkBuilder::build() async and trigger a protocol-version refresh during build.
  • Add Sdk::is_mock() and use it in FFI init to skip refresh for mock SDK instances.
  • Update JS EvoSDK::connect() to await the now-async wasm builder build().

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
packages/wasm-sdk/src/sdk.rs Makes wasm builder build() async and attempts protocol-version refresh during SDK construction.
packages/rs-sdk/src/sdk.rs Adds Sdk::is_mock() predicate to allow init paths to self-skip refresh for mocks.
packages/rs-sdk-ffi/src/sdk.rs Adds a best-effort init-time refresh helper and calls it from FFI create paths.
packages/js-evo-sdk/src/sdk.ts Awaits the now-async wasm SDK builder build() in EvoSDK.connect().

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 316 to +322
let sdk = self.inner.build().map_err(WasmSdkError::from)?;
if let Err(e) = sdk.refresh_protocol_version().await {
tracing::warn!(
error = %e,
"protocol version refresh failed on init; proceeding with floor version"
);
}
}

pub fn build(self) -> Result<WasmSdk, WasmSdkError> {
pub async fn build(self) -> Result<WasmSdk, WasmSdkError> {
Comment on lines +37 to +43
match runtime.block_on(sdk.refresh_protocol_version()) {
Ok(v) => debug!(protocol_version = v, "protocol version refreshed on init"),
Err(e) => warn!(
error = %e,
"protocol version refresh failed on init; proceeding with floor version"
),
}

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/wasm-sdk/src/sdk.rs (1)

289-294: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update the example to match async build().

Line 293 still shows synchronous usage (const sdk = builder.build();) even though build() is now async. This example should await the call to avoid misleading consumers.

Suggested doc fix
-/// const sdk = builder.build();
+/// const sdk = await builder.build();
🤖 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/wasm-sdk/src/sdk.rs` around lines 289 - 294, The documentation
example for the `build()` method in WasmSdkBuilder shows synchronous usage with
`const sdk = builder.build();` but the method is now async. Update the example
code comment to properly `await` the `build()` call by changing it to `const sdk
= await builder.build();` to accurately reflect the async nature of the method
and prevent misleading consumers of the API.
🤖 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.

Outside diff comments:
In `@packages/wasm-sdk/src/sdk.rs`:
- Around line 289-294: The documentation example for the `build()` method in
WasmSdkBuilder shows synchronous usage with `const sdk = builder.build();` but
the method is now async. Update the example code comment to properly `await` the
`build()` call by changing it to `const sdk = await builder.build();` to
accurately reflect the async nature of the method and prevent misleading
consumers of the API.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c4c3451c-9a67-4c0a-a2e4-a34f1f23abe0

📥 Commits

Reviewing files that changed from the base of the PR and between e2039e5 and 1742883.

📒 Files selected for processing (4)
  • packages/js-evo-sdk/src/sdk.ts
  • packages/rs-sdk-ffi/src/sdk.rs
  • packages/rs-sdk/src/sdk.rs
  • packages/wasm-sdk/src/sdk.rs

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

Code Review

PR cleanly wires best-effort refresh_protocol_version into rs-sdk-ffi, wasm-sdk, and js-evo-sdk init paths and adds Sdk::is_mock so mocks self-skip. Verified against HEAD 1742883. Two valid concerns: (1) the helper's outer Err arm is unreachable because refresh_protocol_version always returns Ok by design; (2) in dash_sdk_create_trusted the refresh runs synchronously before the trusted provider's quorum cache prefetch, which both blocks the FFI caller on a network round-trip the trusted path deliberately backgrounds AND means the proven refresh likely either falls back to lazy quorum fetching or fails proof verification on cold start. The non-trusted WASM/JS default path also cannot learn the network protocol version because WasmContext::get_quorum_public_key unconditionally errors, but this is a pre-existing limitation of non-trusted WASM mode rather than a regression introduced here.

🟡 1 suggestion(s) | 💬 2 nitpick(s)

2 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/rs-sdk-ffi/src/sdk.rs`:
- [SUGGESTION] packages/rs-sdk-ffi/src/sdk.rs:507-535: Trusted-setup init blocks on network and refreshes before quorum cache prefetch
  dash_sdk_create_trusted was deliberately structured to return quickly: provider_for_prefetch.update_quorum_caches() is spawned on the runtime (line 531) so the FFI caller is not blocked on a network round-trip. Inserting best_effort_refresh(&sdk, &runtime) at line 509 reverses this design intent in two ways. (1) It runs runtime.block_on(sdk.refresh_protocol_version()) synchronously before returning the handle, forcing the caller to wait on a proven getEpochsInfo round-trip. (2) Sdk::refresh_protocol_version issues a proven query whose verification path needs quorum public keys, but TrustedHttpContextProvider's quorum cache has not yet been warmed at this point — the prefetch is spawned afterward. The proven query will therefore either trigger an on-demand quorum fetch inline (defeating the prefetch design) or fall through refresh.rs's warn-and-keep-current-version branch, leaving the seed protocol version at the floor — exactly the symptom this PR aims to eliminate. Move the refresh into the spawned async block after update_quorum_caches().await completes, or await the quorum prefetch before calling best_effort_refresh.

Comment on lines +29 to +44
/// Best-effort protocol-version refresh on SDK init.
///
/// Logs a warning and proceeds if the network is unreachable; never fails SDK creation.
fn best_effort_refresh(sdk: &Sdk, runtime: &BigStackRuntime) {
// Mock SDKs have no live network; refreshing only logs noise.
if sdk.is_mock() {
return;
}
match runtime.block_on(sdk.refresh_protocol_version()) {
Ok(v) => debug!(protocol_version = v, "protocol version refreshed on init"),
Err(e) => warn!(
error = %e,
"protocol version refresh failed on init; proceeding with floor version"
),
}
}

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.

💬 Nitpick: best_effort_refresh's Err arm is unreachable

Sdk::refresh_protocol_version (packages/rs-sdk/src/sdk/refresh.rs:73-104) already catches ExtendedEpochInfo::fetch_current errors, logs them at warn under target dash_sdk::protocol_version, then unconditionally returns Ok(self.protocol_version_number()). The outer Err(e) => warn!("protocol version refresh failed on init; ...") arm here (and the equivalent in packages/wasm-sdk/src/sdk.rs:317-322) can therefore never fire. The user-visible failure log on init is actually the inner 'proven protocol-version refresh failed; keeping current version' line, not the more descriptive outer message. Either drop the match in favor of letting the inner log speak for itself, or change refresh_protocol_version to surface the inner error so the documented outer warn actually runs.

source: ['claude']

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.

4 participants