Skip to content

fix(swift-sdk): show shielded funding steps and real fee estimate#3845

Merged
QuantumExplorer merged 1 commit into
v3.1-devfrom
claude/trusting-jones-490cdd
Jun 10, 2026
Merged

fix(swift-sdk): show shielded funding steps and real fee estimate#3845
QuantumExplorer merged 1 commit into
v3.1-devfrom
claude/trusting-jones-490cdd

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented Jun 10, 2026

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

Two SwiftExampleApp issues found during a full shielded end-to-end test run on devnet:

  1. Registration progress sheet shows asset-lock steps for shielded funding. When an identity is funded from the shielded balance (Type-20 IdentityCreateFromShieldedPool), the progress sheet still rendered the asset-lock pipeline steps ("Building asset-lock transaction", InstantSend/ChainLock waits). That path has no asset lock at all, so the view sat on the first step for the entire 30–90 s Halo 2 proof.
  2. Send screen fee estimate ~500x too low. The shielded Send screen showed "Estimated Fee: ~0.000003" DASH from hardcoded placeholders, while the real consensus-pinned fees are e.g. 162,851,200 credits (~0.0016 DASH) for a 2-action shielded transfer.

What was done?

Fix 1 — funding-source-aware registration steps

  • IdentityRegistrationController gains a FundingKind (assetLock / shieldedPool, default assetLock), threaded through RegistrationCoordinator.startRegistration; only the shielded call site in CreateIdentityView passes .shieldedPool.
  • RegistrationProgressSection renders a 4-step shielded set (Selecting shielded notes → Generating Halo 2 proof → Broadcasting transition → Registering identity) when funding is shielded. There is no per-stage signal from Rust during the opaque FFI call, so transitions are driven by controller.phase + elapsed time since lastSubmittedAt (same heuristic precedent as the existing broadcast sub-steps). Broadcast/register stay pending rather than fake-checking until the call completes. The asset-lock path is unchanged.

Fix 2 — real shielded fee model via FFI

  • New marshal-only FFI binding platform_wallet_shielded_estimate_fee(kind, num_actions, out_fee) in rs-platform-wallet-ffi, dispatching to rs-dpp's compute_minimum_shielded_fee / compute_shielded_unshield_fee / compute_shielded_withdrawal_fee at PlatformVersion::latest() (the same version the builders pin). No fee logic re-derived in Swift, per the Swift SDK architectural rules.
  • Thin Swift wrapper PlatformWalletManager.estimateShieldedFee(kind:numActions:).
  • SendViewModel now resolves the shielded flows' estimates through the FFI with numActions: 2 (single-note spend with change — the exact count isn't known until the builder selects notes). The Shield flow uses the transfer base fee since the Type-15 builder reserves exactly compute_minimum_shielded_fee(2) as its structure-check minimum. coreToCore / platformToPlatform untouched.

How Has This Been Tested?

  • New Rust unit tests pin the 2-action estimator output to the observed on-chain values: transfer = 162,851,200; unshield = 168,934,000; withdrawal = 275,191,200 credits (cargo test -p platform-wallet-ffi --features shielded estimate_fee — 2 passed), plus an unknown-kind rejection test.
  • cargo fmt --all, cargo check -p platform-wallet-ffi --features shielded, cargo check -p platform-wallet --features shielded — all clean.
  • ./build_ios.sh xcframework rebuild — BUILD SUCCEEDED.
  • SwiftExampleApp xcodebuild ... clean build against the rebuilt framework — exit 0.

Breaking Changes

None — UI labels and a new additive FFI symbol; not consensus-breaking.

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

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features
    • Added shielded fee estimation capability for transaction planning
    • Introduced shielded pool funding option for identity registration
    • Enhanced registration progress UI with funding-method-specific tracking and step visualization

Two SwiftExampleApp fixes from a shielded end-to-end run on devnet:

- The registration progress sheet always rendered the 5 asset-lock
  steps; the Type-20 IdentityCreateFromShieldedPool path has no asset
  lock, so it sat on "Building asset-lock transaction" for the whole
  Halo 2 proof. The controller now carries a FundingKind and the
  progress section renders a 4-step shielded set (select notes ->
  Halo 2 proof -> broadcast -> register) driven by phase + elapsed
  time since submit.

- The Send screen's shielded fee estimates were hardcoded placeholders
  ~500x below the consensus-pinned fees. Added a marshal-only FFI
  binding (platform_wallet_shielded_estimate_fee) over rs-dpp's
  compute_minimum_shielded_fee / compute_shielded_unshield_fee /
  compute_shielded_withdrawal_fee, a thin Swift wrapper, and wired
  SendViewModel to it (2 Orchard actions = single-note spend with
  change). A unit test pins the 2-action values to the observed
  on-chain fees.

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

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

This PR adds two independent features: (1) shielded-transaction fee estimation via a new Rust FFI binding, Swift wrapper enum, and SendViewModel integration; and (2) identity-registration UI support for two funding paths (asset-lock and shielded-pool) with funding-aware progress step rendering and timing logic.

Changes

Shielded Fee Estimation

Layer / File(s) Summary
Rust FFI shielded fee estimator
packages/rs-platform-wallet-ffi/src/shielded_send.rs
New platform_wallet_shielded_estimate_fee FFI function computes flat shielded fees for transfer/unshield/withdrawal transitions pinned to the latest platform version. Validates output pointer, rejects unknown kind with ErrorInvalidParameter, maps computation failures to ErrorArithmeticOverflow, and includes unit tests asserting exact fee values for num_actions = 2 across all fee kinds.
Swift fee estimation API and enum
packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManagerShieldedSync.swift
Introduces ShieldedFeeKind enum mapping fee kinds (transfer, unshield, withdrawal) and adds PlatformWalletManager.estimateShieldedFee(kind:numActions:) method that marshals Swift arguments to the Rust FFI and returns the computed UInt64 fee.
SendViewModel fee estimation integration
packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/ViewModels/SendViewModel.swift
Updates SendViewModel.updateFlow() to derive estimatedFee via new estimateFee(for:) helper that selects fee kind for shielded flows, invokes the Swift FFI estimator, and falls back to enum-based estimates on failure. Documentation clarifies shielded flows use FFI estimates with Swift values as fallback only.

Funding-Kind Registration UI

Layer / File(s) Summary
IdentityRegistrationController funding kind support
packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Services/IdentityRegistrationController.swift
Adds FundingKind enum with cases assetLock and shieldedPool, stores funding kind on controller, and updates constructor to accept optional fundingKind parameter (default .assetLock). Documentation describes step-set derivation for each funding path.
Coordinator and UI entry point threading
packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Services/RegistrationCoordinator.swift, packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/CreateIdentityView.swift
RegistrationCoordinator.startRegistration now accepts and threads fundingKind parameter to controller initialization. CreateIdentityView.submitShieldedFunded passes fundingKind: .shieldedPool when initiating shielded-pool registration.
RegistrationProgressView funding-aware step rendering
packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/RegistrationProgressView.swift
Generalizes progress UI from fixed 5-step asset-lock to dynamic step count based on fundingKind. Adds shieldedCurrentStep computation with elapsed-time thresholds for note selection and Halo 2 proof generation phases. Updates stepTitle, stepState, and footerText to return funding-specific UI strings and skip-logic; asset-lock behavior preserved. Docstrings describe funding-source-specific step sets.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • shumkov
  • lklimek

Poem

A rabbit hops through fee computation's hall,
Two funding paths now dance, both shielded and tall,
Rust whispers estimates to Swift's eager ears,
Progress steps shimmer for each funding tier—
Platform grows wise, with precision sincere! 🐰✨

🚥 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 accurately describes both main changes: shielded funding steps visualization and real fee estimate implementation via FFI binding.
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 claude/trusting-jones-490cdd

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.

@thepastaclaw

thepastaclaw commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

✅ Review complete (commit 1cb2e85)

@QuantumExplorer QuantumExplorer left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Reviewed

@QuantumExplorer
QuantumExplorer merged commit c9f8ef5 into v3.1-dev Jun 10, 2026
17 of 19 checks passed
@QuantumExplorer
QuantumExplorer deleted the claude/trusting-jones-490cdd branch June 10, 2026 14:52
@codecov

codecov Bot commented Jun 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.12%. Comparing base (af5611e) to head (1cb2e85).
⚠️ Report is 9 commits behind head on v3.1-dev.

Additional details and impacted files
@@             Coverage Diff              @@
##           v3.1-dev    #3845      +/-   ##
============================================
+ Coverage     87.04%   87.12%   +0.08%     
============================================
  Files          2677     2641      -36     
  Lines        329918   327535    -2383     
============================================
- Hits         287182   285370    -1812     
+ Misses        42736    42165     -571     
Components Coverage Δ
dpp 87.66% <93.64%> (+0.24%) ⬆️
drive 86.03% <100.00%> (+0.04%) ⬆️
drive-abci 89.30% <ø> (+<0.01%) ⬆️
sdk ∅ <ø> (∅)
dapi-client ∅ <ø> (∅)
platform-version ∅ <ø> (∅)
platform-value 92.20% <ø> (ø)
platform-wallet ∅ <ø> (∅)
drive-proof-verifier 49.55% <ø> (ø)
🚀 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.

Code Review

Focused, in-scope feature PR: a marshal-only FFI fee estimator and funding-source-aware shielded registration steps. No blocking issues. Worth surfacing: the new Swift wrapper traps on negative inputs before the throws path can fire, a retry of a failed registration carries the original funding kind even if the caller passes a new one, the FFI hard-pins to PlatformVersion::latest() while builders thread sdk.version(), and the new estimator test pins exact credit values that will need rebaselining on the next fee-version bump.

🟡 3 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/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManagerShieldedSync.swift`:
- [SUGGESTION] packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManagerShieldedSync.swift:444-456: Negative numActions traps before throws path fires
  `estimateShieldedFee` is a public `throws` API but accepts `numActions: Int` and feeds it straight into `UInt(numActions)`. `UInt.init(Int)` traps on negative values, so an accidental caller passing a negative number crashes the host app at the language boundary instead of surfacing an `invalidParameter` error through the documented throwing channel. The Rust side already returns a clean error on misuse, but Swift never gets there. Guard before the conversion (or change the parameter to `UInt`) so invalid input is reported through the declared error path.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Services/RegistrationCoordinator.swift`:
- [SUGGESTION] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Services/RegistrationCoordinator.swift:97-113: Retry path keeps the old fundingKind on the existing controller
  `startRegistration` now takes a `fundingKind` parameter, but when an existing controller is in `.idle` or `.failed` the code reuses it and submits the new body without updating `fundingKind`. `IdentityRegistrationController.fundingKind` is `let`, so once a controller is created it's frozen. A user who fails registration with one funding source and retries the same slot with the other source will see the wrong step set rendered (asset-lock steps for a shielded retry or vice versa) — the exact mismatch this PR is fixing on the fresh-controller path. Either replace the controller on the restart branch, or make `fundingKind` `private(set) var` and update it before `submit`.

In `packages/rs-platform-wallet-ffi/src/shielded_send.rs`:
- [SUGGESTION] packages/rs-platform-wallet-ffi/src/shielded_send.rs:184-213: Estimator pins PlatformVersion::latest() instead of the SDK's active version
  `platform_wallet_shielded_estimate_fee` resolves `compute_*_shielded_fee` against `PlatformVersion::latest()` and the doc comment claims this matches what the builders pin via `sdk.version()` — so the estimate "can't drift from the fee the builder carves". That equivalence only holds when the host's compiled-in `latest()` matches the network's active protocol version. Mid-upgrade or on a devnet pinned to an older version the displayed estimate can diverge from the consensus-pinned fee actually carved. Either accept a `protocol_version` parameter (so Swift can pass `sdk.version()`), or soften the docstring to acknowledge the drift window. The UI consequence is bounded (a labeled `~estimate` plus a placeholder fallback), so this is a hardening suggestion, not a blocker.

Comment on lines +444 to +456
public static func estimateShieldedFee(
kind: ShieldedFeeKind,
numActions: Int = 2
) throws -> UInt64 {
var fee: UInt64 = 0
// `num_actions` is `usize` on the Rust side → imported as `UInt`.
try platform_wallet_shielded_estimate_fee(
kind.rawValue,
UInt(numActions),
&fee
).check()
return fee
}

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: Negative numActions traps before throws path fires

estimateShieldedFee is a public throws API but accepts numActions: Int and feeds it straight into UInt(numActions). UInt.init(Int) traps on negative values, so an accidental caller passing a negative number crashes the host app at the language boundary instead of surfacing an invalidParameter error through the documented throwing channel. The Rust side already returns a clean error on misuse, but Swift never gets there. Guard before the conversion (or change the parameter to UInt) so invalid input is reported through the declared error path.

Suggested change
public static func estimateShieldedFee(
kind: ShieldedFeeKind,
numActions: Int = 2
) throws -> UInt64 {
var fee: UInt64 = 0
// `num_actions` is `usize` on the Rust side → imported as `UInt`.
try platform_wallet_shielded_estimate_fee(
kind.rawValue,
UInt(numActions),
&fee
).check()
return fee
}
public static func estimateShieldedFee(
kind: ShieldedFeeKind,
numActions: Int = 2
) throws -> UInt64 {
guard numActions >= 0 else {
throw PlatformWalletError.invalidParameter("numActions must be non-negative")
}
var fee: UInt64 = 0
// `num_actions` is `usize` on the Rust side → imported as `UInt`.
try platform_wallet_shielded_estimate_fee(
kind.rawValue,
UInt(numActions),
&fee
).check()
return fee
}

source: ['codex', 'claude']

Comment on lines +184 to +213
pub unsafe extern "C" fn platform_wallet_shielded_estimate_fee(
kind: u8,
num_actions: usize,
out_fee: *mut u64,
) -> PlatformWalletFFIResult {
check_ptr!(out_fee);

let platform_version = dpp::version::PlatformVersion::latest();
let fee = match kind {
0 => compute_minimum_shielded_fee(num_actions, platform_version),
1 => compute_shielded_unshield_fee(num_actions, platform_version),
2 => compute_shielded_withdrawal_fee(num_actions, platform_version),
other => {
return PlatformWalletFFIResult::err(
PlatformWalletFFIResultCode::ErrorInvalidParameter,
format!("unknown shielded fee kind {other} (expected 0/1/2)"),
);
}
};
match fee {
Ok(credits) => {
*out_fee = credits;
PlatformWalletFFIResult::ok()
}
Err(e) => PlatformWalletFFIResult::err(
PlatformWalletFFIResultCode::ErrorArithmeticOverflow,
format!("shielded fee estimation failed: {e}"),
),
}
}

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: Estimator pins PlatformVersion::latest() instead of the SDK's active version

platform_wallet_shielded_estimate_fee resolves compute_*_shielded_fee against PlatformVersion::latest() and the doc comment claims this matches what the builders pin via sdk.version() — so the estimate "can't drift from the fee the builder carves". That equivalence only holds when the host's compiled-in latest() matches the network's active protocol version. Mid-upgrade or on a devnet pinned to an older version the displayed estimate can diverge from the consensus-pinned fee actually carved. Either accept a protocol_version parameter (so Swift can pass sdk.version()), or soften the docstring to acknowledge the drift window. The UI consequence is bounded (a labeled ~estimate plus a placeholder fallback), so this is a hardening suggestion, not a blocker.

source: ['claude']

Comment on lines +1066 to +1097
/// Pin the fee estimator to the on-chain ground-truth values observed at the current platform
/// version with 2 actions (single-note spend + change). These are the exact credits the
/// builder carves and the consensus gate validates, so the host's "Estimated Fee" must match.
#[test]
fn estimate_fee_matches_observed_onchain_values_for_2_actions() {
unsafe {
let estimate = |kind: u8| {
let mut fee: u64 = 0;
let result = platform_wallet_shielded_estimate_fee(kind, 2, &mut fee);
assert_eq!(
result.code,
PlatformWalletFFIResultCode::Success,
"kind {kind} must succeed"
);
fee
};
// kind 0 — ShieldedTransfer / Shield base.
assert_eq!(
estimate(0),
162_851_200,
"shielded transfer fee (2 actions)"
);
// kind 1 — Unshield.
assert_eq!(estimate(1), 168_934_000, "unshield fee (2 actions)");
// kind 2 — ShieldedWithdrawal.
assert_eq!(
estimate(2),
275_191_200,
"shielded withdrawal fee (2 actions)"
);
}
}

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: Hardcoded ground-truth fees will need rebaselining on each fee-version bump

estimate_fee_matches_observed_onchain_values_for_2_actions pins kind 0/1/2 at 2 actions to exact credit values (162_851_200 / 168_934_000 / 275_191_200). Because the FFI itself uses PlatformVersion::latest(), any future protocol version that adjusts the shielded fee constants will break this test even though the FFI plumbing is still correct. Either source the expected values from compute_minimum_shielded_fee(2, PlatformVersion::latest()) (validates plumbing, not constants), or add a comment that this test is intentionally version-pinned for drift detection so future maintainers know to rebaseline rather than tear out.

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.

2 participants