diff --git a/packages/rs-sdk/src/platform/documents/transitions/create.rs b/packages/rs-sdk/src/platform/documents/transitions/create.rs index 26c77e4acda..88ff7f6c0c5 100644 --- a/packages/rs-sdk/src/platform/documents/transitions/create.rs +++ b/packages/rs-sdk/src/platform/documents/transitions/create.rs @@ -222,7 +222,8 @@ impl Sdk { let proof_result = state_transition .broadcast_and_wait::(self, put_settings) - .await?; + .await? + .into_inner(); match proof_result { StateTransitionProofResult::VerifiedDocuments(documents) => { diff --git a/packages/rs-sdk/src/platform/documents/transitions/delete.rs b/packages/rs-sdk/src/platform/documents/transitions/delete.rs index 2f1a8c005c3..fcc4b94ec2e 100644 --- a/packages/rs-sdk/src/platform/documents/transitions/delete.rs +++ b/packages/rs-sdk/src/platform/documents/transitions/delete.rs @@ -258,7 +258,8 @@ impl Sdk { let proof_result = state_transition .broadcast_and_wait::(self, put_settings) - .await?; + .await? + .into_inner(); match proof_result { StateTransitionProofResult::VerifiedDocuments(documents) => { diff --git a/packages/rs-sdk/src/platform/documents/transitions/purchase.rs b/packages/rs-sdk/src/platform/documents/transitions/purchase.rs index b7832cb7782..4594c657a0a 100644 --- a/packages/rs-sdk/src/platform/documents/transitions/purchase.rs +++ b/packages/rs-sdk/src/platform/documents/transitions/purchase.rs @@ -275,7 +275,8 @@ impl Sdk { let proof_result = state_transition .broadcast_and_wait::(self, put_settings) - .await?; + .await? + .into_inner(); match proof_result { StateTransitionProofResult::VerifiedDocuments(documents) => { diff --git a/packages/rs-sdk/src/platform/documents/transitions/replace.rs b/packages/rs-sdk/src/platform/documents/transitions/replace.rs index aacfc2f9624..e833b7f0b4b 100644 --- a/packages/rs-sdk/src/platform/documents/transitions/replace.rs +++ b/packages/rs-sdk/src/platform/documents/transitions/replace.rs @@ -220,7 +220,8 @@ impl Sdk { trace!("document_replace: broadcasting and awaiting response"); let proof_result = state_transition .broadcast_and_wait::(self, put_settings) - .await?; + .await? + .into_inner(); trace!("document_replace: broadcast completed"); match proof_result { diff --git a/packages/rs-sdk/src/platform/documents/transitions/set_price.rs b/packages/rs-sdk/src/platform/documents/transitions/set_price.rs index 61316f3b5c5..be7856f0fe3 100644 --- a/packages/rs-sdk/src/platform/documents/transitions/set_price.rs +++ b/packages/rs-sdk/src/platform/documents/transitions/set_price.rs @@ -260,7 +260,8 @@ impl Sdk { let proof_result = state_transition .broadcast_and_wait::(self, put_settings) - .await?; + .await? + .into_inner(); match proof_result { StateTransitionProofResult::VerifiedDocuments(documents) => { diff --git a/packages/rs-sdk/src/platform/documents/transitions/transfer.rs b/packages/rs-sdk/src/platform/documents/transitions/transfer.rs index ae1f2afbb04..8975476a9e6 100644 --- a/packages/rs-sdk/src/platform/documents/transitions/transfer.rs +++ b/packages/rs-sdk/src/platform/documents/transitions/transfer.rs @@ -259,7 +259,8 @@ impl Sdk { let proof_result = state_transition .broadcast_and_wait::(self, put_settings) - .await?; + .await? + .into_inner(); match proof_result { StateTransitionProofResult::VerifiedDocuments(documents) => { diff --git a/packages/rs-sdk/src/platform/tokens/transitions/burn.rs b/packages/rs-sdk/src/platform/tokens/transitions/burn.rs index 0f93cefd223..2631cd51e35 100644 --- a/packages/rs-sdk/src/platform/tokens/transitions/burn.rs +++ b/packages/rs-sdk/src/platform/tokens/transitions/burn.rs @@ -72,7 +72,8 @@ impl Sdk { let proof_result = state_transition .broadcast_and_wait::(self, put_settings) - .await?; + .await? + .into_inner(); match proof_result { StateTransitionProofResult::VerifiedTokenBalance(owner_id, remaining_balance) => { diff --git a/packages/rs-sdk/src/platform/tokens/transitions/claim.rs b/packages/rs-sdk/src/platform/tokens/transitions/claim.rs index f67c182b28d..7ba1fd5d4f1 100644 --- a/packages/rs-sdk/src/platform/tokens/transitions/claim.rs +++ b/packages/rs-sdk/src/platform/tokens/transitions/claim.rs @@ -63,7 +63,8 @@ impl Sdk { let proof_result = state_transition .broadcast_and_wait::(self, put_settings) - .await?; + .await? + .into_inner(); match proof_result { StateTransitionProofResult::VerifiedTokenActionWithDocument(document) => { diff --git a/packages/rs-sdk/src/platform/tokens/transitions/config_update.rs b/packages/rs-sdk/src/platform/tokens/transitions/config_update.rs index 48c1409de79..685587c7b6d 100644 --- a/packages/rs-sdk/src/platform/tokens/transitions/config_update.rs +++ b/packages/rs-sdk/src/platform/tokens/transitions/config_update.rs @@ -64,7 +64,8 @@ impl Sdk { let proof_result = state_transition .broadcast_and_wait::(self, put_settings) - .await?; + .await? + .into_inner(); match proof_result { StateTransitionProofResult::VerifiedTokenActionWithDocument(document) => { diff --git a/packages/rs-sdk/src/platform/tokens/transitions/destroy_frozen_funds.rs b/packages/rs-sdk/src/platform/tokens/transitions/destroy_frozen_funds.rs index 3c3e52ca2dc..59cee4e7903 100644 --- a/packages/rs-sdk/src/platform/tokens/transitions/destroy_frozen_funds.rs +++ b/packages/rs-sdk/src/platform/tokens/transitions/destroy_frozen_funds.rs @@ -62,7 +62,8 @@ impl Sdk { let proof_result = state_transition .broadcast_and_wait::(self, put_settings) - .await?; + .await? + .into_inner(); match proof_result { StateTransitionProofResult::VerifiedTokenActionWithDocument(doc) => { diff --git a/packages/rs-sdk/src/platform/tokens/transitions/direct_purchase.rs b/packages/rs-sdk/src/platform/tokens/transitions/direct_purchase.rs index 5f994c14c50..51306e769b0 100644 --- a/packages/rs-sdk/src/platform/tokens/transitions/direct_purchase.rs +++ b/packages/rs-sdk/src/platform/tokens/transitions/direct_purchase.rs @@ -71,7 +71,8 @@ impl Sdk { let proof_result = state_transition .broadcast_and_wait::(self, put_settings) - .await?; + .await? + .into_inner(); match proof_result { StateTransitionProofResult::VerifiedTokenBalance(owner_id, balance) => { diff --git a/packages/rs-sdk/src/platform/tokens/transitions/emergency_action.rs b/packages/rs-sdk/src/platform/tokens/transitions/emergency_action.rs index bf07779539f..14e1e1f3a17 100644 --- a/packages/rs-sdk/src/platform/tokens/transitions/emergency_action.rs +++ b/packages/rs-sdk/src/platform/tokens/transitions/emergency_action.rs @@ -64,7 +64,8 @@ impl Sdk { let proof_result = state_transition .broadcast_and_wait::(self, put_settings) - .await?; + .await? + .into_inner(); match proof_result { StateTransitionProofResult::VerifiedTokenActionWithDocument(document) => { diff --git a/packages/rs-sdk/src/platform/tokens/transitions/freeze.rs b/packages/rs-sdk/src/platform/tokens/transitions/freeze.rs index 2fb57253f66..efdbec420b0 100644 --- a/packages/rs-sdk/src/platform/tokens/transitions/freeze.rs +++ b/packages/rs-sdk/src/platform/tokens/transitions/freeze.rs @@ -71,8 +71,8 @@ impl Sdk { let proof_result = state_transition .broadcast_and_wait::(self, put_settings) - .await?; - + .await? + .into_inner(); match proof_result { StateTransitionProofResult::VerifiedTokenIdentityInfo(owner_id_result, info) => { Ok(FreezeResult::IdentityInfo(owner_id_result, info)) diff --git a/packages/rs-sdk/src/platform/tokens/transitions/mint.rs b/packages/rs-sdk/src/platform/tokens/transitions/mint.rs index 207a3f33cb9..a4a2acd1c02 100644 --- a/packages/rs-sdk/src/platform/tokens/transitions/mint.rs +++ b/packages/rs-sdk/src/platform/tokens/transitions/mint.rs @@ -71,8 +71,8 @@ impl Sdk { let proof_result = state_transition .broadcast_and_wait::(self, put_settings) - .await?; - + .await? + .into_inner(); match proof_result { StateTransitionProofResult::VerifiedTokenBalance(recipient_id_result, new_balance) => { Ok(MintResult::TokenBalance(recipient_id_result, new_balance)) diff --git a/packages/rs-sdk/src/platform/tokens/transitions/set_price_for_direct_purchase.rs b/packages/rs-sdk/src/platform/tokens/transitions/set_price_for_direct_purchase.rs index 578b2683441..7a6cd5ce79f 100644 --- a/packages/rs-sdk/src/platform/tokens/transitions/set_price_for_direct_purchase.rs +++ b/packages/rs-sdk/src/platform/tokens/transitions/set_price_for_direct_purchase.rs @@ -76,8 +76,8 @@ impl Sdk { let proof_result = state_transition .broadcast_and_wait::(self, put_settings) - .await?; - + .await? + .into_inner(); match proof_result { StateTransitionProofResult::VerifiedTokenPricingSchedule(owner_id, schedule) => { Ok(SetPriceResult::PricingSchedule(owner_id, schedule)) diff --git a/packages/rs-sdk/src/platform/tokens/transitions/transfer.rs b/packages/rs-sdk/src/platform/tokens/transitions/transfer.rs index 4cb03fe5985..99f0e1524ea 100644 --- a/packages/rs-sdk/src/platform/tokens/transitions/transfer.rs +++ b/packages/rs-sdk/src/platform/tokens/transitions/transfer.rs @@ -69,7 +69,8 @@ impl Sdk { let proof_result = state_transition .broadcast_and_wait::(self, put_settings) - .await?; + .await? + .into_inner(); match proof_result { StateTransitionProofResult::VerifiedTokenIdentitiesBalances(balances) => { diff --git a/packages/rs-sdk/src/platform/tokens/transitions/unfreeze.rs b/packages/rs-sdk/src/platform/tokens/transitions/unfreeze.rs index ed68ca5eefd..248ca3fc2fa 100644 --- a/packages/rs-sdk/src/platform/tokens/transitions/unfreeze.rs +++ b/packages/rs-sdk/src/platform/tokens/transitions/unfreeze.rs @@ -70,8 +70,8 @@ impl Sdk { let proof_result = state_transition .broadcast_and_wait::(self, put_settings) - .await?; - + .await? + .into_inner(); match proof_result { StateTransitionProofResult::VerifiedTokenIdentityInfo(owner_id_result, info) => { Ok(UnfreezeResult::IdentityInfo(owner_id_result, info)) diff --git a/packages/rs-sdk/src/platform/transition.rs b/packages/rs-sdk/src/platform/transition.rs index e477750f5eb..f0573ec1136 100644 --- a/packages/rs-sdk/src/platform/transition.rs +++ b/packages/rs-sdk/src/platform/transition.rs @@ -9,6 +9,7 @@ pub mod put_contract; pub mod put_document; pub mod put_identity; pub mod put_settings; +pub mod state_transition_result; pub mod top_up_address; pub mod top_up_identity; pub mod top_up_identity_from_addresses; @@ -22,4 +23,5 @@ pub(crate) mod validation; pub mod vote; pub mod waitable; pub mod withdraw_from_identity; +pub use state_transition_result::StateTransitionResult; pub use txid::TxId; diff --git a/packages/rs-sdk/src/platform/transition/address_credit_withdrawal.rs b/packages/rs-sdk/src/platform/transition/address_credit_withdrawal.rs index ea28bf89408..234c197b0b9 100644 --- a/packages/rs-sdk/src/platform/transition/address_credit_withdrawal.rs +++ b/packages/rs-sdk/src/platform/transition/address_credit_withdrawal.rs @@ -110,6 +110,7 @@ impl> WithdrawAddressFunds for Sdk { match state_transition .broadcast_and_wait::(self, settings) .await? + .into_inner() { StateTransitionProofResult::VerifiedAddressInfos(address_infos_map) => { let mut expected_addresses: BTreeSet = diff --git a/packages/rs-sdk/src/platform/transition/broadcast.rs b/packages/rs-sdk/src/platform/transition/broadcast.rs index 21b784b3b3c..d8a58490e30 100644 --- a/packages/rs-sdk/src/platform/transition/broadcast.rs +++ b/packages/rs-sdk/src/platform/transition/broadcast.rs @@ -1,5 +1,6 @@ use super::broadcast_request::BroadcastRequestForStateTransition; use super::put_settings::PutSettings; +use super::state_transition_result::StateTransitionResult; use crate::error::StateTransitionBroadcastError; use crate::platform::block_info_from_metadata::block_info_from_metadata; use crate::sync::retry; @@ -30,7 +31,7 @@ pub trait BroadcastStateTransition { &self, sdk: &Sdk, settings: Option, - ) -> Result; + ) -> Result, Error>; } #[async_trait::async_trait] @@ -264,8 +265,14 @@ impl BroadcastStateTransition for StateTransition { &self, sdk: &Sdk, settings: Option, - ) -> Result { + ) -> Result, 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"); @@ -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)) } } diff --git a/packages/rs-sdk/src/platform/transition/put_identity.rs b/packages/rs-sdk/src/platform/transition/put_identity.rs index 42b233efe5b..8014255d66d 100644 --- a/packages/rs-sdk/src/platform/transition/put_identity.rs +++ b/packages/rs-sdk/src/platform/transition/put_identity.rs @@ -184,6 +184,7 @@ async fn put_identity_with_address_funding< match state_transition .broadcast_and_wait::(sdk, settings) .await? + .into_inner() { StateTransitionProofResult::VerifiedIdentityFullWithAddressInfos( proved_identity, diff --git a/packages/rs-sdk/src/platform/transition/state_transition_result.rs b/packages/rs-sdk/src/platform/transition/state_transition_result.rs new file mode 100644 index 00000000000..51e1b8cb556 --- /dev/null +++ b/packages/rs-sdk/src/platform/transition/state_transition_result.rs @@ -0,0 +1,56 @@ +use std::ops::Deref; + +/// Wrapper that bundles a state transition proof result with the transition hash. +/// +/// The transition hash (also known as the transaction ID) is computed deterministically +/// from the serialized `StateTransition` before broadcast, so it does not depend on +/// blockchain state and there is no race condition. +/// +/// `StateTransitionResult` implements `Deref`, so existing code that +/// only needs the inner result can use it transparently. +#[derive(Debug, Clone)] +pub struct StateTransitionResult { + inner: T, + transition_hash: [u8; 32], +} + +impl StateTransitionResult { + /// Creates a new result bundling the proof result with the transition hash. + pub fn new(inner: T, transition_hash: [u8; 32]) -> Self { + Self { + inner, + transition_hash, + } + } + + /// Returns the transition hash (transaction ID) as a 32-byte array. + pub fn transition_hash(&self) -> [u8; 32] { + self.transition_hash + } + + /// Consumes this wrapper, returning the inner result and the transition hash. + pub fn into_parts(self) -> (T, [u8; 32]) { + (self.inner, self.transition_hash) + } + + /// Consumes this wrapper, returning just the inner result. + pub fn into_inner(self) -> T { + self.inner + } + + /// Maps the inner value, preserving the transition hash. + pub fn map U>(self, f: F) -> StateTransitionResult { + StateTransitionResult { + inner: f(self.inner), + transition_hash: self.transition_hash, + } + } +} + +impl Deref for StateTransitionResult { + type Target = T; + + fn deref(&self) -> &T { + &self.inner + } +} diff --git a/packages/rs-sdk/src/platform/transition/top_up_address.rs b/packages/rs-sdk/src/platform/transition/top_up_address.rs index 097b7685d00..bdef88e98a2 100644 --- a/packages/rs-sdk/src/platform/transition/top_up_address.rs +++ b/packages/rs-sdk/src/platform/transition/top_up_address.rs @@ -99,7 +99,8 @@ impl> TopUpAddress for AddressesWithBalances { ensure_valid_state_transition_structure(&state_transition, sdk.version())?; let st_result = state_transition .broadcast_and_wait::(sdk, settings) - .await?; + .await? + .into_inner(); match st_result { StateTransitionProofResult::VerifiedAddressInfos(address_infos) => { let expected_addresses = diff --git a/packages/rs-sdk/src/platform/transition/top_up_identity.rs b/packages/rs-sdk/src/platform/transition/top_up_identity.rs index 422c20c4553..09393b3b2a9 100644 --- a/packages/rs-sdk/src/platform/transition/top_up_identity.rs +++ b/packages/rs-sdk/src/platform/transition/top_up_identity.rs @@ -40,7 +40,10 @@ impl TopUpIdentity for Identity { None, )?; ensure_valid_state_transition_structure(&state_transition, sdk.version())?; - let identity: PartialIdentity = state_transition.broadcast_and_wait(sdk, settings).await?; + let identity: PartialIdentity = state_transition + .broadcast_and_wait::(sdk, settings) + .await? + .into_inner(); identity .balance diff --git a/packages/rs-sdk/src/platform/transition/top_up_identity_from_addresses.rs b/packages/rs-sdk/src/platform/transition/top_up_identity_from_addresses.rs index e0a61362a05..c162bbfd256 100644 --- a/packages/rs-sdk/src/platform/transition/top_up_identity_from_addresses.rs +++ b/packages/rs-sdk/src/platform/transition/top_up_identity_from_addresses.rs @@ -84,6 +84,7 @@ impl> TopUpIdentityFromAddresses for Identity { match state_transition .broadcast_and_wait::(sdk, settings) .await? + .into_inner() { StateTransitionProofResult::VerifiedIdentityWithAddressInfos( identity, diff --git a/packages/rs-sdk/src/platform/transition/transfer.rs b/packages/rs-sdk/src/platform/transition/transfer.rs index 77d385fe928..c9ce0b517b8 100644 --- a/packages/rs-sdk/src/platform/transition/transfer.rs +++ b/packages/rs-sdk/src/platform/transition/transfer.rs @@ -62,8 +62,10 @@ impl TransferToIdentity for Identity { )?; ensure_valid_state_transition_structure(&state_transition, sdk.version())?; - let (sender, receiver): (PartialIdentity, PartialIdentity) = - state_transition.broadcast_and_wait(sdk, settings).await?; + let (sender, receiver): (PartialIdentity, PartialIdentity) = state_transition + .broadcast_and_wait::<(PartialIdentity, PartialIdentity)>(sdk, settings) + .await? + .into_inner(); let sender_balance = sender.balance.ok_or_else(|| { Error::Generic("expected an identity balance after transfer (sender)".to_string()) diff --git a/packages/rs-sdk/src/platform/transition/transfer_address_funds.rs b/packages/rs-sdk/src/platform/transition/transfer_address_funds.rs index 4990ba59edd..fc550f78c4f 100644 --- a/packages/rs-sdk/src/platform/transition/transfer_address_funds.rs +++ b/packages/rs-sdk/src/platform/transition/transfer_address_funds.rs @@ -96,6 +96,7 @@ impl> TransferAddressFunds for Sdk { match state_transition .broadcast_and_wait::(self, settings) .await? + .into_inner() { StateTransitionProofResult::VerifiedAddressInfos(address_infos_map) => { collect_address_infos_from_proof(address_infos_map, &expected_addresses) diff --git a/packages/rs-sdk/src/platform/transition/transfer_to_addresses.rs b/packages/rs-sdk/src/platform/transition/transfer_to_addresses.rs index d1eec49ab7d..01ee4cb7a95 100644 --- a/packages/rs-sdk/src/platform/transition/transfer_to_addresses.rs +++ b/packages/rs-sdk/src/platform/transition/transfer_to_addresses.rs @@ -75,6 +75,7 @@ impl TransferToAddresses for Identity { match state_transition .broadcast_and_wait::(sdk, settings) .await? + .into_inner() { StateTransitionProofResult::VerifiedIdentityWithAddressInfos( identity, diff --git a/packages/rs-sdk/src/platform/transition/withdraw_from_identity.rs b/packages/rs-sdk/src/platform/transition/withdraw_from_identity.rs index 841c04c0fbb..bbd1697fb8f 100644 --- a/packages/rs-sdk/src/platform/transition/withdraw_from_identity.rs +++ b/packages/rs-sdk/src/platform/transition/withdraw_from_identity.rs @@ -65,7 +65,10 @@ impl WithdrawFromIdentity for Identity { )?; ensure_valid_state_transition_structure(&state_transition, sdk.version())?; - let result = state_transition.broadcast_and_wait(sdk, settings).await?; + let result = state_transition + .broadcast_and_wait::(sdk, settings) + .await? + .into_inner(); match result { StateTransitionProofResult::VerifiedPartialIdentity(identity) => { diff --git a/packages/wasm-sdk/src/state_transitions/broadcast.rs b/packages/wasm-sdk/src/state_transitions/broadcast.rs index 6e4a6a279f2..93159a7d53b 100644 --- a/packages/wasm-sdk/src/state_transitions/broadcast.rs +++ b/packages/wasm-sdk/src/state_transitions/broadcast.rs @@ -96,6 +96,6 @@ impl WasmSdk { .await .map_err(|e| WasmSdkError::generic(format!("Failed to broadcast: {}", e)))?; - convert_proof_result(result).map_err(WasmSdkError::from) + convert_proof_result(result.into_inner()).map_err(WasmSdkError::from) } }