Skip to content

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

Draft
thepastaclaw wants to merge 4 commits intodashpay:v3.1-devfrom
thepastaclaw:feat/sdk-transition-hash
Draft

feat(rs-sdk): expose transition hash from state transition methods#3092
thepastaclaw wants to merge 4 commits intodashpay: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

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.

Files modified (30 files)

  • broadcast.rs — core trait change
  • 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

How Has This Been Tested?

  • cargo check -p dash-sdk passes
  • All changes are type-level — existing code using into_inner() or Deref works unchanged

Validation

Build verification

  • cargo check -p dash-sdk — confirms the new StateTransitionResult<T> type and all 28 updated method signatures compile correctly
  • cargo check -p wasm-sdk — confirms the WASM SDK consumer compiles against the updated broadcast_and_wait return type (uses into_inner() to unwrap)

Type-level correctness

  • The change is purely additive at the type level: broadcast_and_wait returns StateTransitionResult<T> instead of T, and the wrapper implements Deref<Target=T>, so all existing call sites that access T methods continue to work without modification
  • All 28 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

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 of the 30 changed files to confirm consistent pattern: each transition method wraps its broadcast result in StateTransitionResult and surfaces it through the return type
  • 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: 0168c36d-062c-4702-ba7d-8166a93a5a93

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
@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 from 261c79f to 28721d3 Compare March 12, 2026 17:48
thepastaclaw and others added 3 commits March 12, 2026 15:17
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.
@thepastaclaw thepastaclaw force-pushed the feat/sdk-transition-hash branch 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
Comment thread packages/rs-sdk/src/platform/transition/state_transition_result.rs
Comment thread packages/rs-sdk/src/platform/transition/state_transition_result.rs
Comment thread packages/rs-sdk/src/platform/transition/state_transition_result.rs
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 complete (commit ffa5609)

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

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.

3 participants