From ad0979c34f24ba918dd9954401fd46568d8144c0 Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Wed, 18 Feb 2026 06:22:30 -0600 Subject: [PATCH] feat(sdk): add client-side validate_structure() to remaining state transitions Add client-side structure validation to 6 state transition SDK construction methods, following the pattern established in PR #3096. This ensures invalid transitions are caught early on the client side before being submitted. State transitions updated: - AddressCreditWithdrawalTransition - AddressFundingFromAssetLockTransition - AddressFundsTransferTransition - IdentityCreateFromAddressesTransition - IdentityCreditTransferToAddressesTransition - IdentityTopUpFromAddressesTransition Co-Authored-By: Claude Opus 4.6 --- .../v0/v0_methods.rs | 12 +++++++++++- .../v0/v0_methods.rs | 11 ++++++++++- .../v0/v0_methods.rs | 11 ++++++++++- .../v0/v0_methods.rs | 12 +++++++++++- .../v0/v0_methods.rs | 17 ++++++++++++++--- .../v0/v0_methods.rs | 11 ++++++++++- 6 files changed, 66 insertions(+), 8 deletions(-) diff --git a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/v0_methods.rs index 2aa4e223bba..30dd8644697 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/v0_methods.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/v0_methods.rs @@ -14,6 +14,8 @@ use crate::serialization::Signable; use crate::state_transition::address_credit_withdrawal_transition::methods::AddressCreditWithdrawalTransitionMethodsV0; use crate::state_transition::address_credit_withdrawal_transition::v0::AddressCreditWithdrawalTransitionV0; #[cfg(feature = "state-transition-signing")] +use crate::state_transition::StateTransitionStructureValidation; +#[cfg(feature = "state-transition-signing")] use crate::withdrawal::Pooling; #[cfg(feature = "state-transition-signing")] use crate::{ @@ -35,7 +37,7 @@ impl AddressCreditWithdrawalTransitionMethodsV0 for AddressCreditWithdrawalTrans output_script: CoreScript, signer: &S, user_fee_increase: UserFeeIncrease, - _platform_version: &PlatformVersion, + platform_version: &PlatformVersion, ) -> Result { tracing::debug!("try_from_inputs_with_signer: Started"); tracing::debug!( @@ -66,6 +68,14 @@ impl AddressCreditWithdrawalTransitionMethodsV0 for AddressCreditWithdrawalTrans .map(|address| signer.sign_create_witness(address, &signable_bytes)) .collect::, ProtocolError>>()?; + // Validate the fully-constructed transition structure + let validation_result = + address_credit_withdrawal_transition.validate_structure(platform_version); + if !validation_result.is_valid() { + let first_error = validation_result.errors.into_iter().next().unwrap(); + return Err(ProtocolError::ConsensusError(Box::new(first_error))); + } + tracing::debug!("try_from_inputs_with_signer: Successfully created transition"); Ok(address_credit_withdrawal_transition.into()) } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/v0/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/v0/v0_methods.rs index 33a824521b4..4e0193ed609 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/v0/v0_methods.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/v0/v0_methods.rs @@ -14,6 +14,8 @@ use crate::serialization::Signable; use crate::state_transition::address_funding_from_asset_lock_transition::methods::AddressFundingFromAssetLockTransitionMethodsV0; use crate::state_transition::address_funding_from_asset_lock_transition::v0::AddressFundingFromAssetLockTransitionV0; #[cfg(feature = "state-transition-signing")] +use crate::state_transition::StateTransitionStructureValidation; +#[cfg(feature = "state-transition-signing")] use crate::{prelude::UserFeeIncrease, state_transition::StateTransition, ProtocolError}; #[cfg(feature = "state-transition-signing")] use dashcore::signer; @@ -30,7 +32,7 @@ impl AddressFundingFromAssetLockTransitionMethodsV0 for AddressFundingFromAssetL fee_strategy: AddressFundsFeeStrategy, signer: &S, user_fee_increase: UserFeeIncrease, - _platform_version: &PlatformVersion, + platform_version: &PlatformVersion, ) -> Result { tracing::debug!("try_from_asset_lock_with_signer: Started"); tracing::debug!( @@ -64,6 +66,13 @@ impl AddressFundingFromAssetLockTransitionMethodsV0 for AddressFundingFromAssetL .map(|address| signer.sign_create_witness(address, &signable_bytes)) .collect::, ProtocolError>>()?; + // Validate the fully-constructed transition structure + let validation_result = address_funding_transition.validate_structure(platform_version); + if !validation_result.is_valid() { + let first_error = validation_result.errors.into_iter().next().unwrap(); + return Err(ProtocolError::ConsensusError(Box::new(first_error))); + } + tracing::debug!("try_from_asset_lock_with_signer: Successfully created transition"); Ok(address_funding_transition.into()) } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funds_transfer_transition/v0/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funds_transfer_transition/v0/v0_methods.rs index a7aa6396e34..a412dfc906f 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funds_transfer_transition/v0/v0_methods.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funds_transfer_transition/v0/v0_methods.rs @@ -12,6 +12,8 @@ use crate::serialization::Signable; use crate::state_transition::address_funds_transfer_transition::methods::AddressFundsTransferTransitionMethodsV0; use crate::state_transition::address_funds_transfer_transition::v0::AddressFundsTransferTransitionV0; #[cfg(feature = "state-transition-signing")] +use crate::state_transition::StateTransitionStructureValidation; +#[cfg(feature = "state-transition-signing")] use crate::{ prelude::{AddressNonce, UserFeeIncrease}, state_transition::StateTransition, @@ -28,7 +30,7 @@ impl AddressFundsTransferTransitionMethodsV0 for AddressFundsTransferTransitionV fee_strategy: AddressFundsFeeStrategy, signer: &S, user_fee_increase: UserFeeIncrease, - _platform_version: &PlatformVersion, + platform_version: &PlatformVersion, ) -> Result { tracing::debug!("try_from_inputs_with_signer: Started"); tracing::debug!( @@ -55,6 +57,13 @@ impl AddressFundsTransferTransitionMethodsV0 for AddressFundsTransferTransitionV .map(|address| signer.sign_create_witness(address, &signable_bytes)) .collect::, ProtocolError>>()?; + // Validate the fully-constructed transition structure + let validation_result = address_funds_transition.validate_structure(platform_version); + if !validation_result.is_valid() { + let first_error = validation_result.errors.into_iter().next().unwrap(); + return Err(ProtocolError::ConsensusError(Box::new(first_error))); + } + tracing::debug!("try_from_inputs_with_signer: Successfully created transition"); Ok(address_funds_transition.into()) } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/v0/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/v0/v0_methods.rs index f668b42c0cd..e323fb31828 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/v0/v0_methods.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/v0/v0_methods.rs @@ -20,6 +20,8 @@ use crate::state_transition::StateTransitionType; // Crate: Feature-Gated (state-transition-signing) // ============================ #[cfg(feature = "state-transition-signing")] +use crate::state_transition::StateTransitionStructureValidation; +#[cfg(feature = "state-transition-signing")] use crate::{ address_funds::AddressFundsFeeStrategy, identity::{ @@ -46,7 +48,7 @@ impl IdentityCreateFromAddressesTransitionMethodsV0 for IdentityCreateFromAddres identity_public_key_signer: &S, address_signer: &WS, user_fee_increase: UserFeeIncrease, - _platform_version: &PlatformVersion, + platform_version: &PlatformVersion, ) -> Result { // Create the unsigned transition let mut identity_create_from_addresses_transition = @@ -90,6 +92,14 @@ impl IdentityCreateFromAddressesTransitionMethodsV0 for IdentityCreateFromAddres .map(|address| address_signer.sign_create_witness(address, &signable_bytes)) .collect::, ProtocolError>>()?; + // Validate the fully-constructed transition structure + let validation_result = + identity_create_from_addresses_transition.validate_structure(platform_version); + if !validation_result.is_valid() { + let first_error = validation_result.errors.into_iter().next().unwrap(); + return Err(ProtocolError::ConsensusError(Box::new(first_error))); + } + Ok(identity_create_from_addresses_transition.into()) } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/v0/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/v0/v0_methods.rs index 6827f46d772..0f2bcb1e883 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/v0/v0_methods.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/v0/v0_methods.rs @@ -6,6 +6,8 @@ use crate::address_funds::PlatformAddress; #[cfg(feature = "state-transition-signing")] use crate::fee::Credits; #[cfg(feature = "state-transition-signing")] +use crate::state_transition::StateTransitionStructureValidation; +#[cfg(feature = "state-transition-signing")] use crate::{ identity::{ accessors::IdentityGettersV0, @@ -35,22 +37,31 @@ impl IdentityCreditTransferToAddressesTransitionMethodsV0 signer: &S, signing_withdrawal_key_to_use: Option<&IdentityPublicKey>, nonce: IdentityNonce, - _platform_version: &PlatformVersion, + platform_version: &PlatformVersion, _version: Option, ) -> Result { tracing::debug!("try_from_identity: Started"); tracing::debug!(identity_id = %identity.id(), "try_from_identity"); tracing::debug!(recipient_addresses = ?to_recipient_addresses, has_signing_key = signing_withdrawal_key_to_use.is_some(), "try_from_identity inputs"); - let mut transition: StateTransition = IdentityCreditTransferToAddressesTransitionV0 { + let transition_v0 = IdentityCreditTransferToAddressesTransitionV0 { identity_id: identity.id(), recipient_addresses: to_recipient_addresses, nonce, user_fee_increase, signature_public_key_id: 0, signature: Default::default(), + }; + + // Validate structure before .into() conversion and signing, since this transition + // uses sign_external on the StateTransition rather than setting witnesses on the V0 struct. + let validation_result = transition_v0.validate_structure(platform_version); + if !validation_result.is_valid() { + let first_error = validation_result.errors.into_iter().next().unwrap(); + return Err(ProtocolError::ConsensusError(Box::new(first_error))); } - .into(); + + let mut transition: StateTransition = transition_v0.into(); let identity_public_key = match signing_withdrawal_key_to_use { Some(key) => { diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_from_addresses_transition/v0/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_from_addresses_transition/v0/v0_methods.rs index 15aa6deba70..71eb89745f2 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_from_addresses_transition/v0/v0_methods.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_from_addresses_transition/v0/v0_methods.rs @@ -19,6 +19,7 @@ use { prelude::{AddressNonce, UserFeeIncrease}, serialization::Signable, state_transition::StateTransition, + state_transition::StateTransitionStructureValidation, version::FeatureVersion, ProtocolError, }, @@ -33,7 +34,7 @@ impl IdentityTopUpFromAddressesTransitionMethodsV0 for IdentityTopUpFromAddresse inputs: BTreeMap, signer: &S, user_fee_increase: UserFeeIncrease, - _platform_version: &PlatformVersion, + platform_version: &PlatformVersion, _version: Option, ) -> Result { let mut identity_top_up_from_addresses_transition = @@ -58,6 +59,14 @@ impl IdentityTopUpFromAddressesTransitionMethodsV0 for IdentityTopUpFromAddresse .map(|address| signer.sign_create_witness(address, &signable_bytes)) .collect::, ProtocolError>>()?; + // Validate the fully-constructed transition structure + let validation_result = + identity_top_up_from_addresses_transition.validate_structure(platform_version); + if !validation_result.is_valid() { + let first_error = validation_result.errors.into_iter().next().unwrap(); + return Err(ProtocolError::ConsensusError(Box::new(first_error))); + } + Ok(identity_top_up_from_addresses_transition.into()) } }