Skip to content

feat(rs-sdk): expose transition hash from state transition methods#3092

Draft
thepastaclaw wants to merge 7 commits into
dashpay:v3.1-devfrom
thepastaclaw:feat/sdk-transition-hash
Draft

feat(rs-sdk): expose transition hash from state transition methods#3092
thepastaclaw wants to merge 7 commits into
dashpay:v3.1-devfrom
thepastaclaw:feat/sdk-transition-hash

Conversation

@thepastaclaw
Copy link
Copy Markdown
Collaborator

@thepastaclaw thepastaclaw commented Feb 17, 2026

Issue being fixed or feature implemented

State transition methods in the Rust SDK (broadcast_and_wait, put_to_platform_and_wait_for_response, etc.) discard the transition hash (transaction ID) after broadcast. Downstream consumers (WASM SDK, evo-sdk) cannot access it without re-computing or restructuring their calls.

This is the Rust SDK foundation for the work started in #2953, restructured per review feedback to implement the logic in the Rust SDK first (rather than only in the WASM SDK).

What was done?

Added StateTransitionResult<T> — a thin wrapper that bundles a proof result T with the 32-byte transition hash (transaction ID).

Core changes:

  • New type: StateTransitionResult<T> in packages/rs-sdk/src/platform/transition/state_transition_result.rs
    • transition_hash() -> [u8; 32] — get the hash
    • into_parts() -> (T, [u8; 32]) — destructure
    • into_inner() -> T — unwrap (backward compat)
    • Deref<Target=T> — transparent access to inner result
  • Modified: BroadcastStateTransition::broadcast_and_wait returns StateTransitionResult<T> instead of T
  • Updated all 28 downstream methods across documents, tokens, identity, and transfer operations
  • Preserved post-broadcast wait failures: *_with_transition_hash helpers now wrap wait failures as WaitForStateTransitionResultFailedAfterBroadcast { transition_hash, source } after broadcast succeeds, so callers can recover the txid even when waiting fails
  • WASM structured error field: WasmSdkError exposes transitionHash as a Uint8Array for post-broadcast wait failures, matching the successful BroadcastAndWaitResultWasm.transitionHash path
  • Documentation: updated the SDK book BroadcastStateTransition excerpt and broadcast_and_wait description for StateTransitionResult<T>

Race condition avoidance

The transition hash is computed deterministically from the StateTransition struct before broadcast via StateTransition::transaction_id() (SHA-256 of serialized transition). It does not depend on blockchain state — no race condition possible. This addresses the concern raised in #2953.

WASM ergonomic API follow-up

The low-level WASM broadcastAndWait success path and post-broadcast wait-failure error path now expose the transition hash structurally. Higher-level ergonomic WASM helpers that currently return domain objects directly are intentionally left to the existing #2953 follow-up so those JS return shapes can be redesigned together instead of piecemeal in this PR.

Files modified (30+ files)

  • broadcast.rs — core trait change and post-broadcast wait error wrapper
  • state_transition_result.rs — new wrapper type
  • 6 document transitions (create, delete, purchase, replace, set_price, transfer)
  • 11 token transitions (burn, claim, config_update, destroy_frozen_funds, direct_purchase, emergency_action, freeze, mint, set_price_for_direct_purchase, transfer, unfreeze)
  • 9 identity/transfer operations
  • packages/wasm-sdk/src/error.rs — structured transitionHash getter for wait failures after broadcast
  • book/src/sdk/put-operations.md — updated broadcast_and_wait docs

How Has This Been Tested?

  • cargo fmt --all — passes
  • cargo test -p dash-sdk platform::transition::broadcast --lib — passes (3 tests)
  • cargo test -p wasm-sdk error --lib — passes (2 tests)
  • cargo check -p wasm-sdk — passes
  • cargo check -p dash-sdk — passes
  • cargo check -p wasm-sdk --target wasm32-unknown-unknown — attempted, blocked by local clang/sysroot issue: unable to create target: 'No available targets are compatible with triple "wasm32-unknown-unknown"'

Validation

Build verification

  • cargo check -p dash-sdk — confirms the new StateTransitionResult<T> type and all updated method signatures compile correctly
  • cargo check -p wasm-sdk — confirms the WASM SDK consumer compiles against the updated broadcast_and_wait return type and structured error field

Type-level correctness

  • broadcast_and_wait returns StateTransitionResult<T> instead of T, and the wrapper implements Deref<Target=T>, so callers can inspect the hash while existing method access remains ergonomic
  • Downstream callers updated to propagate StateTransitionResult<T> through put_to_platform_and_wait_for_response → callers that need only T use .into_inner()
  • The wasm-sdk broadcast layer correctly unwraps via .into_inner() for broadcast_and_wait while leaving wait_for_response (which already returns T directly) unchanged
  • Post-broadcast wait failures preserve the same transaction id hash through WaitForStateTransitionResultFailedAfterBroadcast

Backward compatibility

  • No existing public API is removed — into_inner() provides a zero-cost migration path for callers that don't need the transition hash
  • Deref impl means existing code that calls methods on the result works transparently without changes

Manual review

  • Verified each changed transition helper to confirm only post-broadcast wait errors are wrapped with the precomputed transition hash; signing, nonce, validation, and broadcast errors still return unchanged
  • Confirmed transaction_id() is called before broadcast (no race condition with blockchain state)

Breaking Changes

broadcast_and_wait now returns StateTransitionResult<T> instead of T. Callers that need just T can use .into_inner() or rely on Deref.

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

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 17, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d59b64e7-627b-4da8-bc5d-35dab1aedc46

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

A new StateTransitionResult<T> wrapper type is introduced to bundle state transition results with deterministic transition hashes. The broadcast_and_wait API is updated to return wrapped results, and 20+ call sites across document, token, and general transitions are modified to unwrap the inner value via into_inner().

Changes

Cohort / File(s) Summary
Core State Transition Infrastructure
packages/rs-sdk/src/platform/transition.rs, packages/rs-sdk/src/platform/transition/state_transition_result.rs
Introduces new public StateTransitionResult<T> generic wrapper struct with methods new(), transition_hash(), into_parts(), into_inner(), map(), and Deref impl. Adds public module export in transition.rs.
Broadcast API Change
packages/rs-sdk/src/platform/transition/broadcast.rs
Updates broadcast_and_wait return type from Result<T, Error> to Result<StateTransitionResult<T>, Error>. Adds transition_hash computation via transaction_id() and wraps final result via StateTransitionResult::new().
Document Transitions Call Site Updates
packages/rs-sdk/src/platform/documents/transitions/create.rs, delete.rs, purchase.rs, replace.rs, set_price.rs, transfer.rs
Added .into_inner() calls after awaiting broadcast_and_wait to extract wrapped inner values before pattern matching.
Token Transitions Call Site Updates
packages/rs-sdk/src/platform/tokens/transitions/burn.rs, claim.rs, config_update.rs, destroy_frozen_funds.rs, direct_purchase.rs, emergency_action.rs, freeze.rs, mint.rs, set_price_for_direct_purchase.rs, transfer.rs, unfreeze.rs
Added .into_inner() calls after awaiting broadcast_and_wait to extract wrapped inner values before pattern matching.
General Transition Call Site Updates
packages/rs-sdk/src/platform/transition/address_credit_withdrawal.rs, put_identity.rs, top_up_address.rs, top_up_identity.rs, top_up_identity_from_addresses.rs, transfer.rs, transfer_address_funds.rs, transfer_to_addresses.rs, withdraw_from_identity.rs
Added .into_inner() calls after awaiting broadcast_and_wait to extract wrapped inner values before pattern matching. Some call sites explicitly specify generic type parameters (e.g., :<PartialIdentity>).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A transition wrapped with care,
Hash and value form a pair,
Unwrapped with .into_inner() call,
Consistent changes throughout it all!
Deterministic hops, SDK takes flight,

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(rs-sdk): expose transition hash from state transition methods' directly and accurately describes the main objective of the PR: introducing a wrapper type to expose the 32-byte transition hash from state transition methods.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

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

@thepastaclaw
Copy link
Copy Markdown
Collaborator Author

@shumkov This implements the transition hash exposure in the Rust SDK first (as you suggested on #2953), so the WASM SDK stays thin. Would appreciate your review when you have a moment.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

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 (2)
packages/rs-sdk/src/platform/transition/state_transition_result.rs (1)

11-15: Consider deriving PartialEq and Eq for testability.

Adding PartialEq and Eq (bounded on T: PartialEq/T: Eq) would make it straightforward to assert equality in tests without having to decompose into parts.

Suggested change
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq, Eq)]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rs-sdk/src/platform/transition/state_transition_result.rs` around
lines 11 - 15, The StateTransitionResult struct should derive PartialEq and Eq
for easier testing: add PartialEq and Eq to the derive list on
StateTransitionResult and constrain implementations so these derives require T:
PartialEq (and T: Eq for Eq). Update the derive macro on the struct
(StateTransitionResult) to include PartialEq and Eq so tests can directly
compare instances without manual decomposition.
packages/rs-sdk/src/platform/documents/transitions/create.rs (1)

174-179: Consider propagating the transition hash through the result enums.

All document transition methods (create, delete, replace, purchase, set_price) currently discard the transition hash via into_inner(). To fully surface the hash to SDK consumers — which is the goal of this PR — these per-operation result enums (e.g., DocumentCreateResult) could carry the [u8; 32] alongside the payload, or the methods could return StateTransitionResult<DocumentCreateResult> directly. This can be a follow-up.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rs-sdk/src/platform/documents/transitions/create.rs` around lines
174 - 179, The DocumentCreateResult enum currently drops the transition hash
(via into_inner()), so update the API to propagate the transition hash: either
add a hash field to the enum (e.g., change
DocumentCreateResult::Document(Document) to carry (Document, [u8; 32]) or
similar) and adjust all callers that construct/consume DocumentCreateResult, or
instead change the create method(s) to return
StateTransitionResult<DocumentCreateResult> directly so the hash is preserved;
apply the same pattern for the other per-operation enums/methods (delete,
replace, purchase, set_price) and ensure functions that call into_inner() are
updated to handle the preserved hash accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/rs-sdk/src/platform/tokens/transitions/unfreeze.rs`:
- Around line 69-70: The broadcast call hardcodes None for settings causing
caller-provided settings to be ignored; change the unfreeze transition to
extract settings from the builder (use
unfreeze_tokens_transition_builder.settings or the builder’s with_settings()
value) and pass that into
state_transition.broadcast_and_wait::<StateTransitionProofResult>(self,
put_settings) instead of None; apply the same fix pattern used in
destroy_frozen_funds (and replicate for mint.rs, freeze.rs,
set_price_for_direct_purchase.rs) so user_fee_increase and retry configuration
are honored.

---

Nitpick comments:
In `@packages/rs-sdk/src/platform/documents/transitions/create.rs`:
- Around line 174-179: The DocumentCreateResult enum currently drops the
transition hash (via into_inner()), so update the API to propagate the
transition hash: either add a hash field to the enum (e.g., change
DocumentCreateResult::Document(Document) to carry (Document, [u8; 32]) or
similar) and adjust all callers that construct/consume DocumentCreateResult, or
instead change the create method(s) to return
StateTransitionResult<DocumentCreateResult> directly so the hash is preserved;
apply the same pattern for the other per-operation enums/methods (delete,
replace, purchase, set_price) and ensure functions that call into_inner() are
updated to handle the preserved hash accordingly.

In `@packages/rs-sdk/src/platform/transition/state_transition_result.rs`:
- Around line 11-15: The StateTransitionResult struct should derive PartialEq
and Eq for easier testing: add PartialEq and Eq to the derive list on
StateTransitionResult and constrain implementations so these derives require T:
PartialEq (and T: Eq for Eq). Update the derive macro on the struct
(StateTransitionResult) to include PartialEq and Eq so tests can directly
compare instances without manual decomposition.

Comment thread packages/rs-sdk/src/platform/tokens/transitions/unfreeze.rs Outdated
@thepastaclaw
Copy link
Copy Markdown
Collaborator Author

Addressed the CodeRabbit review in 22fc89c:

Fixed:

  • Settings bugunfreeze, mint, freeze, and set_price_for_direct_purchase now extract settings from the builder before sign() consumes it, passing them to broadcast_and_wait instead of hardcoded None. Matches the pattern in destroy_frozen_funds.
  • PartialEq/Eq — Added derives to StateTransitionResult for testability.

Deferred:

  • The into_inner() hash propagation suggestion — agreed this is better as a follow-up since it changes the public API of all document transition result enums.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/rs-sdk/src/platform/transition/state_transition_result.rs (1)

27-29: Nit: consider returning &[u8; 32] from transition_hash().

Currently returns by value (copy). Since [u8; 32] is Copy this is fine and arguably simpler, but returning a reference would be more conventional for accessor methods on fixed-size data and avoids a 32-byte copy on non-optimized builds. Totally optional.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rs-sdk/src/platform/transition/state_transition_result.rs` around
lines 27 - 29, The accessor transition_hash() currently returns [u8; 32] by
value which copies the array; change its signature to return a reference ->
&[u8; 32] and return &self.transition_hash to avoid the copy (update the method
in the impl for the relevant struct). After changing the signature, update any
call sites that rely on ownership (e.g., places that expect a copied array
should explicitly clone/copy if needed) and run tests to ensure no API
breakages.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/rs-sdk/src/platform/transition/state_transition_result.rs`:
- Around line 27-29: The accessor transition_hash() currently returns [u8; 32]
by value which copies the array; change its signature to return a reference ->
&[u8; 32] and return &self.transition_hash to avoid the copy (update the method
in the impl for the relevant struct). After changing the signature, update any
call sites that rely on ownership (e.g., places that expect a copied array
should explicitly clone/copy if needed) and run tests to ensure no API
breakages.

Copy link
Copy Markdown
Collaborator

@shumkov shumkov left a comment

Choose a reason for hiding this comment

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

It doesn't make sense to me. You have already state transition around so you can store hash into varibable and reuse it. @PastaPastaPasta please review your friend's PRs first.

@thepastaclaw
Copy link
Copy Markdown
Collaborator Author

Thanks for the review @shumkov. You make a fair point — if the caller already has the state transition, they can compute the hash themselves via state_transition.transaction_id().expect("...").

The motivation was convenience for the WASM SDK layer (where thephez reported needing the txid from put_to_platform calls), but I agree this can be done at the call site without modifying the SDK return types.

Happy to close this if the consensus is that it's unnecessary overhead. @PastaPastaPasta thoughts?

@PastaPastaPasta
Copy link
Copy Markdown
Member

Shumkov and claw most times you use the SDK you don't actually have the transition? It gets built internally I think.

I see no reason to not return txid.

@thepastaclaw
Copy link
Copy Markdown
Collaborator Author

@shumkov — I want to respectfully push back on this.

You're right that inside the SDK, the state transition exists as a local variable. But that's exactly the point — it's local to the SDK internals. The caller never sees it.

Look at create.rs:204-250:

pub async fn document_create(...) -> Result<DocumentCreateResult, Error> {
    let state_transition = builder.sign(...).await?;  // built internally
    let proof_result = state_transition.broadcast_and_wait(self, settings).await?;
    match proof_result { ... }  // consumed internally — only proof result returned
}

The StateTransition object is constructed, broadcast, and consumed entirely within this function. The caller gets back a DocumentCreateResult, MintResult, Identity, etc. — never the StateTransition.

So "you already have the state transition around" is true for someone modifying the SDK, but false for SDK consumers (WASM SDK, js-evo-sdk, application code). Without this PR (or similar), there's no way for WASM SDK callers to get the transaction ID after a put_to_platform call.

The StateTransitionResult<T> wrapper is a zero-cost way to surface this — Deref and into_inner() mean existing code doesn't break, and callers who don't need the hash simply ignore it.

As Pasta noted: "most times you use the SDK you don't actually have the transition. It gets built internally." This PR fixes that gap.

Comment thread packages/rs-sdk/src/platform/tokens/transitions/freeze.rs
Comment thread packages/rs-sdk/src/platform/transition/state_transition_result.rs
@thepastaclaw
Copy link
Copy Markdown
Collaborator Author

Re: put_settings question:

Good catch — the put_settings additions are a separate bug fix. Those transition builders were not propagating the caller's PutSettings (retry config, timeout, etc.) to the broadcast_and_wait_for_state_transition_result call, so any custom settings would get silently ignored. I'll split it into its own PR.

Re: T including transition hash:

That's a good idea. The challenge is that T is currently the inner result type (e.g., Document, Identity) which comes from the platform response, while the transition hash is computed locally before broadcast. We could add a trait bound like WithTransitionHash to T or wrap them together in a new struct. I think the cleanest approach would be a TransitionResult<T> struct that pairs T with its hash — which is essentially what this PR does. Want me to rename for clarity?

@thepastaclaw
Copy link
Copy Markdown
Collaborator Author

Re: @PastaPastaPasta's inline comments:

On put_settings in freeze/mint tokens: Good catch — those are indeed a separate bug fix (settings not being passed through to the broadcast call). Will split them into their own PR to keep it clean. 1 PR = 1 thing.

On including transition hash in T: That's a good idea — including the hash directly in the generic StateTransitionProofResult<T> type would be cleaner than a separate accessor. Let me look at whether StateTransitionProofResult can carry a transaction_id field alongside the existing data. Will prototype it.

@thepastaclaw
Copy link
Copy Markdown
Collaborator Author

Re: put_settings question:

Done — split into its own PR: #3132

Removed the put_settings changes from this PR.

Re: T including transition hash:

Looked into this. StateTransitionProofResult is an enum with 20+ variants — adding a transaction_id to each would be very invasive. The StateTransitionResult<T> wrapper in this PR is essentially the same thing: it pairs any result T with its transition hash, with Deref and into_inner() so existing code doesn't break. This is the cleanest approach without touching every variant.

@thepastaclaw
Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 20, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

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/rs-sdk/src/platform/transition/transfer_to_addresses.rs (1)

23-27: ⚠️ Potential issue | 🟡 Minor

Stale duplicate bullet in the Returns doc-comment.

The return type is a 2-tuple (AddressInfos, Credits) but the doc lists three bullets, repeating "Proof-backed address infos for provided recipients" as both the first and third items.

📝 Proposed fix
-    /// Returns tuple of:
-    /// * Proof-backed address infos for provided recipients
-    /// * Updated identity balance
-    /// * Proof-backed address infos for provided recipients
+    /// Returns tuple of:
+    /// * Proof-backed address infos for provided recipients
+    /// * Updated identity balance after the transfer
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rs-sdk/src/platform/transition/transfer_to_addresses.rs` around
lines 23 - 27, The Returns doc-comment in transfer_to_addresses.rs currently
lists three bullets and repeats "Proof-backed address infos for provided
recipients" although the function returns a 2-tuple (AddressInfos, Credits);
update the doc-comment for the function (e.g., the transfer_to_addresses
function) to list exactly two return bullets matching the actual return types:
1) Proof-backed address infos for provided recipients (AddressInfos) and 2)
Updated identity balance (Credits), removing the duplicated third bullet and
keeping wording consistent with the return type names.
🧹 Nitpick comments (1)
packages/rs-sdk/src/platform/transition/top_up_identity_from_addresses.rs (1)

84-116: Transition hash is silently discarded — high-level helpers won't propagate it to WASM/JS callers.

All five high-level SDK methods updated in this PR (top_up_from_addresses_with_nonce, token_freeze, document_transfer, transfer_credits_to_addresses, put_with_address_funding) call .into_inner() immediately and throw away the 32-byte transition hash. The hash is only observable from callers who invoke broadcast_and_wait directly — but the PR's stated motivation is specifically that "WASM SDK, js-evo-sdk, application code do not have access to the internal StateTransition". Callers using these high-level SDK methods, including the WASM SDK thin layer built on top of them, will still receive no hash.

Consider either:

  • Returning (ReturnType, [u8; 32]) from each high-level method, or
  • Defining a higher-level counterpart of StateTransitionResult<T> that wraps the domain-specific result types (FreezeResult, DocumentTransferResult, etc.)

If propagation is intentionally deferred, tracking the gap in a follow-up issue would help ensure it's not forgotten.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rs-sdk/src/platform/transition/top_up_identity_from_addresses.rs`
around lines 84 - 116, The code discards the 32-byte transition hash by calling
.into_inner() on broadcast_and_wait results; update
top_up_from_addresses_with_nonce (and the other high-level methods) to capture
the transition hash before into_inner() and propagate it to callers—either by
returning a tuple like (address_infos, balance, transition_hash) from the
handler that matches
StateTransitionProofResult::VerifiedIdentityWithAddressInfos (use the existing
collect_address_infos_from_proof and identity.balance logic) or by introducing a
small wrapper type (e.g., StateTransitionResult<T> containing result and
[u8;32]) and returning that instead so the transition hash from
broadcast_and_wait is not lost.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/rs-sdk/src/platform/transition/transfer_to_addresses.rs`:
- Around line 23-27: The Returns doc-comment in transfer_to_addresses.rs
currently lists three bullets and repeats "Proof-backed address infos for
provided recipients" although the function returns a 2-tuple (AddressInfos,
Credits); update the doc-comment for the function (e.g., the
transfer_to_addresses function) to list exactly two return bullets matching the
actual return types: 1) Proof-backed address infos for provided recipients
(AddressInfos) and 2) Updated identity balance (Credits), removing the
duplicated third bullet and keeping wording consistent with the return type
names.

---

Nitpick comments:
In `@packages/rs-sdk/src/platform/transition/top_up_identity_from_addresses.rs`:
- Around line 84-116: The code discards the 32-byte transition hash by calling
.into_inner() on broadcast_and_wait results; update
top_up_from_addresses_with_nonce (and the other high-level methods) to capture
the transition hash before into_inner() and propagate it to callers—either by
returning a tuple like (address_infos, balance, transition_hash) from the
handler that matches
StateTransitionProofResult::VerifiedIdentityWithAddressInfos (use the existing
collect_address_infos_from_proof and identity.balance logic) or by introducing a
small wrapper type (e.g., StateTransitionResult<T> containing result and
[u8;32]) and returning that instead so the transition hash from
broadcast_and_wait is not lost.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

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/state_transitions/broadcast.rs (1)

94-100: ⚠️ Potential issue | 🟠 Major

transition_hash is silently discarded — consider surfacing it to JS callers.

.into_inner() correctly satisfies the convert_proof_result type, but it throws away the transition_hash that StateTransitionResult<T> carries. The PR discussion explicitly identifies WASM SDK consumers (js-evo-sdk, browser apps) as the primary beneficiaries of having the transaction hash, yet those callers receive no way to observe it from broadcastAndWait.

A minimal approach would be to return a small two-field JS object that contains both the proof result and the hex-encoded hash, e.g.:

💡 Sketch of a hash-exposing return value
+use wasm_bindgen::JsValue;
+use js_sys::Object;

 pub async fn broadcast_and_wait(
     &self,
     #[wasm_bindgen(js_name = "stateTransition")] state_transition: &StateTransitionWasm,
     settings: Option<PutSettingsJs>,
-) -> Result<StateTransitionProofResultTypeJs, WasmSdkError> {
+) -> Result<JsValue, WasmSdkError> {
     let st: StateTransition = state_transition.into();
     let put_settings = parse_put_settings(settings)?;

     let result = st
         .broadcast_and_wait::<StateTransitionProofResult>(self.as_ref(), put_settings)
         .await
         .map_err(|e| WasmSdkError::generic(format!("Failed to broadcast: {}", e)))?;

+    let (inner, hash) = result.into_parts();
+    let proof = convert_proof_result(inner).map_err(WasmSdkError::from)?;
+    let hash_hex = hex::encode(hash);
+    let obj = Object::new();
+    js_sys::Reflect::set(&obj, &"result".into(), &proof.into())?;
+    js_sys::Reflect::set(&obj, &"transitionHash".into(), &hash_hex.into())?;
+    Ok(obj.into())
-    convert_proof_result(result.into_inner()).map_err(WasmSdkError::from)
 }

Alternatively, exposing a broadcastAndWaitWithHash sibling method would avoid a breaking change to broadcastAndWait's return type for existing callers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/wasm-sdk/src/state_transitions/broadcast.rs` around lines 94 - 100,
The code currently discards the transition_hash by calling result.into_inner()
before convert_proof_result; update the broadcast path (the method invoking
st.broadcast_and_wait::<StateTransitionProofResult> — the wasm-facing
broadcastAndWait wrapper) to extract both the inner proof result and the
transition_hash from the returned StateTransitionResult<T> (do not call
into_inner() alone), then return both to JS either by changing the return to a
small two-field object { proofResult, transitionHashHex } or by adding a sibling
method broadcastAndWaitWithHash that returns that object; ensure you hex-encode
the transition_hash, keep existing error handling (WasmSdkError) and still call
convert_proof_result on the proof payload before packaging the result.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/wasm-sdk/src/state_transitions/broadcast.rs`:
- Around line 94-100: The code currently discards the transition_hash by calling
result.into_inner() before convert_proof_result; update the broadcast path (the
method invoking st.broadcast_and_wait::<StateTransitionProofResult> — the
wasm-facing broadcastAndWait wrapper) to extract both the inner proof result and
the transition_hash from the returned StateTransitionResult<T> (do not call
into_inner() alone), then return both to JS either by changing the return to a
small two-field object { proofResult, transitionHashHex } or by adding a sibling
method broadcastAndWaitWithHash that returns that object; ensure you hex-encode
the transition_hash, keep existing error handling (WasmSdkError) and still call
convert_proof_result on the proof payload before packaging the result.

@thepastaclaw thepastaclaw force-pushed the feat/sdk-transition-hash branch from 9aba70e to 07b2e5c Compare February 21, 2026 04:50
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

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/rs-sdk/src/platform/transition.rs (1)

19-19: Optional: state_transition_result module declaration is out of alphabetical order.

It is placed after transfer_to_addresses (line 18) rather than between put_settings and top_up_address, breaking the existing alphabetical grouping. Consider moving it to maintain consistency.

♻️ Suggested placement
 pub mod put_settings;
+pub mod state_transition_result;
 pub mod top_up_address;
 ...
-pub mod state_transition_result;  // remove from line 19
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rs-sdk/src/platform/transition.rs` at line 19, The module
declaration for state_transition_result is out of alphabetical order relative to
the other pub mod lines; move the pub mod state_transition_result; declaration
so it appears between pub mod put_settings; and pub mod top_up_address; (i.e.,
place state_transition_result between those two) to restore the alphabetical
grouping and consistent ordering with transfer_to_addresses, put_settings, and
top_up_address.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/rs-sdk/src/platform/transition/state_transition_result.rs`:
- Line 11: Add PartialEq and Eq to the derive list for the StateTransitionResult
struct so tests can use assert_eq!; update the existing #[derive(Debug, Clone)]
line to #[derive(Debug, Clone, PartialEq, Eq)] (i.e., extend the derive on the
struct defined in state_transition_result.rs) and run cargo test to verify
equality-based assertions now work.

---

Nitpick comments:
In `@packages/rs-sdk/src/platform/transition.rs`:
- Line 19: The module declaration for state_transition_result is out of
alphabetical order relative to the other pub mod lines; move the pub mod
state_transition_result; declaration so it appears between pub mod put_settings;
and pub mod top_up_address; (i.e., place state_transition_result between those
two) to restore the alphabetical grouping and consistent ordering with
transfer_to_addresses, put_settings, and top_up_address.

Comment thread packages/rs-sdk/src/platform/transition/state_transition_result.rs Outdated
@thepastaclaw thepastaclaw force-pushed the feat/sdk-transition-hash branch 2 times, most recently from 4e49bd2 to b2f3b42 Compare February 21, 2026 19:25
thepastaclaw added a commit to thepastaclaw/platform that referenced this pull request Feb 21, 2026
…3092)

The into_inner() calls on broadcast results require the StateTransitionResult
type from PR dashpay#3092. Since this PR targets v3.1-dev which doesn't have dashpay#3092
merged yet, these calls break compilation. Reverting until dashpay#3092 lands.
Comment thread packages/rs-sdk/src/platform/transition/state_transition_result.rs Outdated
@thepastaclaw
Copy link
Copy Markdown
Collaborator Author

Addressing the review comments:

  1. PutSettings bug — Yes, that was a separate bug. Split it out to PR #3132. This PR no longer includes those changes.

  2. Transition hash in T vs wrapper — Replied inline. The StateTransitionResult<T> wrapper was chosen to avoid modifying 20+ result types, but happy to refactor to embedded fields if you prefer that approach.

@thepastaclaw thepastaclaw marked this pull request as draft February 25, 2026 08:21
@thepastaclaw thepastaclaw force-pushed the feat/sdk-transition-hash branch from 8d45834 to 261c79f Compare February 25, 2026 09:41
@github-actions github-actions Bot added this to the v3.1.0 milestone Feb 25, 2026
@thepastaclaw thepastaclaw force-pushed the feat/sdk-transition-hash branch 2 times, most recently from 28721d3 to cb08e5e Compare March 12, 2026 20:18
Copy link
Copy Markdown
Collaborator Author

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

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

Code Review

Well-structured PR that cleanly introduces StateTransitionResult to bundle the transition hash with broadcast results. The core implementation is correct — the hash is computed deterministically before broadcast, the wrapper is simple, and all 28 RS-SDK callers are properly updated. The WASM SDK intentionally unwraps the hash for now (acknowledged in the PR as follow-up work). Two design-level suggestions around type safety and test coverage are worth addressing.

Reviewed commit: cb08e5e

🟡 3 suggestion(s) | 💬 2 nitpick(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/wasm-sdk/src/state_transitions/broadcast.rs`:
- [SUGGESTION] line 99: WASM broadcastAndWait discards the transition hash before crossing the FFI boundary
  The WASM SDK's public `broadcastAndWait` calls `result.into_inner()` at line 99, permanently discarding the transition hash before converting to `StateTransitionProofResultTypeJs`. JavaScript consumers have no way to access the transition hash through this path. The same pattern exists in `contract.rs:228-230` and `identity.rs:691-694`, where `broadcast_and_wait` is called and the entire `StateTransitionResult` is dropped.

If this is intentionally deferred to the #2953 follow-up, consider adding a brief `// TODO(#2953): propagate transition_hash to JS` comment so the gap is tracked alongside the code.

In `packages/rs-sdk/src/platform/transition/state_transition_result.rs`:
- [SUGGESTION] lines 1-56: No unit tests for StateTransitionResult<T>
  The new type has 6 public methods (`new`, `transition_hash`, `into_parts`, `into_inner`, `map`) plus a `Deref` impl, but no `#[cfg(test)]` module. While the type is simple, unit tests would lock the contract — especially that `into_parts()` returns both components, that `map` preserves the hash, and that `Deref` provides transparent access to the inner value. This also guards against future refactors silently breaking the hash-carrying invariant.
- [SUGGESTION] line 14: Use the existing TxId newtype instead of raw [u8; 32] for transition_hash
  The `transition_hash` field uses `[u8; 32]`, but a `TxId` newtype wrapping `[u8; 32]` already exists in `transition/txid.rs` and is re-exported from the same module (`pub use txid::TxId`). Using `TxId` would provide domain-level type safety, preventing accidental use of an arbitrary 32-byte array as a transition hash, and would give callers access to future `TxId` methods (like `is_confirmed`) without a conversion step.

Comment thread packages/wasm-sdk/src/state_transitions/broadcast.rs Outdated
Comment thread packages/rs-sdk/src/platform/transition/state_transition_result.rs Outdated
Comment thread packages/rs-sdk/src/platform/transition/state_transition_result.rs
Comment thread packages/rs-sdk/src/platform/transition/state_transition_result.rs Outdated
Comment thread packages/rs-sdk/src/platform/transition/broadcast.rs
@thepastaclaw
Copy link
Copy Markdown
Collaborator Author

Re-checked this draft on current head cb08e5ed.

A couple of stale review threads are now cleaned up:

The remaining open items on this draft look like suggestions/nits rather than blockers (tests for StateTransitionResult<T>, possible TxId newtype cleanup, and the follow-up to propagate the hash through the WASM/JS boundary).

@shumkov — your old changes-request still seems to be the only thing keeping reviewDecision=CHANGES_REQUESTED. Since the later discussion clarified the consumer side (the high-level SDK methods build/broadcast the transition internally, so callers usually do not already have the state transition object), can you say whether you still want changes here? If so, I’d appreciate the concrete preferred API shape.

@thepastaclaw
Copy link
Copy Markdown
Collaborator Author

Pushed 7088daeeb to feat/sdk-transition-hash to address the remaining review follow-ups on the current draft:

  • added StateTransitionResult<T> unit tests covering the wrapper contract
  • added explicit TODO(#2953) markers at the WASM/JS boundary where the transition hash is still intentionally dropped for now
  • verified with cargo test -p dash-sdk state_transition_result and cargo check -p wasm-sdk

I left the TxId/Deref design suggestions as follow-up decisions rather than broadening this draft further.

@thepastaclaw
Copy link
Copy Markdown
Collaborator Author

thepastaclaw commented Apr 14, 2026

Review Gate

Commit: 127ba6ee

  • Debounce: 9579m ago (need 30m)

  • CI checks: build failure: Build book preview

  • CodeRabbit review: comment found

  • Off-peak hours: off-peak (03:34 AM PT Tuesday)

  • Run review now (check to override)

@thepastaclaw thepastaclaw force-pushed the feat/sdk-transition-hash branch from 7088dae to ffa5609 Compare April 14, 2026 11:57
Copy link
Copy Markdown
Collaborator Author

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

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

Code Review

PR adds a StateTransitionResult<T> wrapper to BroadcastStateTransition::broadcast_and_wait so callers can recover the transition hash. Implementation at HEAD ffa5609 is sound and contained — no consensus-critical or security concerns. Validated findings are scope/quality nits: stale book signature, missing wiring test, Deref anti-pattern, and gaps where the new type isn't yet threaded through higher-level helpers (with TODO(#2953) tracking the WASM follow-up).

Reviewed commit: ffa5609

Fresh rerun of the review pipeline for this dispatcher item. An earlier same-SHA automated review already exists, so I am posting this current run as a top-level review to avoid duplicating inline threads while still recording the fresh verification.

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

🟡 suggestion: Book documents stale `broadcast_and_wait` signature

book/src/sdk/put-operations.md:186-190

Verified at lines 186-190: the trait excerpt still shows Result<T, Error>, but broadcast.rs:268 now returns Result<StateTransitionResult<T>, Error>. Since the book is the canonical reference for BroadcastStateTransition, leaving the old signature will mislead consumers who upgrade and try to match on T directly. Update the snippet and add a short note pointing readers at into_inner() / transition_hash() so the breaking change called out in the PR description is reflected in the docs.

💡 Suggested change
    async fn broadcast_and_wait<T: TryFrom<StateTransitionProofResult> + Send>(
        &self,
        sdk: &Sdk,
        settings: Option<PutSettings>,
    ) -> Result<StateTransitionResult<T>, Error>;
🟡 suggestion: No test verifies `broadcast_and_wait` returns the correct transition hash

packages/rs-sdk/src/platform/transition/broadcast.rs:264-285

Verified at state_transition_result.rs:58-103: the unit tests only cover the inert wrapper (new, into_parts, into_inner, map, Deref). The actual contract this PR introduces — that broadcast_and_wait returns transition_hash() == StateTransition::transaction_id() of the broadcast transition — is not exercised. Downstream consumers (WASM SDK, evo-sdk) will key on this hash to reconcile against wait_for_state_transition_result_request (broadcast_request.rs:81 uses the same transaction_id()), so a regression here would be silent. Add a mock-mode integration test asserting result.transition_hash() == state_transition.transaction_id()? so future refactors of transaction_id() cannot diverge unnoticed. The PR checklist also leaves the unit/integration tests box unchecked.

🟡 suggestion: `Deref` on a non-smart-pointer wrapper is a Rust API anti-pattern

packages/rs-sdk/src/platform/transition/state_transition_result.rs:50-56

Rust API Guidelines (C-DEREF) recommend Deref only for smart pointers. StateTransitionResult<T> is a value bundle, not a pointer-like wrapper. Two practical hazards: (1) any future method on T that collides with transition_hash, into_inner, into_parts, or map will silently shadow without a compiler error; (2) callers can keep using the inner result transparently and never get nudged toward the hash, partially defeating the point of the wrapper. An explicit .result() / .inner() accessor or AsRef<T> impl would surface the same ergonomics without the foot-guns.

🟡 suggestion: Higher-level `put_*_and_wait_for_response` helpers still discard the transition hash

packages/rs-sdk/src/platform/transition/put_identity.rs:88-107

Verified across put_identity.rs:88-107, put_contract.rs:81-93, put_document.rs, top_up_identity.rs, transfer.rs: these helpers do not call broadcast_and_wait — they call put_to_platform followed by Waitable::wait_for_response, returning bare domain values (Identity, DataContract, etc.). So the new StateTransitionResult<T> is stranded at the lowest layer and Rust callers of the ergonomic API still cannot retrieve the transaction ID without manually serializing the transition again. This is the natural follow-up to thread the hash through; doing it now would close the loop on the rs-sdk side rather than leaving a partial migration. Codex's two reports labelled this blocking; downgraded here because the PR scope is clearly the BroadcastStateTransition trait and the wrapper is non-breaking via into_inner()/Deref.

🟡 suggestion: WASM `broadcastAndWait` still drops the transition hash before crossing into JS

packages/wasm-sdk/src/state_transitions/broadcast.rs:94-102

Verified: the WASM binding immediately calls result.into_inner() and only converts the inner StateTransitionProofResult, so JavaScript callers cannot read the txid the new wrapper just plumbed through. There is, however, an explicit // TODO(#2953) on line 99 acknowledging this is deferred to a follow-up. Codex flagged this as blocking in two reports; downgraded to suggestion because the deferral is intentional and tracked. Worth confirming that #2953 is filed and linked in the PR description before merge so the follow-up is not lost.

💬 nitpick: `broadcast_and_wait` now serializes the transition an extra time to compute the hash

packages/rs-sdk/src/platform/transition/broadcast.rs:264-285

StateTransition::transaction_id() (rs-dpp state_transition/mod.rs:749) calls PlatformSerializable::serialize_to_bytes(self) and SHA-256s the result. With the new line 274, the same serialization runs in addition to the one inside broadcast_request_for_state_transition() and the trace/timeout paths in wait_for_response (line 250) and wait_for_state_transition_result_request (broadcast_request.rs:81). For BatchTransitions with many document operations the body is non-trivial and ends up being serialized 3-4 times per submission. Caching the hash once in broadcast_and_wait and threading it (or the bytes) into the inner calls would amortize the cost. Not blocking — the SDK is not a hot path — but the new wrapper makes the redundancy more visible.

💬 nitpick: Eager `transaction_id()?` changes failure ordering of `broadcast_and_wait`

packages/rs-sdk/src/platform/transition/broadcast.rs:274

Placing let transition_hash = self.transaction_id()?; before self.broadcast(...) means a serialization error now preempts broadcast and surfaces a ProtocolError-shaped error before any nonce/retry path runs. In practice transaction_id() cannot fail for a transition the caller could have broadcast anyway (broadcast itself serializes), so this is mostly observable in error message ordering and timing. If the eager check is intentional (so a broken transition fails fast without occupying the broadcast pipeline), a one-line comment would prevent future refactors from quietly moving it.

🤖 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 `book/src/sdk/put-operations.md`:
- [SUGGESTION] lines 186-190: Book documents stale `broadcast_and_wait` signature
  Verified at lines 186-190: the trait excerpt still shows `Result<T, Error>`, but `broadcast.rs:268` now returns `Result<StateTransitionResult<T>, Error>`. Since the book is the canonical reference for `BroadcastStateTransition`, leaving the old signature will mislead consumers who upgrade and try to match on `T` directly. Update the snippet and add a short note pointing readers at `into_inner()` / `transition_hash()` so the breaking change called out in the PR description is reflected in the docs.

In `packages/rs-sdk/src/platform/transition/broadcast.rs`:
- [SUGGESTION] lines 264-285: No test verifies `broadcast_and_wait` returns the correct transition hash
  Verified at `state_transition_result.rs:58-103`: the unit tests only cover the inert wrapper (`new`, `into_parts`, `into_inner`, `map`, `Deref`). The actual contract this PR introduces — that `broadcast_and_wait` returns `transition_hash() == StateTransition::transaction_id()` of the broadcast transition — is not exercised. Downstream consumers (WASM SDK, evo-sdk) will key on this hash to reconcile against `wait_for_state_transition_result_request` (`broadcast_request.rs:81` uses the same `transaction_id()`), so a regression here would be silent. Add a mock-mode integration test asserting `result.transition_hash() == state_transition.transaction_id()?` so future refactors of `transaction_id()` cannot diverge unnoticed. The PR checklist also leaves the unit/integration tests box unchecked.

In `packages/rs-sdk/src/platform/transition/state_transition_result.rs`:
- [SUGGESTION] lines 50-56: `Deref<Target = T>` on a non-smart-pointer wrapper is a Rust API anti-pattern
  Rust API Guidelines (C-DEREF) recommend `Deref` only for smart pointers. `StateTransitionResult<T>` is a value bundle, not a pointer-like wrapper. Two practical hazards: (1) any future method on `T` that collides with `transition_hash`, `into_inner`, `into_parts`, or `map` will silently shadow without a compiler error; (2) callers can keep using the inner result transparently and never get nudged toward the hash, partially defeating the point of the wrapper. An explicit `.result()` / `.inner()` accessor or `AsRef<T>` impl would surface the same ergonomics without the foot-guns.

In `packages/rs-sdk/src/platform/transition/put_identity.rs`:
- [SUGGESTION] lines 88-107: Higher-level `put_*_and_wait_for_response` helpers still discard the transition hash
  Verified across `put_identity.rs:88-107`, `put_contract.rs:81-93`, `put_document.rs`, `top_up_identity.rs`, `transfer.rs`: these helpers do not call `broadcast_and_wait` — they call `put_to_platform` followed by `Waitable::wait_for_response`, returning bare domain values (`Identity`, `DataContract`, etc.). So the new `StateTransitionResult<T>` is stranded at the lowest layer and Rust callers of the ergonomic API still cannot retrieve the transaction ID without manually serializing the transition again. This is the natural follow-up to thread the hash through; doing it now would close the loop on the rs-sdk side rather than leaving a partial migration. Codex's two reports labelled this `blocking`; downgraded here because the PR scope is clearly the `BroadcastStateTransition` trait and the wrapper is non-breaking via `into_inner()`/`Deref`.

In `packages/wasm-sdk/src/state_transitions/broadcast.rs`:
- [SUGGESTION] lines 94-102: WASM `broadcastAndWait` still drops the transition hash before crossing into JS
  Verified: the WASM binding immediately calls `result.into_inner()` and only converts the inner `StateTransitionProofResult`, so JavaScript callers cannot read the txid the new wrapper just plumbed through. There is, however, an explicit `// TODO(#2953)` on line 99 acknowledging this is deferred to a follow-up. Codex flagged this as `blocking` in two reports; downgraded to suggestion because the deferral is intentional and tracked. Worth confirming that #2953 is filed and linked in the PR description before merge so the follow-up is not lost.

Copy link
Copy Markdown
Collaborator Author

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

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

Code Review

PR adds a StateTransitionResult<T> wrapper exposing the deterministic transition hash from broadcast_and_wait and rewires all internal callers. Mechanically correct and security-clean, but the value never reaches any consumer in this PR — every site, including the WASM boundary, immediately calls .into_inner() and the higher-level put_*_and_wait_for_response helpers don't use broadcast_and_wait at all. Behavioral change is unit-tested only at the wrapper level, not at the broadcast path that actually computes the hash.

Reviewed commit: ffa5609

🟡 6 suggestion(s) | 💬 3 nitpick(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-sdk/src/platform/transition/top_up_identity.rs`:
- [SUGGESTION] lines 43-46: All higher-level SDK helpers immediately discard the new transition hash
  `broadcast_and_wait` now returns `StateTransitionResult<T>`, but every internal caller — `top_up_identity` (line 44–46), `documents/transitions/create.rs:222–225`, the token/transfer/withdrawal helpers, and the WASM bindings — calls `.into_inner()` and returns the bare domain type. Worse, the `put_to_platform_and_wait_for_response` family used by most ergonomic call sites (`put_document.rs`, `put_contract.rs`, `put_identity.rs`, `vote.rs`, `dpns_usernames`, `dashpay::contact_request`) splits into `broadcast` + `wait_for_response` and never goes through `broadcast_and_wait`, so the wrapper never reaches them at all. Consumers using the documented top-level methods still cannot read the transaction ID, which is the original problem #2953 asks the SDK to solve. Either plumb the hash through `put_*_and_wait_for_response` so the change has at least one consumer-visible benefit, or narrow the scope/title to clarify that this PR only refactors the leaf `broadcast_and_wait` API.

In `packages/wasm-sdk/src/state_transitions/broadcast.rs`:
- [SUGGESTION] lines 85-101: Rust→WASM boundary strips the transition hash before JS can read it
  `WasmSdk::broadcastAndWait` calls `result.into_inner()` and returns `Promise<StateTransitionProofResultType>`, dropping the new `[u8; 32]` before `wasm-bindgen` converts the value. The same pattern repeats in the higher-level WASM helpers (`contract.rs`, `identity.rs`) and `packages/js-evo-sdk/src/state-transitions/facade.ts` still types the result as `Promise<wasm.StateTransitionProofResultType>`. The hash is FFI-trivial (Copy, no allocation) and could be exposed today as a `Uint8Array`/hex field on the JS return type. The TODO(#2953) acknowledges this gap, but as written this PR's stated downstream consumer (the WASM SDK) gets no observable value from the change.

In `packages/rs-sdk/src/platform/transition/broadcast.rs`:
- [SUGGESTION] lines 264-285: `broadcast_and_wait`'s new hash wiring is not covered by any test
  The added unit tests in `state_transition_result.rs` only exercise the wrapper's mechanical surface (`new`, `into_parts`, `into_inner`, `map`, `Deref`) using a hard-coded `[7; 32]`. None of them verify the load-bearing invariant of this PR: that the value placed in `transition_hash` is bit-for-bit `StateTransition::transaction_id()` of the broadcast transition, equal to the `state_transition_hash` keyed in `wait_for_state_transition_result_request` (broadcast_request.rs:81), and survives the `TryFrom<StateTransitionProofResult>` conversion. With every modified call site immediately calling `.into_inner()`, a future regression that drops or recomputes the hash post-broadcast would not be caught. Add a MockSdk-driven test in `broadcast.rs` (or `tests/fetch/broadcast.rs`) asserting `result.transition_hash() == st.transaction_id()?`.
- [SUGGESTION] lines 271-285: Transition hash is computed twice per `broadcast_and_wait` call
  `self.transaction_id()?` at line 274 performs a full `PlatformSerializable::serialize_to_bytes` plus SHA-256. The subsequent `wait_for_response` reaches `wait_for_state_transition_result_request` (broadcast_request.rs:81), which calls `self.transaction_id()?` again — and that path runs once per retry attempt. For BatchTransition with many document/token operations or large DataContractCreate payloads this is non-trivial extra CPU and allocation in a hot path, paid by every caller even though all current callers discard the hash. Either thread the precomputed `transition_hash` into the wait-request path so the per-retry recomputation is eliminated, or keep the broadcast-path recomputation alone and have `broadcast_and_wait` reuse the value from the `WaitForStateTransitionResultRequestV0` it builds.
- [SUGGESTION] lines 277-285: Transition hash is dropped on the error path even after a successful broadcast
  The hash is computed before broadcast (good — race-free), but if `wait_for_response` fails (timeout, proof error, network error, conversion failure) the precomputed `transition_hash` is silently discarded by the `?` propagation at lines 277/279 and the `result.map(...)` at 284. The single most useful application of an exposed transition hash is recovery: "the broadcast went out, but waiting failed — let me query by tx id later." That recovery path is impossible here because the hash never reaches the caller on `Err`. Consider returning the hash through a richer error variant (e.g. `Error::WaitFailed { transition_hash: [u8;32], source: Box<Error> }`) for the wait failure case, so the feature actually closes the gap it is meant to address.

In `packages/rs-sdk/src/platform/transition/state_transition_result.rs`:
- [SUGGESTION] lines 50-56: `Deref<Target = T>` on a value-bundle wrapper violates Rust API guidelines
  Per the Rust API guidelines (C-DEREF), `Deref` is reserved for smart-pointer-like types (`Box`, `Arc`, `MutexGuard`, owning containers like `String`/`Vec`). `StateTransitionResult<T>` is a value bundle of `(T, [u8; 32])`, not a pointer. The hazards are well-known: method-resolution ambiguity if `T` ever gains a method whose name collides with one on the wrapper (`map`, `into_inner` are already at risk), confusing diagnostics for consumers (`&result` vs `&*result`), and silent semantic drift when a future method on `StateTransitionResult` shadows a `T` method that callers were using through deref coercion. Internally the rs-sdk doesn't rely on `Deref` — every modified call site uses `.into_inner()` explicitly — so removing it would not break anything in this PR. Prefer `AsRef<T>`/explicit `inner()`/`inner_mut()` accessors.

Comment on lines +43 to +46
let identity: PartialIdentity = state_transition
.broadcast_and_wait::<PartialIdentity>(sdk, settings)
.await?
.into_inner();
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🟡 Suggestion: All higher-level SDK helpers immediately discard the new transition hash

broadcast_and_wait now returns StateTransitionResult<T>, but every internal caller — top_up_identity (line 44–46), documents/transitions/create.rs:222–225, the token/transfer/withdrawal helpers, and the WASM bindings — calls .into_inner() and returns the bare domain type. Worse, the put_to_platform_and_wait_for_response family used by most ergonomic call sites (put_document.rs, put_contract.rs, put_identity.rs, vote.rs, dpns_usernames, dashpay::contact_request) splits into broadcast + wait_for_response and never goes through broadcast_and_wait, so the wrapper never reaches them at all. Consumers using the documented top-level methods still cannot read the transaction ID, which is the original problem #2953 asks the SDK to solve. Either plumb the hash through put_*_and_wait_for_response so the change has at least one consumer-visible benefit, or narrow the scope/title to clarify that this PR only refactors the leaf broadcast_and_wait API.

source: ['claude', 'codex']

🤖 Fix this 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/src/platform/transition/top_up_identity.rs`:
- [SUGGESTION] lines 43-46: All higher-level SDK helpers immediately discard the new transition hash
  `broadcast_and_wait` now returns `StateTransitionResult<T>`, but every internal caller — `top_up_identity` (line 44–46), `documents/transitions/create.rs:222–225`, the token/transfer/withdrawal helpers, and the WASM bindings — calls `.into_inner()` and returns the bare domain type. Worse, the `put_to_platform_and_wait_for_response` family used by most ergonomic call sites (`put_document.rs`, `put_contract.rs`, `put_identity.rs`, `vote.rs`, `dpns_usernames`, `dashpay::contact_request`) splits into `broadcast` + `wait_for_response` and never goes through `broadcast_and_wait`, so the wrapper never reaches them at all. Consumers using the documented top-level methods still cannot read the transaction ID, which is the original problem #2953 asks the SDK to solve. Either plumb the hash through `put_*_and_wait_for_response` so the change has at least one consumer-visible benefit, or narrow the scope/title to clarify that this PR only refactors the leaf `broadcast_and_wait` API.

Comment on lines +96 to +101
.await
.map_err(|e| WasmSdkError::generic(format!("Failed to broadcast: {}", e)))?;

convert_proof_result(result).map_err(WasmSdkError::from)
// TODO(#2953): The transition hash currently stops at the WASM/JS boundary here;
// propagate it through the JS-facing result type in the follow-up.
convert_proof_result(result.into_inner()).map_err(WasmSdkError::from)
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🟡 Suggestion: Rust→WASM boundary strips the transition hash before JS can read it

WasmSdk::broadcastAndWait calls result.into_inner() and returns Promise<StateTransitionProofResultType>, dropping the new [u8; 32] before wasm-bindgen converts the value. The same pattern repeats in the higher-level WASM helpers (contract.rs, identity.rs) and packages/js-evo-sdk/src/state-transitions/facade.ts still types the result as Promise<wasm.StateTransitionProofResultType>. The hash is FFI-trivial (Copy, no allocation) and could be exposed today as a Uint8Array/hex field on the JS return type. The TODO(#2953) acknowledges this gap, but as written this PR's stated downstream consumer (the WASM SDK) gets no observable value from the change.

source: ['claude', 'codex']

🤖 Fix this 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/wasm-sdk/src/state_transitions/broadcast.rs`:
- [SUGGESTION] lines 85-101: Rust→WASM boundary strips the transition hash before JS can read it
  `WasmSdk::broadcastAndWait` calls `result.into_inner()` and returns `Promise<StateTransitionProofResultType>`, dropping the new `[u8; 32]` before `wasm-bindgen` converts the value. The same pattern repeats in the higher-level WASM helpers (`contract.rs`, `identity.rs`) and `packages/js-evo-sdk/src/state-transitions/facade.ts` still types the result as `Promise<wasm.StateTransitionProofResultType>`. The hash is FFI-trivial (Copy, no allocation) and could be exposed today as a `Uint8Array`/hex field on the JS return type. The TODO(#2953) acknowledges this gap, but as written this PR's stated downstream consumer (the WASM SDK) gets no observable value from the change.

Comment on lines 281 to 285
Ok(_) => trace!("broadcast_and_wait: complete success"),
Err(e) => warn!(error = ?e, "broadcast_and_wait: failed"),
}
result
result.map(|inner| StateTransitionResult::new(inner, transition_hash))
}
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🟡 Suggestion: broadcast_and_wait's new hash wiring is not covered by any test

The added unit tests in state_transition_result.rs only exercise the wrapper's mechanical surface (new, into_parts, into_inner, map, Deref) using a hard-coded [7; 32]. None of them verify the load-bearing invariant of this PR: that the value placed in transition_hash is bit-for-bit StateTransition::transaction_id() of the broadcast transition, equal to the state_transition_hash keyed in wait_for_state_transition_result_request (broadcast_request.rs:81), and survives the TryFrom<StateTransitionProofResult> conversion. With every modified call site immediately calling .into_inner(), a future regression that drops or recomputes the hash post-broadcast would not be caught. Add a MockSdk-driven test in broadcast.rs (or tests/fetch/broadcast.rs) asserting result.transition_hash() == st.transaction_id()?.

source: ['claude', 'codex']

🤖 Fix this 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/src/platform/transition/broadcast.rs`:
- [SUGGESTION] lines 264-285: `broadcast_and_wait`'s new hash wiring is not covered by any test
  The added unit tests in `state_transition_result.rs` only exercise the wrapper's mechanical surface (`new`, `into_parts`, `into_inner`, `map`, `Deref`) using a hard-coded `[7; 32]`. None of them verify the load-bearing invariant of this PR: that the value placed in `transition_hash` is bit-for-bit `StateTransition::transaction_id()` of the broadcast transition, equal to the `state_transition_hash` keyed in `wait_for_state_transition_result_request` (broadcast_request.rs:81), and survives the `TryFrom<StateTransitionProofResult>` conversion. With every modified call site immediately calling `.into_inner()`, a future regression that drops or recomputes the hash post-broadcast would not be caught. Add a MockSdk-driven test in `broadcast.rs` (or `tests/fetch/broadcast.rs`) asserting `result.transition_hash() == st.transaction_id()?`.

Comment on lines 271 to 285
@@ -274,6 +281,6 @@ impl BroadcastStateTransition for StateTransition {
Ok(_) => trace!("broadcast_and_wait: complete success"),
Err(e) => warn!(error = ?e, "broadcast_and_wait: failed"),
}
result
result.map(|inner| StateTransitionResult::new(inner, transition_hash))
}
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🟡 Suggestion: Transition hash is computed twice per broadcast_and_wait call

self.transaction_id()? at line 274 performs a full PlatformSerializable::serialize_to_bytes plus SHA-256. The subsequent wait_for_response reaches wait_for_state_transition_result_request (broadcast_request.rs:81), which calls self.transaction_id()? again — and that path runs once per retry attempt. For BatchTransition with many document/token operations or large DataContractCreate payloads this is non-trivial extra CPU and allocation in a hot path, paid by every caller even though all current callers discard the hash. Either thread the precomputed transition_hash into the wait-request path so the per-retry recomputation is eliminated, or keep the broadcast-path recomputation alone and have broadcast_and_wait reuse the value from the WaitForStateTransitionResultRequestV0 it builds.

source: ['claude']

🤖 Fix this 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/src/platform/transition/broadcast.rs`:
- [SUGGESTION] lines 271-285: Transition hash is computed twice per `broadcast_and_wait` call
  `self.transaction_id()?` at line 274 performs a full `PlatformSerializable::serialize_to_bytes` plus SHA-256. The subsequent `wait_for_response` reaches `wait_for_state_transition_result_request` (broadcast_request.rs:81), which calls `self.transaction_id()?` again — and that path runs once per retry attempt. For BatchTransition with many document/token operations or large DataContractCreate payloads this is non-trivial extra CPU and allocation in a hot path, paid by every caller even though all current callers discard the hash. Either thread the precomputed `transition_hash` into the wait-request path so the per-retry recomputation is eliminated, or keep the broadcast-path recomputation alone and have `broadcast_and_wait` reuse the value from the `WaitForStateTransitionResultRequestV0` it builds.

Comment on lines +50 to +56
impl<T> Deref for StateTransitionResult<T> {
type Target = T;

fn deref(&self) -> &T {
&self.inner
}
}
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🟡 Suggestion: Deref<Target = T> on a value-bundle wrapper violates Rust API guidelines

Per the Rust API guidelines (C-DEREF), Deref is reserved for smart-pointer-like types (Box, Arc, MutexGuard, owning containers like String/Vec). StateTransitionResult<T> is a value bundle of (T, [u8; 32]), not a pointer. The hazards are well-known: method-resolution ambiguity if T ever gains a method whose name collides with one on the wrapper (map, into_inner are already at risk), confusing diagnostics for consumers (&result vs &*result), and silent semantic drift when a future method on StateTransitionResult shadows a T method that callers were using through deref coercion. Internally the rs-sdk doesn't rely on Deref — every modified call site uses .into_inner() explicitly — so removing it would not break anything in this PR. Prefer AsRef<T>/explicit inner()/inner_mut() accessors.

source: ['claude']

🤖 Fix this 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/src/platform/transition/state_transition_result.rs`:
- [SUGGESTION] lines 50-56: `Deref<Target = T>` on a value-bundle wrapper violates Rust API guidelines
  Per the Rust API guidelines (C-DEREF), `Deref` is reserved for smart-pointer-like types (`Box`, `Arc`, `MutexGuard`, owning containers like `String`/`Vec`). `StateTransitionResult<T>` is a value bundle of `(T, [u8; 32])`, not a pointer. The hazards are well-known: method-resolution ambiguity if `T` ever gains a method whose name collides with one on the wrapper (`map`, `into_inner` are already at risk), confusing diagnostics for consumers (`&result` vs `&*result`), and silent semantic drift when a future method on `StateTransitionResult` shadows a `T` method that callers were using through deref coercion. Internally the rs-sdk doesn't rely on `Deref` — every modified call site uses `.into_inner()` explicitly — so removing it would not break anything in this PR. Prefer `AsRef<T>`/explicit `inner()`/`inner_mut()` accessors.

Comment on lines 277 to 285
@@ -274,6 +281,6 @@ impl BroadcastStateTransition for StateTransition {
Ok(_) => trace!("broadcast_and_wait: complete success"),
Err(e) => warn!(error = ?e, "broadcast_and_wait: failed"),
}
result
result.map(|inner| StateTransitionResult::new(inner, transition_hash))
}
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🟡 Suggestion: Transition hash is dropped on the error path even after a successful broadcast

The hash is computed before broadcast (good — race-free), but if wait_for_response fails (timeout, proof error, network error, conversion failure) the precomputed transition_hash is silently discarded by the ? propagation at lines 277/279 and the result.map(...) at 284. The single most useful application of an exposed transition hash is recovery: "the broadcast went out, but waiting failed — let me query by tx id later." That recovery path is impossible here because the hash never reaches the caller on Err. Consider returning the hash through a richer error variant (e.g. Error::WaitFailed { transition_hash: [u8;32], source: Box<Error> }) for the wait failure case, so the feature actually closes the gap it is meant to address.

source: ['claude']

🤖 Fix this 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/src/platform/transition/broadcast.rs`:
- [SUGGESTION] lines 277-285: Transition hash is dropped on the error path even after a successful broadcast
  The hash is computed before broadcast (good — race-free), but if `wait_for_response` fails (timeout, proof error, network error, conversion failure) the precomputed `transition_hash` is silently discarded by the `?` propagation at lines 277/279 and the `result.map(...)` at 284. The single most useful application of an exposed transition hash is recovery: "the broadcast went out, but waiting failed — let me query by tx id later." That recovery path is impossible here because the hash never reaches the caller on `Err`. Consider returning the hash through a richer error variant (e.g. `Error::WaitFailed { transition_hash: [u8;32], source: Box<Error> }`) for the wait failure case, so the feature actually closes the gap it is meant to address.

Comment on lines +271 to 277
// Compute the transition hash deterministically BEFORE broadcast.
// This is a SHA-256 hash of the serialized StateTransition and does
// not depend on blockchain state, so there is no race condition.
let transition_hash = self.transaction_id()?;

trace!("broadcast_and_wait: step 1 - broadcasting");
self.broadcast(sdk, settings).await?;
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

💬 Nitpick: Eager transaction_id()? changes failure ordering

Previously, all serialization happened inside broadcast / wait_for_response, where errors are wrapped in the retry/ExecutionError machinery and trigger refresh_identity_nonce recovery. With the new eager let transition_hash = self.transaction_id()?;, a serialization error surfaces before any broadcast attempt and bypasses both the retry and nonce-refresh paths. In practice both paths use PlatformSerializable::serialize_to_bytes, so failures here would also have been hit downstream — but the change in error wrapping/recovery semantics is worth confirming as intentional.

source: ['claude']

Comment on lines +11 to +15
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StateTransitionResult<T> {
inner: T,
transition_hash: [u8; 32],
}
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

💬 Nitpick: Add #[must_use] to the result wrapper

StateTransitionResult<T> is the public return type of every broadcast_and_wait call and the new public surface of this PR. Annotating the struct with #[must_use = "the transition hash and proof result should be inspected"] would catch accidentally ignored returns at the type level — relevant given the whole point of the change is that callers should be examining the wrapper.

💡 Suggested change
Suggested change
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StateTransitionResult<T> {
inner: T,
transition_hash: [u8; 32],
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[must_use = "the transition hash and proof result should be inspected"]
pub struct StateTransitionResult<T> {
inner: T,
transition_hash: [u8; 32],
}

source: ['claude']

Comment on lines +41 to +48
/// Maps the inner value, preserving the transition hash.
pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> StateTransitionResult<U> {
StateTransitionResult {
inner: f(self.inner),
transition_hash: self.transition_hash,
}
}
}
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

💬 Nitpick: map method has no in-tree caller

StateTransitionResult::map is defined and unit-tested but no production code uses it — broadcast_and_wait constructs the wrapper at the leaf and downstream callers immediately .into_inner(). Drop it to keep the public surface minimal until a real consumer needs it.

source: ['claude']

Copy link
Copy Markdown
Collaborator Author

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

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

Code Review

Follow-up successfully addressed the prior major review items: Deref removed in favor of AsRef/AsMut + explicit accessors, the success path threads the precomputed transition_hash through to JS via BroadcastAndWaitResultWasm, and a new Error::WaitForStateTransitionResultFailedAfterBroadcast variant carries the hash through wait failures in broadcast_and_wait. Remaining items are non-blocking: the new *_with_transition_hash put/document/identity helpers still drop the hash on wait failure (defeating the new error variant), the WASM error mapping flattens that structured hash into a hex-encoded message string, the book documents the pre-PR signature, higher-level WASM bindings (contractPublish/Update, identityCreate/TopUp/Update) still throw the hash away, and there are several smaller wasm-bindgen / surface-area nits. No blocking issues.

Reviewed commit: 9582fc3

🟡 5 suggestion(s) | 💬 4 nitpick(s)

1 additional finding

🟡 suggestion: Book documents the pre-PR `broadcast_and_wait` signature

book/src/sdk/put-operations.md (lines 186-190)

The trait excerpt in the book still prints broadcast_and_wait -> Result<T, Error>, but packages/rs-sdk/src/platform/transition/broadcast.rs:197-201 now returns Result<StateTransitionResult<T>, Error>. Since the PR description calls this out as a breaking change and the book is the canonical narrative reference for BroadcastStateTransition, leaving the snippet stale will mislead consumers upgrading to this release and hides the new transitionHash / into_inner() behavior. The descriptive paragraph at line 200 ("Combines both -- broadcast, then wait.") also makes no mention of the hash, which is the entire point of the PR.

💡 Suggested change
    async fn broadcast_and_wait<T: TryFrom<StateTransitionProofResult> + Send>(
        &self,
        sdk: &Sdk,
        settings: Option<PutSettings>,
    ) -> Result<StateTransitionResult<T>, Error>;
🤖 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/src/platform/transition/put_contract.rs`:
- [SUGGESTION] lines 48-57: New `*_with_transition_hash` helpers drop the hash on wait failure
  The new `put_to_platform_and_wait_for_response_with_transition_hash` helpers compute `transition_hash` after the broadcast in `put_to_platform` has already gone out, then call `Waitable::wait_for_response(...).await?`. If the wait fails (timeout, proof error, conversion failure), the `?` strips the precomputed hash and returns the bare inner error, which is precisely the recovery scenario the new `Error::WaitForStateTransitionResultFailedAfterBroadcast { transition_hash, source }` variant was added to support in `broadcast.rs:271-294`. The same pattern repeats in `put_document.rs:71-75`, `put_identity.rs`, `purchase_document.rs`, `transfer_document.rs`, `update_price_of_document.rs`, plus `vote.rs` (and friends) via `Waitable::wait_for_response`. Wrap the wait error here with `wrap_wait_error_after_broadcast` (or have `Waitable::wait_for_response` return `StateTransitionResult` so the hash is preserved by construction). As-is the helpers only deliver the hash on the happy path — exactly the gap the rest of the PR was trying to close.

In `book/src/sdk/put-operations.md`:
- [SUGGESTION] lines 186-190: Book documents the pre-PR `broadcast_and_wait` signature
  The trait excerpt in the book still prints `broadcast_and_wait -> Result<T, Error>`, but `packages/rs-sdk/src/platform/transition/broadcast.rs:197-201` now returns `Result<StateTransitionResult<T>, Error>`. Since the PR description calls this out as a breaking change and the book is the canonical narrative reference for `BroadcastStateTransition`, leaving the snippet stale will mislead consumers upgrading to this release and hides the new `transitionHash` / `into_inner()` behavior. The descriptive paragraph at line 200 ("Combines both -- broadcast, then wait.") also makes no mention of the hash, which is the entire point of the PR.

In `packages/wasm-sdk/src/error.rs`:
- [SUGGESTION] lines 183-195: WASM error variant flattens the structured `transition_hash` into a hex-encoded message
  Rust preserves the txid as a typed `[u8; 32]` in `Error::WaitForStateTransitionResultFailedAfterBroadcast { transition_hash, source }` (`rs-sdk/src/error.rs:115-123`), which is exactly the recovery affordance this PR set out to provide ("broadcast went out, but waiting failed — query by tx id later"). At the WASM boundary, the variant is mapped to `WasmSdkError` and the hash is `hex::encode`d directly into `message`. `WasmSdkError`'s exported surface (`kind`, `name`, `message`, `code`, `isRetriable`, lines 249-313) has no slot for the hash, so JS consumers must regex-parse English text to recover it. Add a structured `transitionHash: Option<Vec<u8>>` field exposed via a `Uint8Array` getter (analogous to the existing `code` field, and mirroring `BroadcastAndWaitResultWasm.transitionHash`) and populate it for this variant. The success path crosses cleanly; the operationally most-interesting path still does not.

In `packages/wasm-sdk/src/state_transitions/contract.rs`:
- [SUGGESTION] lines 103-234: Higher-level WASM helpers still drop the transition hash before the JS boundary
  The leaf `WasmSdk::broadcastAndWait` correctly forwards the hash, but every higher-level wasm-bindgen entry point still discards it before crossing into JS, with `TODO(#2953)` comments acknowledging the gap: `contractPublish` (contract.rs:103-119), `contractUpdate` (contract.rs:228-234), `identityCreate` (identity.rs:115-129), `identityTopUp` (identity.rs:206-220), `identityCreditTransfer` (identity.rs:338-353), `identityCreditWithdrawal` (identity.rs:489-503), `identityUpdate` (identity.rs:697-700). Each of these now has a hash-carrying rs-sdk helper available (the `*_with_transition_hash` variants and the `broadcast_and_wait` `StateTransitionResult`), but JS callers using the ergonomic APIs still cannot recover the txid. Either thread `transitionHash` through these return values now, or ensure #2953 is filed and linked in the PR description so the work is not lost.

In `packages/rs-sdk/src/platform/transition/broadcast.rs`:
- [SUGGESTION] lines 271-294: No test asserts `broadcast_and_wait`'s hash equals `StateTransition::transaction_id()`
  The PR's load-bearing invariant is that the hash placed in `StateTransitionResult::transition_hash` is bit-for-bit equal to `StateTransition::transaction_id()` of the broadcast transition (the same hash keyed in `wait_for_state_transition_result_request_with_hash`). The added tests cover (a) the wrapper's mechanical surface with hard-coded `[7; 32]`, (b) the error wrapper, and (c) the wait-request-with-hash builder — none traverse `broadcast_and_wait` end-to-end via `MockSdk` to assert `result.transition_hash() == st.transaction_id()?` and that the wait-failure path produces `WaitForStateTransitionResultFailedAfterBroadcast` carrying the same hash. With every internal call site immediately calling `.into_inner()`, a future regression that swaps in or recomputes a different hash post-broadcast would not be caught. The PR checklist still has the test-coverage box unchecked.

Comment on lines +48 to +57
) -> Result<StateTransitionResult<DataContract>, Error> {
let state_transition = self
.put_to_platform(sdk, identity_public_key, signer, settings)
.await?;
let transition_hash = state_transition.transaction_id()?;
let contract =
<DataContract as Waitable>::wait_for_response(sdk, state_transition, settings).await?;

Ok(StateTransitionResult::new(contract, transition_hash))
}
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🟡 Suggestion: New *_with_transition_hash helpers drop the hash on wait failure

The new put_to_platform_and_wait_for_response_with_transition_hash helpers compute transition_hash after the broadcast in put_to_platform has already gone out, then call Waitable::wait_for_response(...).await?. If the wait fails (timeout, proof error, conversion failure), the ? strips the precomputed hash and returns the bare inner error, which is precisely the recovery scenario the new Error::WaitForStateTransitionResultFailedAfterBroadcast { transition_hash, source } variant was added to support in broadcast.rs:271-294. The same pattern repeats in put_document.rs:71-75, put_identity.rs, purchase_document.rs, transfer_document.rs, update_price_of_document.rs, plus vote.rs (and friends) via Waitable::wait_for_response. Wrap the wait error here with wrap_wait_error_after_broadcast (or have Waitable::wait_for_response return StateTransitionResult so the hash is preserved by construction). As-is the helpers only deliver the hash on the happy path — exactly the gap the rest of the PR was trying to close.

💡 Suggested change
Suggested change
) -> Result<StateTransitionResult<DataContract>, Error> {
let state_transition = self
.put_to_platform(sdk, identity_public_key, signer, settings)
.await?;
let transition_hash = state_transition.transaction_id()?;
let contract =
<DataContract as Waitable>::wait_for_response(sdk, state_transition, settings).await?;
Ok(StateTransitionResult::new(contract, transition_hash))
}
let state_transition = self
.put_to_platform(sdk, identity_public_key, signer, settings)
.await?;
let transition_hash = state_transition.transaction_id()?;
let contract = <DataContract as Waitable>::wait_for_response(sdk, state_transition, settings)
.await
.map_err(|source| Error::WaitForStateTransitionResultFailedAfterBroadcast {
transition_hash,
source: Box::new(source),
})?;
Ok(StateTransitionResult::new(contract, transition_hash))

source: ['claude', 'codex']

🤖 Fix this 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/src/platform/transition/put_contract.rs`:
- [SUGGESTION] lines 48-57: New `*_with_transition_hash` helpers drop the hash on wait failure
  The new `put_to_platform_and_wait_for_response_with_transition_hash` helpers compute `transition_hash` after the broadcast in `put_to_platform` has already gone out, then call `Waitable::wait_for_response(...).await?`. If the wait fails (timeout, proof error, conversion failure), the `?` strips the precomputed hash and returns the bare inner error, which is precisely the recovery scenario the new `Error::WaitForStateTransitionResultFailedAfterBroadcast { transition_hash, source }` variant was added to support in `broadcast.rs:271-294`. The same pattern repeats in `put_document.rs:71-75`, `put_identity.rs`, `purchase_document.rs`, `transfer_document.rs`, `update_price_of_document.rs`, plus `vote.rs` (and friends) via `Waitable::wait_for_response`. Wrap the wait error here with `wrap_wait_error_after_broadcast` (or have `Waitable::wait_for_response` return `StateTransitionResult` so the hash is preserved by construction). As-is the helpers only deliver the hash on the happy path — exactly the gap the rest of the PR was trying to close.

Comment on lines +183 to +195
WaitForStateTransitionResultFailedAfterBroadcast {
transition_hash,
source,
} => Self::new(
WasmSdkErrorKind::WaitForStateTransitionResultFailedAfterBroadcast,
format!(
"state transition broadcast succeeded for {} but waiting for the result failed: {}",
hex::encode(transition_hash),
source
),
None,
retriable,
),
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🟡 Suggestion: WASM error variant flattens the structured transition_hash into a hex-encoded message

Rust preserves the txid as a typed [u8; 32] in Error::WaitForStateTransitionResultFailedAfterBroadcast { transition_hash, source } (rs-sdk/src/error.rs:115-123), which is exactly the recovery affordance this PR set out to provide ("broadcast went out, but waiting failed — query by tx id later"). At the WASM boundary, the variant is mapped to WasmSdkError and the hash is hex::encoded directly into message. WasmSdkError's exported surface (kind, name, message, code, isRetriable, lines 249-313) has no slot for the hash, so JS consumers must regex-parse English text to recover it. Add a structured transitionHash: Option<Vec<u8>> field exposed via a Uint8Array getter (analogous to the existing code field, and mirroring BroadcastAndWaitResultWasm.transitionHash) and populate it for this variant. The success path crosses cleanly; the operationally most-interesting path still does not.

source: ['claude', 'codex']

🤖 Fix this 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/wasm-sdk/src/error.rs`:
- [SUGGESTION] lines 183-195: WASM error variant flattens the structured `transition_hash` into a hex-encoded message
  Rust preserves the txid as a typed `[u8; 32]` in `Error::WaitForStateTransitionResultFailedAfterBroadcast { transition_hash, source }` (`rs-sdk/src/error.rs:115-123`), which is exactly the recovery affordance this PR set out to provide ("broadcast went out, but waiting failed — query by tx id later"). At the WASM boundary, the variant is mapped to `WasmSdkError` and the hash is `hex::encode`d directly into `message`. `WasmSdkError`'s exported surface (`kind`, `name`, `message`, `code`, `isRetriable`, lines 249-313) has no slot for the hash, so JS consumers must regex-parse English text to recover it. Add a structured `transitionHash: Option<Vec<u8>>` field exposed via a `Uint8Array` getter (analogous to the existing `code` field, and mirroring `BroadcastAndWaitResultWasm.transitionHash`) and populate it for this variant. The success path crosses cleanly; the operationally most-interesting path still does not.

Comment on lines 227 to 234

// Broadcast the transition
use dash_sdk::dpp::state_transition::proof_result::StateTransitionProofResult;
// TODO(#2953): The transition hash currently stops at the WASM/JS boundary for
// contract update results; propagate it through the JS-facing API in the follow-up.
state_transition
.broadcast_and_wait::<StateTransitionProofResult>(self.inner_sdk(), settings)
.await?;
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🟡 Suggestion: Higher-level WASM helpers still drop the transition hash before the JS boundary

The leaf WasmSdk::broadcastAndWait correctly forwards the hash, but every higher-level wasm-bindgen entry point still discards it before crossing into JS, with TODO(#2953) comments acknowledging the gap: contractPublish (contract.rs:103-119), contractUpdate (contract.rs:228-234), identityCreate (identity.rs:115-129), identityTopUp (identity.rs:206-220), identityCreditTransfer (identity.rs:338-353), identityCreditWithdrawal (identity.rs:489-503), identityUpdate (identity.rs:697-700). Each of these now has a hash-carrying rs-sdk helper available (the *_with_transition_hash variants and the broadcast_and_wait StateTransitionResult), but JS callers using the ergonomic APIs still cannot recover the txid. Either thread transitionHash through these return values now, or ensure #2953 is filed and linked in the PR description so the work is not lost.

source: ['claude', 'codex']

🤖 Fix this 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/wasm-sdk/src/state_transitions/contract.rs`:
- [SUGGESTION] lines 103-234: Higher-level WASM helpers still drop the transition hash before the JS boundary
  The leaf `WasmSdk::broadcastAndWait` correctly forwards the hash, but every higher-level wasm-bindgen entry point still discards it before crossing into JS, with `TODO(#2953)` comments acknowledging the gap: `contractPublish` (contract.rs:103-119), `contractUpdate` (contract.rs:228-234), `identityCreate` (identity.rs:115-129), `identityTopUp` (identity.rs:206-220), `identityCreditTransfer` (identity.rs:338-353), `identityCreditWithdrawal` (identity.rs:489-503), `identityUpdate` (identity.rs:697-700). Each of these now has a hash-carrying rs-sdk helper available (the `*_with_transition_hash` variants and the `broadcast_and_wait` `StateTransitionResult`), but JS callers using the ergonomic APIs still cannot recover the txid. Either thread `transitionHash` through these return values now, or ensure #2953 is filed and linked in the PR description so the work is not lost.

Comment on lines 271 to +294
async fn broadcast_and_wait<T: TryFrom<StateTransitionProofResult> + Send>(
&self,
sdk: &Sdk,
settings: Option<PutSettings>,
) -> Result<T, Error> {
) -> Result<StateTransitionResult<T>, Error> {
trace!(state_transition = %self.name(), "broadcast_and_wait: start");

// Compute the transition hash deterministically BEFORE broadcast.
// This is a SHA-256 hash of the serialized StateTransition and does
// not depend on blockchain state, so there is no race condition.
let transition_hash = self.transaction_id()?;

trace!("broadcast_and_wait: step 1 - broadcasting");
self.broadcast(sdk, settings).await?;
trace!("broadcast_and_wait: step 2 - waiting for response");
let result = self.wait_for_response::<T>(sdk, settings).await;
let result = wait_for_response_with_hash(self, sdk, settings, transition_hash).await;
match &result {
Ok(_) => trace!("broadcast_and_wait: complete success"),
Err(e) => warn!(error = ?e, "broadcast_and_wait: failed"),
}
result
.map(|inner| StateTransitionResult::new(inner, transition_hash))
.map_err(|e| wrap_wait_error_after_broadcast(transition_hash, e))
}
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🟡 Suggestion: No test asserts broadcast_and_wait's hash equals StateTransition::transaction_id()

The PR's load-bearing invariant is that the hash placed in StateTransitionResult::transition_hash is bit-for-bit equal to StateTransition::transaction_id() of the broadcast transition (the same hash keyed in wait_for_state_transition_result_request_with_hash). The added tests cover (a) the wrapper's mechanical surface with hard-coded [7; 32], (b) the error wrapper, and (c) the wait-request-with-hash builder — none traverse broadcast_and_wait end-to-end via MockSdk to assert result.transition_hash() == st.transaction_id()? and that the wait-failure path produces WaitForStateTransitionResultFailedAfterBroadcast carrying the same hash. With every internal call site immediately calling .into_inner(), a future regression that swaps in or recomputes a different hash post-broadcast would not be caught. The PR checklist still has the test-coverage box unchecked.

source: ['claude', 'codex']

🤖 Fix this 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/src/platform/transition/broadcast.rs`:
- [SUGGESTION] lines 271-294: No test asserts `broadcast_and_wait`'s hash equals `StateTransition::transaction_id()`
  The PR's load-bearing invariant is that the hash placed in `StateTransitionResult::transition_hash` is bit-for-bit equal to `StateTransition::transaction_id()` of the broadcast transition (the same hash keyed in `wait_for_state_transition_result_request_with_hash`). The added tests cover (a) the wrapper's mechanical surface with hard-coded `[7; 32]`, (b) the error wrapper, and (c) the wait-request-with-hash builder — none traverse `broadcast_and_wait` end-to-end via `MockSdk` to assert `result.transition_hash() == st.transaction_id()?` and that the wait-failure path produces `WaitForStateTransitionResultFailedAfterBroadcast` carrying the same hash. With every internal call site immediately calling `.into_inner()`, a future regression that swaps in or recomputes a different hash post-broadcast would not be caught. The PR checklist still has the test-coverage box unchecked.

Comment on lines +20 to +32
#[wasm_bindgen(typescript_custom_section)]
const BROADCAST_AND_WAIT_RESULT_TS: &str = r#"
export interface BroadcastAndWaitResult {
result: StateTransitionProofResultType;
transitionHash: Uint8Array;
}
"#;

#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(typescript_type = "BroadcastAndWaitResult")]
pub type BroadcastAndWaitResultJs;
}
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

💬 Nitpick: BroadcastAndWaitResultJs extern alias and typescript_custom_section are dead code

pub type BroadcastAndWaitResultJs; (line 31) is declared with #[wasm_bindgen(typescript_type = "BroadcastAndWaitResult")] but never appears as a parameter or return type — broadcast_and_wait returns BroadcastAndWaitResultWasm (line 134) which is exported as the JS class BroadcastAndWaitResult via js_class. The typescript_custom_section interface at lines 21-26 then collides with the wasm-bindgen-generated class declaration of the same name; TypeScript only accepts both via class/interface merging while their members agree, and any future getter change has to be manually mirrored in the interface. Either drop both (the class declaration is sufficient) or wire BroadcastAndWaitResultJs into the return type so the typed interface is actually emitted.

source: ['claude']

Comment on lines 289 to 297
self,
Error::DapiClientError(DapiClientError::NoAvailableAddresses)
| Error::DapiClientError(DapiClientError::NoAvailableAddressesToRetry(_))
) || matches!(
self,
Error::WaitForStateTransitionResultFailedAfterBroadcast { source, .. }
if source.is_no_available_addresses()
)
}
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

💬 Nitpick: WaitForStateTransitionResultFailedAfterBroadcast retriability semantics deserve a doc note

Including the new variant in the retriable set means external callers looping on broadcast_and_wait based on can_retry() will retry the whole call after a successful broadcast, re-broadcasting the same transition. Internal retry already happened inside broadcast / wait_for_response_with_hash, so by the time this variant escapes, the wait has exhausted its budget. For nonce-based transitions this just produces AlreadyExists on the next attempt; the recovery use case the variant was designed for is a query-by-transition_hash, not a re-broadcast. Either document that this is intentionally retriable only because the underlying broadcast is idempotent under nonce reuse, or make it non-retriable and expose an explicit wait_again_for(transition_hash) API.

source: ['claude']

Comment on lines +6 to +10
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StateTransitionResult<T> {
inner: T,
transition_hash: [u8; 32],
}
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

💬 Nitpick: StateTransitionResult is not #[must_use]

StateTransitionResult<T> is the public return type of every broadcast_and_wait call and the entire reason this PR exists — callers ignoring the wrapper defeats its purpose. Annotating with #[must_use = "the transition hash and proof result should be inspected"] would catch accidentally dropped returns at the type level. With Deref removed, an ignored return is harder to overlook than it used to be, but let _ = ... still silently throws away the hash.

💡 Suggested change
Suggested change
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StateTransitionResult<T> {
inner: T,
transition_hash: [u8; 32],
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[must_use = "the transition hash and proof result should be inspected"]
pub struct StateTransitionResult<T> {
inner: T,
transition_hash: [u8; 32],
}

source: ['claude']

Comment on lines +46 to +52
/// Maps the inner value, preserving the transition hash.
pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> StateTransitionResult<U> {
StateTransitionResult {
inner: f(self.inner),
transition_hash: self.transition_hash,
}
}
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

💬 Nitpick: StateTransitionResult::map has no in-tree caller

map is defined and unit-tested but unused in production: broadcast_and_wait constructs the wrapper at the leaf, and every consumer calls .into_inner() or .into_parts(). Carrying public surface for hypothetical consumers tends to ossify; either drop it now or wait for a real consumer to need it. Not a defect, just unused surface.

source: ['claude']

thepastaclaw and others added 6 commits May 28, 2026 21:37
Add StateTransitionResult<T> wrapper type that bundles the proof result
with the transition hash (transaction ID). The hash is computed
deterministically from the serialized StateTransition BEFORE broadcast,
avoiding any race condition with blockchain state.

broadcast_and_wait now returns StateTransitionResult<T> instead of T.
The wrapper implements Deref<Target=T> for ergonomic access and provides
into_inner() for callers that only need the result. All existing callers
updated to use .into_inner() to maintain current behavior.

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

- Move state_transition_result module to alphabetical position in transition.rs
- Update wasm-sdk broadcast.rs to unwrap StateTransitionResult with into_inner()
wait_for_response returns T directly, not StateTransitionResult<T>,
so into_inner() is not needed there.
Adapt PR scope to the renamed identity APIs in v3.1-dev:
- put_identity: with_transition_hash wrapper now calls
  put_to_platform_with_private_key (rename of put_to_platform)
- wasm-sdk error: fill new transition_hash arg at remaining call sites
- platform-wallet update_identity_with_signer: unwrap the new
  StateTransitionResult wrapper before returning the proof result
@thepastaclaw thepastaclaw force-pushed the feat/sdk-transition-hash branch from 127ba6e to ace6143 Compare May 29, 2026 02:46
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