diff --git a/.gitignore b/.gitignore index 897406169e4..d1840921176 100644 --- a/.gitignore +++ b/.gitignore @@ -85,3 +85,4 @@ packages/wasm-sdk/extracted_definitions.json # gRPC coverage report grpc-coverage-report.txt +worktrees/ diff --git a/packages/rs-sdk/Cargo.toml b/packages/rs-sdk/Cargo.toml index 41ffae82784..6252cce415e 100644 --- a/packages/rs-sdk/Cargo.toml +++ b/packages/rs-sdk/Cargo.toml @@ -9,6 +9,7 @@ arc-swap = { version = "1.7.1" } chrono = { version = "0.4.38" } dpp = { path = "../rs-dpp", default-features = false, features = [ "dash-sdk-features", + "validation", ] } dapi-grpc = { path = "../dapi-grpc", default-features = false } rs-dapi-client = { path = "../rs-dapi-client", default-features = false } diff --git a/packages/rs-sdk/src/platform/documents/transitions/create.rs b/packages/rs-sdk/src/platform/documents/transitions/create.rs index 26c77e4acda..ed529bc9854 100644 --- a/packages/rs-sdk/src/platform/documents/transitions/create.rs +++ b/packages/rs-sdk/src/platform/documents/transitions/create.rs @@ -167,6 +167,25 @@ impl DocumentCreateTransitionBuilder { self.state_transition_creation_options, )?; + // Validate the transition structure before returning + let validation_result = match &state_transition { + StateTransition::Batch(batch_transition) => { + batch_transition.validate_base_structure(platform_version)? + } + _ => { + return Err(Error::Protocol( + dpp::ProtocolError::InvalidStateTransitionType( + "expected Batch transition".to_string(), + ), + )); + } + }; + if let Some(first_error) = validation_result.errors.into_iter().next() { + return Err(Error::Protocol(dpp::ProtocolError::ConsensusError( + Box::new(first_error), + ))); + } + Ok(state_transition) } } @@ -249,3 +268,11 @@ impl Sdk { } } } + +#[cfg(test)] +mod validation_tests { + #[test] + fn validate_base_structure_error_case() { + super::super::tests::assert_document_create_validate_base_structure_error(); + } +} diff --git a/packages/rs-sdk/src/platform/documents/transitions/delete.rs b/packages/rs-sdk/src/platform/documents/transitions/delete.rs index 2f1a8c005c3..50f491bcbd1 100644 --- a/packages/rs-sdk/src/platform/documents/transitions/delete.rs +++ b/packages/rs-sdk/src/platform/documents/transitions/delete.rs @@ -207,6 +207,25 @@ impl DocumentDeleteTransitionBuilder { self.state_transition_creation_options, )?; + // Validate the transition structure before returning + let validation_result = match &state_transition { + StateTransition::Batch(batch_transition) => { + batch_transition.validate_base_structure(platform_version)? + } + _ => { + return Err(Error::Protocol( + dpp::ProtocolError::InvalidStateTransitionType( + "expected Batch transition".to_string(), + ), + )); + } + }; + if let Some(first_error) = validation_result.errors.into_iter().next() { + return Err(Error::Protocol(dpp::ProtocolError::ConsensusError( + Box::new(first_error), + ))); + } + Ok(state_transition) } } @@ -285,3 +304,11 @@ impl Sdk { } } } + +#[cfg(test)] +mod validation_tests { + #[test] + fn validate_base_structure_error_case() { + super::super::tests::assert_document_delete_validate_base_structure_error(); + } +} diff --git a/packages/rs-sdk/src/platform/documents/transitions/mod.rs b/packages/rs-sdk/src/platform/documents/transitions/mod.rs index 200a2164267..a93fda02beb 100644 --- a/packages/rs-sdk/src/platform/documents/transitions/mod.rs +++ b/packages/rs-sdk/src/platform/documents/transitions/mod.rs @@ -3,6 +3,8 @@ pub mod delete; pub mod purchase; pub mod replace; pub mod set_price; +#[cfg(test)] +mod tests; pub mod transfer; pub use create::{DocumentCreateResult, DocumentCreateTransitionBuilder}; diff --git a/packages/rs-sdk/src/platform/documents/transitions/purchase.rs b/packages/rs-sdk/src/platform/documents/transitions/purchase.rs index b7832cb7782..d34f70e9975 100644 --- a/packages/rs-sdk/src/platform/documents/transitions/purchase.rs +++ b/packages/rs-sdk/src/platform/documents/transitions/purchase.rs @@ -221,6 +221,25 @@ impl DocumentPurchaseTransitionBuilder { self.state_transition_creation_options, )?; + // Validate the transition structure before returning + let validation_result = match &state_transition { + StateTransition::Batch(batch_transition) => { + batch_transition.validate_base_structure(platform_version)? + } + _ => { + return Err(Error::Protocol( + dpp::ProtocolError::InvalidStateTransitionType( + "expected Batch transition".to_string(), + ), + )); + } + }; + if let Some(first_error) = validation_result.errors.into_iter().next() { + return Err(Error::Protocol(dpp::ProtocolError::ConsensusError( + Box::new(first_error), + ))); + } + Ok(state_transition) } } @@ -302,3 +321,11 @@ impl Sdk { } } } + +#[cfg(test)] +mod validation_tests { + #[test] + fn validate_base_structure_error_case() { + super::super::tests::assert_document_purchase_validate_base_structure_error(); + } +} diff --git a/packages/rs-sdk/src/platform/documents/transitions/replace.rs b/packages/rs-sdk/src/platform/documents/transitions/replace.rs index aacfc2f9624..77b510088e1 100644 --- a/packages/rs-sdk/src/platform/documents/transitions/replace.rs +++ b/packages/rs-sdk/src/platform/documents/transitions/replace.rs @@ -160,6 +160,25 @@ impl DocumentReplaceTransitionBuilder { self.state_transition_creation_options, )?; + // Validate the transition structure before returning + let validation_result = match &state_transition { + StateTransition::Batch(batch_transition) => { + batch_transition.validate_base_structure(platform_version)? + } + _ => { + return Err(Error::Protocol( + dpp::ProtocolError::InvalidStateTransitionType( + "expected Batch transition".to_string(), + ), + )); + } + }; + if let Some(first_error) = validation_result.errors.into_iter().next() { + return Err(Error::Protocol(dpp::ProtocolError::ConsensusError( + Box::new(first_error), + ))); + } + Ok(state_transition) } } @@ -248,3 +267,11 @@ impl Sdk { } } } + +#[cfg(test)] +mod validation_tests { + #[test] + fn validate_base_structure_error_case() { + super::super::tests::assert_document_replace_validate_base_structure_error(); + } +} 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..cda735f7c5d 100644 --- a/packages/rs-sdk/src/platform/documents/transitions/set_price.rs +++ b/packages/rs-sdk/src/platform/documents/transitions/set_price.rs @@ -208,6 +208,25 @@ impl DocumentSetPriceTransitionBuilder { self.state_transition_creation_options, )?; + // Validate the transition structure before returning + let validation_result = match &state_transition { + StateTransition::Batch(batch_transition) => { + batch_transition.validate_base_structure(platform_version)? + } + _ => { + return Err(Error::Protocol( + dpp::ProtocolError::InvalidStateTransitionType( + "expected Batch transition".to_string(), + ), + )); + } + }; + if let Some(first_error) = validation_result.errors.into_iter().next() { + return Err(Error::Protocol(dpp::ProtocolError::ConsensusError( + Box::new(first_error), + ))); + } + Ok(state_transition) } } @@ -286,3 +305,11 @@ impl Sdk { } } } + +#[cfg(test)] +mod validation_tests { + #[test] + fn validate_base_structure_error_case() { + super::super::tests::assert_document_set_price_validate_base_structure_error(); + } +} diff --git a/packages/rs-sdk/src/platform/documents/transitions/tests.rs b/packages/rs-sdk/src/platform/documents/transitions/tests.rs new file mode 100644 index 00000000000..dd88faaa80a --- /dev/null +++ b/packages/rs-sdk/src/platform/documents/transitions/tests.rs @@ -0,0 +1,364 @@ +use super::delete::DocumentDeleteTransitionBuilder; +use crate::{Error, Sdk, SdkBuilder}; +use dpp::address_funds::AddressWitness; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::config::DataContractConfig; +use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; +use dpp::data_contract::document_type::DocumentType; +use dpp::data_contract::DataContractFactory; +use dpp::document::{Document, DocumentV0}; +use dpp::identity::identity_public_key::v0::IdentityPublicKeyV0; +use dpp::identity::signer::Signer; +use dpp::identity::{IdentityPublicKey, KeyType, Purpose, SecurityLevel}; +use dpp::platform_value::{platform_value, BinaryData, Value}; +use dpp::prelude::Identifier; +use dpp::state_transition::batch_transition::methods::v0::DocumentsBatchTransitionMethodsV0; +use dpp::state_transition::batch_transition::BatchTransition; +use dpp::state_transition::StateTransition; +use dpp::ProtocolError; +use drive_proof_verifier::types::IdentityContractNonceFetcher; +use std::collections::BTreeMap; +use std::sync::Arc; + +#[derive(Debug)] +struct TestSigner; + +impl Signer for TestSigner { + fn sign(&self, _key: &IdentityPublicKey, _data: &[u8]) -> Result { + Ok(BinaryData::from(vec![1; 65])) + } + + fn sign_create_witness( + &self, + _key: &IdentityPublicKey, + _data: &[u8], + ) -> Result { + Err(ProtocolError::CorruptedCodeExecution( + "sign_create_witness is not used in these tests".to_string(), + )) + } + + fn can_sign_with(&self, _key: &IdentityPublicKey) -> bool { + true + } +} + +fn test_identity_public_key() -> IdentityPublicKey { + IdentityPublicKey::V0(IdentityPublicKeyV0 { + id: 1, + purpose: Purpose::AUTHENTICATION, + security_level: SecurityLevel::CRITICAL, + contract_bounds: None, + key_type: KeyType::ECDSA_SECP256K1, + read_only: false, + data: BinaryData::from(vec![2; 33]), + disabled_at: None, + }) +} + +fn test_data_contract(document_type_name: &str) -> Arc { + let platform_version = dpp::version::PlatformVersion::latest(); + let config = + DataContractConfig::default_for_version(platform_version).expect("create contract config"); + + let schema = platform_value!({ + "type": "object", + "properties": { + "a": { + "type": "string", + "maxLength": 10, + "position": 0 + } + }, + "additionalProperties": false, + }); + + let document_type = DocumentType::try_from_schema( + Identifier::random(), + 1, + config.version(), + document_type_name, + schema, + None, + &BTreeMap::new(), + &config, + true, + &mut vec![], + platform_version, + ) + .expect("create test document type"); + + let mut document_types: BTreeMap = BTreeMap::new(); + document_types.insert( + document_type.name().to_string(), + document_type.schema().clone(), + ); + + let contract = DataContractFactory::new(platform_version.protocol_version) + .expect("create data contract factory") + .create( + Identifier::random(), + 0, + platform_value!(document_types), + None, + None, + ) + .expect("create test data contract") + .data_contract_owned(); + + Arc::new(contract) +} + +const TEST_DOCUMENT_TYPE_NAME: &str = "testDoc"; +const INVALID_NONCE: u64 = 1_u64 << 50; + +fn test_document(owner_id: Identifier) -> Document { + Document::V0(DocumentV0 { + id: Identifier::random(), + owner_id, + properties: Default::default(), + revision: Some(1), + created_at: None, + updated_at: None, + transferred_at: None, + created_at_block_height: None, + updated_at_block_height: None, + transferred_at_block_height: None, + created_at_core_block_height: None, + updated_at_core_block_height: None, + transferred_at_core_block_height: None, + creator_id: None, + }) +} + +fn validate_transition_like_builder(state_transition: &StateTransition) -> Result<(), Error> { + let platform_version = dpp::version::PlatformVersion::latest(); + let validation_result = match state_transition { + StateTransition::Batch(batch_transition) => { + batch_transition.validate_base_structure(platform_version)? + } + _ => { + return Err(Error::Protocol( + dpp::ProtocolError::InvalidStateTransitionType( + "expected Batch transition".to_string(), + ), + )) + } + }; + if let Some(first_error) = validation_result.errors.into_iter().next() { + return Err(Error::Protocol(dpp::ProtocolError::ConsensusError( + Box::new(first_error), + ))); + } + Ok(()) +} + +pub(super) fn assert_document_create_validate_base_structure_error() { + let platform_version = dpp::version::PlatformVersion::latest(); + let data_contract = test_data_contract(TEST_DOCUMENT_TYPE_NAME); + let owner_id = Identifier::random(); + let document = test_document(owner_id); + let document_type = data_contract + .document_type_for_name(TEST_DOCUMENT_TYPE_NAME) + .expect("expected test document type"); + let transition = BatchTransition::new_document_creation_transition_from_document( + document, + document_type, + [7; 32], + &test_identity_public_key(), + INVALID_NONCE, + 0, + None, + &TestSigner, + platform_version, + None, + ) + .expect("transition should build"); + + let result = validate_transition_like_builder(&transition); + assert!(result.is_err(), "expected validation error, got {result:?}"); +} + +pub(super) fn assert_document_delete_validate_base_structure_error() { + let platform_version = dpp::version::PlatformVersion::latest(); + let data_contract = test_data_contract(TEST_DOCUMENT_TYPE_NAME); + let owner_id = Identifier::random(); + let document = test_document(owner_id); + let document_type = data_contract + .document_type_for_name(TEST_DOCUMENT_TYPE_NAME) + .expect("expected test document type"); + let transition = BatchTransition::new_document_deletion_transition_from_document( + document, + document_type, + &test_identity_public_key(), + INVALID_NONCE, + 0, + None, + &TestSigner, + platform_version, + None, + ) + .expect("transition should build"); + + let result = validate_transition_like_builder(&transition); + assert!(result.is_err(), "expected validation error, got {result:?}"); +} + +pub(super) fn assert_document_purchase_validate_base_structure_error() { + let platform_version = dpp::version::PlatformVersion::latest(); + let data_contract = test_data_contract(TEST_DOCUMENT_TYPE_NAME); + let owner_id = Identifier::random(); + let document = test_document(owner_id); + let purchaser_id = Identifier::random(); + let document_type = data_contract + .document_type_for_name(TEST_DOCUMENT_TYPE_NAME) + .expect("expected test document type"); + let transition = BatchTransition::new_document_purchase_transition_from_document( + document, + document_type, + purchaser_id, + 100, + &test_identity_public_key(), + INVALID_NONCE, + 0, + None, + &TestSigner, + platform_version, + None, + ) + .expect("transition should build"); + + let result = validate_transition_like_builder(&transition); + assert!(result.is_err(), "expected validation error, got {result:?}"); +} + +pub(super) fn assert_document_replace_validate_base_structure_error() { + let platform_version = dpp::version::PlatformVersion::latest(); + let data_contract = test_data_contract(TEST_DOCUMENT_TYPE_NAME); + let owner_id = Identifier::random(); + let document = test_document(owner_id); + let document_type = data_contract + .document_type_for_name(TEST_DOCUMENT_TYPE_NAME) + .expect("expected test document type"); + let transition = BatchTransition::new_document_replacement_transition_from_document( + document, + document_type, + &test_identity_public_key(), + INVALID_NONCE, + 0, + None, + &TestSigner, + platform_version, + None, + ) + .expect("transition should build"); + + let result = validate_transition_like_builder(&transition); + assert!(result.is_err(), "expected validation error, got {result:?}"); +} + +pub(super) fn assert_document_set_price_validate_base_structure_error() { + let platform_version = dpp::version::PlatformVersion::latest(); + let data_contract = test_data_contract(TEST_DOCUMENT_TYPE_NAME); + let owner_id = Identifier::random(); + let document = test_document(owner_id); + let document_type = data_contract + .document_type_for_name(TEST_DOCUMENT_TYPE_NAME) + .expect("expected test document type"); + let transition = BatchTransition::new_document_update_price_transition_from_document( + document, + document_type, + 200, + &test_identity_public_key(), + INVALID_NONCE, + 0, + None, + &TestSigner, + platform_version, + None, + ) + .expect("transition should build"); + + let result = validate_transition_like_builder(&transition); + assert!(result.is_err(), "expected validation error, got {result:?}"); +} + +pub(super) fn assert_document_transfer_validate_base_structure_error() { + let platform_version = dpp::version::PlatformVersion::latest(); + let data_contract = test_data_contract(TEST_DOCUMENT_TYPE_NAME); + let owner_id = Identifier::random(); + let document = test_document(owner_id); + let recipient_id = Identifier::random(); + let document_type = data_contract + .document_type_for_name(TEST_DOCUMENT_TYPE_NAME) + .expect("expected test document type"); + let transition = BatchTransition::new_document_transfer_transition_from_document( + document, + document_type, + recipient_id, + &test_identity_public_key(), + INVALID_NONCE, + 0, + None, + &TestSigner, + platform_version, + None, + ) + .expect("transition should build"); + + let result = validate_transition_like_builder(&transition); + assert!(result.is_err(), "expected validation error, got {result:?}"); +} + +async fn new_mock_sdk_with_contract_nonce( + identity_id: Identifier, + contract_id: Identifier, + fetched_nonce: u64, +) -> Sdk { + let mut sdk = SdkBuilder::new_mock().build().expect("build mock sdk"); + + sdk.mock() + .expect_fetch::( + (identity_id, contract_id), + Some(IdentityContractNonceFetcher(fetched_nonce)), + ) + .await + .expect("set nonce fetch expectation"); + + sdk +} + +#[tokio::test] +async fn document_builder_sign_masks_nonce_so_out_of_bounds_is_unreachable() { + // Document builders obtain nonce through `Sdk::get_identity_contract_nonce`, + // which masks out-of-bounds bits. This makes `validate_base_structure` + // nonce-out-of-bounds errors unreachable through the builder API. + // One test suffices since all document builders use the same SDK nonce path. + let document_type_name = "testDoc"; + let data_contract = test_data_contract(document_type_name); + let owner_id = Identifier::random(); + + let sdk = new_mock_sdk_with_contract_nonce(owner_id, data_contract.id(), 1_u64 << 50).await; + + let builder = DocumentDeleteTransitionBuilder::new( + Arc::clone(&data_contract), + document_type_name.to_string(), + Identifier::random(), + owner_id, + ); + + let result = builder + .sign( + &sdk, + &test_identity_public_key(), + &TestSigner, + dpp::version::PlatformVersion::latest(), + ) + .await; + + assert!( + result.is_ok(), + "SDK should mask nonce internally; got error: {:?}", + result.err() + ); +} diff --git a/packages/rs-sdk/src/platform/documents/transitions/transfer.rs b/packages/rs-sdk/src/platform/documents/transitions/transfer.rs index ae1f2afbb04..452d5590265 100644 --- a/packages/rs-sdk/src/platform/documents/transitions/transfer.rs +++ b/packages/rs-sdk/src/platform/documents/transitions/transfer.rs @@ -207,6 +207,25 @@ impl DocumentTransferTransitionBuilder { self.state_transition_creation_options, )?; + // Validate the transition structure before returning + let validation_result = match &state_transition { + StateTransition::Batch(batch_transition) => { + batch_transition.validate_base_structure(platform_version)? + } + _ => { + return Err(Error::Protocol( + dpp::ProtocolError::InvalidStateTransitionType( + "expected Batch transition".to_string(), + ), + )); + } + }; + if let Some(first_error) = validation_result.errors.into_iter().next() { + return Err(Error::Protocol(dpp::ProtocolError::ConsensusError( + Box::new(first_error), + ))); + } + Ok(state_transition) } } @@ -286,3 +305,11 @@ impl Sdk { } } } + +#[cfg(test)] +mod validation_tests { + #[test] + fn validate_base_structure_error_case() { + super::super::tests::assert_document_transfer_validate_base_structure_error(); + } +} diff --git a/packages/rs-sdk/src/platform/tokens/builders/burn.rs b/packages/rs-sdk/src/platform/tokens/builders/burn.rs index 5519042a615..324f53b1844 100644 --- a/packages/rs-sdk/src/platform/tokens/builders/burn.rs +++ b/packages/rs-sdk/src/platform/tokens/builders/burn.rs @@ -179,6 +179,33 @@ impl TokenBurnTransitionBuilder { self.state_transition_creation_options, )?; + // Validate the transition structure before returning + let validation_result = match &state_transition { + StateTransition::Batch(batch_transition) => { + batch_transition.validate_base_structure(platform_version)? + } + _ => { + return Err(Error::Protocol( + dpp::ProtocolError::InvalidStateTransitionType( + "expected Batch transition".to_string(), + ), + )); + } + }; + if let Some(first_error) = validation_result.errors.into_iter().next() { + return Err(Error::Protocol(dpp::ProtocolError::ConsensusError( + Box::new(first_error), + ))); + } + Ok(state_transition) } } + +#[cfg(test)] +mod validation_tests { + #[test] + fn validate_base_structure_error_case() { + super::super::tests::assert_token_burn_validate_base_structure_error(); + } +} diff --git a/packages/rs-sdk/src/platform/tokens/builders/claim.rs b/packages/rs-sdk/src/platform/tokens/builders/claim.rs index bde33938446..22fc107cdbe 100644 --- a/packages/rs-sdk/src/platform/tokens/builders/claim.rs +++ b/packages/rs-sdk/src/platform/tokens/builders/claim.rs @@ -165,6 +165,33 @@ impl TokenClaimTransitionBuilder { self.state_transition_creation_options, )?; + // Validate the transition structure before returning + let validation_result = match &state_transition { + StateTransition::Batch(batch_transition) => { + batch_transition.validate_base_structure(platform_version)? + } + _ => { + return Err(Error::Protocol( + dpp::ProtocolError::InvalidStateTransitionType( + "expected Batch transition".to_string(), + ), + )); + } + }; + if let Some(first_error) = validation_result.errors.into_iter().next() { + return Err(Error::Protocol(dpp::ProtocolError::ConsensusError( + Box::new(first_error), + ))); + } + Ok(state_transition) } } + +#[cfg(test)] +mod validation_tests { + #[test] + fn validate_base_structure_error_case() { + super::super::tests::assert_token_claim_validate_base_structure_error(); + } +} diff --git a/packages/rs-sdk/src/platform/tokens/builders/config_update.rs b/packages/rs-sdk/src/platform/tokens/builders/config_update.rs index 8cf15ae4a85..e231858099d 100644 --- a/packages/rs-sdk/src/platform/tokens/builders/config_update.rs +++ b/packages/rs-sdk/src/platform/tokens/builders/config_update.rs @@ -187,6 +187,33 @@ impl TokenConfigUpdateTransitionBuilder { self.state_transition_creation_options, )?; + // Validate the transition structure before returning + let validation_result = match &state_transition { + StateTransition::Batch(batch_transition) => { + batch_transition.validate_base_structure(platform_version)? + } + _ => { + return Err(Error::Protocol( + dpp::ProtocolError::InvalidStateTransitionType( + "expected Batch transition".to_string(), + ), + )); + } + }; + if let Some(first_error) = validation_result.errors.into_iter().next() { + return Err(Error::Protocol(dpp::ProtocolError::ConsensusError( + Box::new(first_error), + ))); + } + Ok(state_transition) } } + +#[cfg(test)] +mod validation_tests { + #[test] + fn validate_base_structure_error_case() { + super::super::tests::assert_token_config_update_validate_base_structure_error(); + } +} diff --git a/packages/rs-sdk/src/platform/tokens/builders/destroy.rs b/packages/rs-sdk/src/platform/tokens/builders/destroy.rs index 8269ce074ee..b07baf37190 100644 --- a/packages/rs-sdk/src/platform/tokens/builders/destroy.rs +++ b/packages/rs-sdk/src/platform/tokens/builders/destroy.rs @@ -185,6 +185,33 @@ impl TokenDestroyFrozenFundsTransitionBuilder { self.state_transition_creation_options, )?; + // Validate the transition structure before returning + let validation_result = match &state_transition { + StateTransition::Batch(batch_transition) => { + batch_transition.validate_base_structure(platform_version)? + } + _ => { + return Err(Error::Protocol( + dpp::ProtocolError::InvalidStateTransitionType( + "expected Batch transition".to_string(), + ), + )); + } + }; + if let Some(first_error) = validation_result.errors.into_iter().next() { + return Err(Error::Protocol(dpp::ProtocolError::ConsensusError( + Box::new(first_error), + ))); + } + Ok(state_transition) } } + +#[cfg(test)] +mod validation_tests { + #[test] + fn validate_base_structure_error_case() { + super::super::tests::assert_token_destroy_validate_base_structure_error(); + } +} diff --git a/packages/rs-sdk/src/platform/tokens/builders/emergency_action.rs b/packages/rs-sdk/src/platform/tokens/builders/emergency_action.rs index c6f3e97caf3..da44967f312 100644 --- a/packages/rs-sdk/src/platform/tokens/builders/emergency_action.rs +++ b/packages/rs-sdk/src/platform/tokens/builders/emergency_action.rs @@ -213,6 +213,33 @@ impl TokenEmergencyActionTransitionBuilder { self.state_transition_creation_options, )?; + // Validate the transition structure before returning + let validation_result = match &state_transition { + StateTransition::Batch(batch_transition) => { + batch_transition.validate_base_structure(platform_version)? + } + _ => { + return Err(Error::Protocol( + dpp::ProtocolError::InvalidStateTransitionType( + "expected Batch transition".to_string(), + ), + )); + } + }; + if let Some(first_error) = validation_result.errors.into_iter().next() { + return Err(Error::Protocol(dpp::ProtocolError::ConsensusError( + Box::new(first_error), + ))); + } + Ok(state_transition) } } + +#[cfg(test)] +mod validation_tests { + #[test] + fn validate_base_structure_error_case() { + super::super::tests::assert_token_emergency_action_validate_base_structure_error(); + } +} diff --git a/packages/rs-sdk/src/platform/tokens/builders/freeze.rs b/packages/rs-sdk/src/platform/tokens/builders/freeze.rs index 29c4f4d86d5..a260b5f12eb 100644 --- a/packages/rs-sdk/src/platform/tokens/builders/freeze.rs +++ b/packages/rs-sdk/src/platform/tokens/builders/freeze.rs @@ -185,6 +185,33 @@ impl TokenFreezeTransitionBuilder { self.state_transition_creation_options, )?; + // Validate the transition structure before returning + let validation_result = match &state_transition { + StateTransition::Batch(batch_transition) => { + batch_transition.validate_base_structure(platform_version)? + } + _ => { + return Err(Error::Protocol( + dpp::ProtocolError::InvalidStateTransitionType( + "expected Batch transition".to_string(), + ), + )); + } + }; + if let Some(first_error) = validation_result.errors.into_iter().next() { + return Err(Error::Protocol(dpp::ProtocolError::ConsensusError( + Box::new(first_error), + ))); + } + Ok(state_transition) } } + +#[cfg(test)] +mod validation_tests { + #[test] + fn validate_base_structure_error_case() { + super::super::tests::assert_token_freeze_validate_base_structure_error(); + } +} diff --git a/packages/rs-sdk/src/platform/tokens/builders/mint.rs b/packages/rs-sdk/src/platform/tokens/builders/mint.rs index fe14d7bf5f2..0f6dfa52635 100644 --- a/packages/rs-sdk/src/platform/tokens/builders/mint.rs +++ b/packages/rs-sdk/src/platform/tokens/builders/mint.rs @@ -206,6 +206,33 @@ impl TokenMintTransitionBuilder { self.state_transition_creation_options, )?; + // Validate the transition structure before returning + let validation_result = match &state_transition { + StateTransition::Batch(batch_transition) => { + batch_transition.validate_base_structure(platform_version)? + } + _ => { + return Err(Error::Protocol( + dpp::ProtocolError::InvalidStateTransitionType( + "expected Batch transition".to_string(), + ), + )); + } + }; + if let Some(first_error) = validation_result.errors.into_iter().next() { + return Err(Error::Protocol(dpp::ProtocolError::ConsensusError( + Box::new(first_error), + ))); + } + Ok(state_transition) } } + +#[cfg(test)] +mod validation_tests { + #[test] + fn validate_base_structure_error_case() { + super::super::tests::assert_token_mint_validate_base_structure_error(); + } +} diff --git a/packages/rs-sdk/src/platform/tokens/builders/mod.rs b/packages/rs-sdk/src/platform/tokens/builders/mod.rs index eec8c140062..bc675dcdfc9 100644 --- a/packages/rs-sdk/src/platform/tokens/builders/mod.rs +++ b/packages/rs-sdk/src/platform/tokens/builders/mod.rs @@ -9,5 +9,7 @@ pub mod freeze; pub mod mint; pub mod purchase; pub mod set_price; +#[cfg(test)] +mod tests; pub mod transfer; pub mod unfreeze; diff --git a/packages/rs-sdk/src/platform/tokens/builders/purchase.rs b/packages/rs-sdk/src/platform/tokens/builders/purchase.rs index fce0a483cf8..58be6b3c45d 100644 --- a/packages/rs-sdk/src/platform/tokens/builders/purchase.rs +++ b/packages/rs-sdk/src/platform/tokens/builders/purchase.rs @@ -153,6 +153,33 @@ impl TokenDirectPurchaseTransitionBuilder { self.state_transition_creation_options, )?; + // Validate the transition structure before returning + let validation_result = match &state_transition { + StateTransition::Batch(batch_transition) => { + batch_transition.validate_base_structure(platform_version)? + } + _ => { + return Err(Error::Protocol( + dpp::ProtocolError::InvalidStateTransitionType( + "expected Batch transition".to_string(), + ), + )); + } + }; + if let Some(first_error) = validation_result.errors.into_iter().next() { + return Err(Error::Protocol(dpp::ProtocolError::ConsensusError( + Box::new(first_error), + ))); + } + Ok(state_transition) } } + +#[cfg(test)] +mod validation_tests { + #[test] + fn validate_base_structure_error_case() { + super::super::tests::assert_token_purchase_validate_base_structure_error(); + } +} diff --git a/packages/rs-sdk/src/platform/tokens/builders/set_price.rs b/packages/rs-sdk/src/platform/tokens/builders/set_price.rs index 65ded0944e3..05fa7867c8b 100644 --- a/packages/rs-sdk/src/platform/tokens/builders/set_price.rs +++ b/packages/rs-sdk/src/platform/tokens/builders/set_price.rs @@ -230,6 +230,33 @@ impl TokenChangeDirectPurchasePriceTransitionBuilder { self.state_transition_creation_options, )?; + // Validate the transition structure before returning + let validation_result = match &state_transition { + StateTransition::Batch(batch_transition) => { + batch_transition.validate_base_structure(platform_version)? + } + _ => { + return Err(Error::Protocol( + dpp::ProtocolError::InvalidStateTransitionType( + "expected Batch transition".to_string(), + ), + )); + } + }; + if let Some(first_error) = validation_result.errors.into_iter().next() { + return Err(Error::Protocol(dpp::ProtocolError::ConsensusError( + Box::new(first_error), + ))); + } + Ok(state_transition) } } + +#[cfg(test)] +mod validation_tests { + #[test] + fn validate_base_structure_error_case() { + super::super::tests::assert_token_set_price_validate_base_structure_error(); + } +} diff --git a/packages/rs-sdk/src/platform/tokens/builders/tests.rs b/packages/rs-sdk/src/platform/tokens/builders/tests.rs new file mode 100644 index 00000000000..51659b0ad14 --- /dev/null +++ b/packages/rs-sdk/src/platform/tokens/builders/tests.rs @@ -0,0 +1,940 @@ +use super::burn::TokenBurnTransitionBuilder; +use super::claim::TokenClaimTransitionBuilder; +use super::config_update::TokenConfigUpdateTransitionBuilder; +use super::destroy::TokenDestroyFrozenFundsTransitionBuilder; +use super::emergency_action::TokenEmergencyActionTransitionBuilder; +use super::freeze::TokenFreezeTransitionBuilder; +use super::mint::TokenMintTransitionBuilder; +use super::purchase::TokenDirectPurchaseTransitionBuilder; +use super::set_price::TokenChangeDirectPurchasePriceTransitionBuilder; +use super::transfer::TokenTransferTransitionBuilder; +use super::unfreeze::TokenUnfreezeTransitionBuilder; +use crate::{Error, Sdk, SdkBuilder}; +use dpp::address_funds::AddressWitness; +use dpp::consensus::basic::BasicError; +use dpp::consensus::ConsensusError; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::associated_token::token_configuration_item::TokenConfigurationChangeItem; +use dpp::data_contract::associated_token::token_distribution_key::TokenDistributionType; +use dpp::data_contract::config::DataContractConfig; +use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; +use dpp::data_contract::document_type::DocumentType; +use dpp::data_contract::DataContractFactory; +use dpp::group::{GroupStateTransitionInfo, GroupStateTransitionInfoStatus}; +use dpp::identity::identity_public_key::v0::IdentityPublicKeyV0; +use dpp::identity::signer::Signer; +use dpp::identity::{IdentityPublicKey, KeyType, Purpose, SecurityLevel}; +use dpp::platform_value::{platform_value, BinaryData, Value}; +use dpp::prelude::Identifier; +use dpp::state_transition::batch_transition::methods::v1::DocumentsBatchTransitionMethodsV1; +use dpp::state_transition::batch_transition::BatchTransition; +use dpp::state_transition::StateTransition; +use dpp::tokens::calculate_token_id; +use dpp::tokens::emergency_action::TokenEmergencyAction; +use dpp::tokens::token_pricing_schedule::TokenPricingSchedule; +use dpp::ProtocolError; +use drive_proof_verifier::types::IdentityContractNonceFetcher; +use std::collections::BTreeMap; +use std::sync::Arc; + +#[derive(Debug)] +struct TestSigner; + +impl Signer for TestSigner { + fn sign(&self, _key: &IdentityPublicKey, _data: &[u8]) -> Result { + Ok(BinaryData::from(vec![1; 65])) + } + + fn sign_create_witness( + &self, + _key: &IdentityPublicKey, + _data: &[u8], + ) -> Result { + Err(ProtocolError::CorruptedCodeExecution( + "sign_create_witness is not used in these tests".to_string(), + )) + } + + fn can_sign_with(&self, _key: &IdentityPublicKey) -> bool { + true + } +} + +fn test_identity_public_key() -> IdentityPublicKey { + IdentityPublicKey::V0(IdentityPublicKeyV0 { + id: 1, + purpose: Purpose::AUTHENTICATION, + security_level: SecurityLevel::CRITICAL, + contract_bounds: None, + key_type: KeyType::ECDSA_SECP256K1, + read_only: false, + data: BinaryData::from(vec![2; 33]), + disabled_at: None, + }) +} + +fn test_data_contract(document_type_name: &str) -> Arc { + let platform_version = dpp::version::PlatformVersion::latest(); + let config = + DataContractConfig::default_for_version(platform_version).expect("create contract config"); + + let schema = platform_value!({ + "type": "object", + "properties": { + "a": { + "type": "string", + "maxLength": 10, + "position": 0 + } + }, + "additionalProperties": false, + }); + + let document_type = DocumentType::try_from_schema( + Identifier::random(), + 1, + config.version(), + document_type_name, + schema, + None, + &BTreeMap::new(), + &config, + true, + &mut vec![], + platform_version, + ) + .expect("create test document type"); + + let mut document_types: BTreeMap = BTreeMap::new(); + document_types.insert( + document_type.name().to_string(), + document_type.schema().clone(), + ); + + let contract = DataContractFactory::new(platform_version.protocol_version) + .expect("create data contract factory") + .create( + Identifier::random(), + 0, + platform_value!(document_types), + None, + None, + ) + .expect("create test data contract") + .data_contract_owned(); + + Arc::new(contract) +} + +const TEST_DOCUMENT_TYPE_NAME: &str = "testDoc"; +const TEST_TOKEN_POSITION: u16 = 0; +const INVALID_NONCE: u64 = 1_u64 << 50; + +fn validate_transition_like_builder(state_transition: &StateTransition) -> Result<(), Error> { + let platform_version = dpp::version::PlatformVersion::latest(); + let validation_result = match state_transition { + StateTransition::Batch(batch_transition) => { + batch_transition.validate_base_structure(platform_version)? + } + _ => { + return Err(Error::Protocol( + dpp::ProtocolError::InvalidStateTransitionType( + "expected Batch transition".to_string(), + ), + )) + } + }; + if let Some(first_error) = validation_result.errors.into_iter().next() { + return Err(Error::Protocol(dpp::ProtocolError::ConsensusError( + Box::new(first_error), + ))); + } + Ok(()) +} + +fn token_setup() -> ( + Arc, + Identifier, + Identifier, +) { + let data_contract = test_data_contract(TEST_DOCUMENT_TYPE_NAME); + let owner_id = Identifier::random(); + let token_id = Identifier::from(calculate_token_id( + data_contract.id().as_bytes(), + TEST_TOKEN_POSITION, + )); + (data_contract, owner_id, token_id) +} + +pub(super) fn assert_token_burn_validate_base_structure_error() { + let platform_version = dpp::version::PlatformVersion::latest(); + let (data_contract, owner_id, token_id) = token_setup(); + let transition = BatchTransition::new_token_burn_transition( + token_id, + owner_id, + data_contract.id(), + TEST_TOKEN_POSITION, + 1, + None, + None, + &test_identity_public_key(), + INVALID_NONCE, + 0, + &TestSigner, + platform_version, + None, + ) + .expect("transition should build"); + + let result = validate_transition_like_builder(&transition); + assert!(result.is_err(), "expected validation error, got {result:?}"); +} + +pub(super) fn assert_token_claim_validate_base_structure_error() { + let platform_version = dpp::version::PlatformVersion::latest(); + let (data_contract, owner_id, token_id) = token_setup(); + let transition = BatchTransition::new_token_claim_transition( + token_id, + owner_id, + data_contract.id(), + TEST_TOKEN_POSITION, + TokenDistributionType::PreProgrammed, + None, + &test_identity_public_key(), + INVALID_NONCE, + 0, + &TestSigner, + platform_version, + None, + ) + .expect("transition should build"); + + let result = validate_transition_like_builder(&transition); + assert!(result.is_err(), "expected validation error, got {result:?}"); +} + +pub(super) fn assert_token_config_update_validate_base_structure_error() { + let platform_version = dpp::version::PlatformVersion::latest(); + let (data_contract, owner_id, token_id) = token_setup(); + let transition = BatchTransition::new_token_config_update_transition( + token_id, + owner_id, + data_contract.id(), + TEST_TOKEN_POSITION, + TokenConfigurationChangeItem::TokenConfigurationNoChange, + None, + None, + &test_identity_public_key(), + INVALID_NONCE, + 0, + &TestSigner, + platform_version, + None, + ) + .expect("transition should build"); + + let result = validate_transition_like_builder(&transition); + assert!(result.is_err(), "expected validation error, got {result:?}"); +} + +pub(super) fn assert_token_destroy_validate_base_structure_error() { + let platform_version = dpp::version::PlatformVersion::latest(); + let (data_contract, owner_id, token_id) = token_setup(); + let transition = BatchTransition::new_token_destroy_frozen_funds_transition( + token_id, + owner_id, + data_contract.id(), + TEST_TOKEN_POSITION, + Identifier::random(), + None, + None, + &test_identity_public_key(), + INVALID_NONCE, + 0, + &TestSigner, + platform_version, + None, + ) + .expect("transition should build"); + + let result = validate_transition_like_builder(&transition); + assert!(result.is_err(), "expected validation error, got {result:?}"); +} + +pub(super) fn assert_token_emergency_action_validate_base_structure_error() { + let platform_version = dpp::version::PlatformVersion::latest(); + let (data_contract, owner_id, token_id) = token_setup(); + let transition = BatchTransition::new_token_emergency_action_transition( + token_id, + owner_id, + data_contract.id(), + TEST_TOKEN_POSITION, + TokenEmergencyAction::Pause, + None, + None, + &test_identity_public_key(), + INVALID_NONCE, + 0, + &TestSigner, + platform_version, + None, + ) + .expect("transition should build"); + + let result = validate_transition_like_builder(&transition); + assert!(result.is_err(), "expected validation error, got {result:?}"); +} + +pub(super) fn assert_token_freeze_validate_base_structure_error() { + let platform_version = dpp::version::PlatformVersion::latest(); + let (data_contract, owner_id, token_id) = token_setup(); + let transition = BatchTransition::new_token_freeze_transition( + token_id, + owner_id, + data_contract.id(), + TEST_TOKEN_POSITION, + Identifier::random(), + None, + None, + &test_identity_public_key(), + INVALID_NONCE, + 0, + &TestSigner, + platform_version, + None, + ) + .expect("transition should build"); + + let result = validate_transition_like_builder(&transition); + assert!(result.is_err(), "expected validation error, got {result:?}"); +} + +pub(super) fn assert_token_mint_validate_base_structure_error() { + let platform_version = dpp::version::PlatformVersion::latest(); + let (data_contract, owner_id, token_id) = token_setup(); + let transition = BatchTransition::new_token_mint_transition( + token_id, + owner_id, + data_contract.id(), + TEST_TOKEN_POSITION, + 1, + Some(Identifier::random()), + None, + None, + &test_identity_public_key(), + INVALID_NONCE, + 0, + &TestSigner, + platform_version, + None, + ) + .expect("transition should build"); + + let result = validate_transition_like_builder(&transition); + assert!(result.is_err(), "expected validation error, got {result:?}"); +} + +pub(super) fn assert_token_purchase_validate_base_structure_error() { + let platform_version = dpp::version::PlatformVersion::latest(); + let (data_contract, owner_id, token_id) = token_setup(); + let transition = BatchTransition::new_token_direct_purchase_transition( + token_id, + owner_id, + data_contract.id(), + TEST_TOKEN_POSITION, + 1, + 10, + &test_identity_public_key(), + INVALID_NONCE, + 0, + &TestSigner, + platform_version, + None, + ) + .expect("transition should build"); + + let result = validate_transition_like_builder(&transition); + assert!(result.is_err(), "expected validation error, got {result:?}"); +} + +pub(super) fn assert_token_set_price_validate_base_structure_error() { + let platform_version = dpp::version::PlatformVersion::latest(); + let (data_contract, owner_id, token_id) = token_setup(); + let transition = BatchTransition::new_token_change_direct_purchase_price_transition( + token_id, + owner_id, + data_contract.id(), + TEST_TOKEN_POSITION, + Some(TokenPricingSchedule::SinglePrice(5)), + None, + None, + &test_identity_public_key(), + INVALID_NONCE, + 0, + &TestSigner, + platform_version, + None, + ) + .expect("transition should build"); + + let result = validate_transition_like_builder(&transition); + assert!(result.is_err(), "expected validation error, got {result:?}"); +} + +pub(super) fn assert_token_transfer_validate_base_structure_error() { + let platform_version = dpp::version::PlatformVersion::latest(); + let (data_contract, owner_id, token_id) = token_setup(); + let transition = BatchTransition::new_token_transfer_transition( + token_id, + owner_id, + data_contract.id(), + TEST_TOKEN_POSITION, + 1, + Identifier::random(), + None, + None, + None, + &test_identity_public_key(), + INVALID_NONCE, + 0, + &TestSigner, + platform_version, + None, + ) + .expect("transition should build"); + + let result = validate_transition_like_builder(&transition); + assert!(result.is_err(), "expected validation error, got {result:?}"); +} + +pub(super) fn assert_token_unfreeze_validate_base_structure_error() { + let platform_version = dpp::version::PlatformVersion::latest(); + let (data_contract, owner_id, token_id) = token_setup(); + let transition = BatchTransition::new_token_unfreeze_transition( + token_id, + owner_id, + data_contract.id(), + TEST_TOKEN_POSITION, + Identifier::random(), + None, + None, + &test_identity_public_key(), + INVALID_NONCE, + 0, + &TestSigner, + platform_version, + None, + ) + .expect("transition should build"); + + let result = validate_transition_like_builder(&transition); + assert!(result.is_err(), "expected validation error, got {result:?}"); +} + +async fn new_mock_sdk_with_contract_nonce( + identity_id: Identifier, + contract_id: Identifier, + fetched_nonce: u64, +) -> Sdk { + let mut sdk = SdkBuilder::new_mock().build().expect("build mock sdk"); + + sdk.mock() + .expect_fetch::( + (identity_id, contract_id), + Some(IdentityContractNonceFetcher(fetched_nonce)), + ) + .await + .expect("set nonce fetch expectation"); + + sdk +} + +#[tokio::test] +async fn token_mint_sign_returns_invalid_token_amount_error_when_amount_is_zero() { + let issuer_id = Identifier::random(); + let data_contract = test_data_contract(TEST_DOCUMENT_TYPE_NAME); + let sdk = new_mock_sdk_with_contract_nonce(issuer_id, data_contract.id(), 0).await; + + let result = TokenMintTransitionBuilder::new(Arc::clone(&data_contract), 0, issuer_id, 0) + .issued_to_identity_id(Identifier::random()) + .sign( + &sdk, + &test_identity_public_key(), + &TestSigner, + dpp::version::PlatformVersion::latest(), + ) + .await; + + assert!( + matches!( + result, + Err(Error::Protocol(ProtocolError::ConsensusError(ref consensus_error))) + if matches!(**consensus_error, ConsensusError::BasicError(BasicError::InvalidTokenAmountError(_))) + ), + "unexpected result: {:?}", + result + ); +} + +#[tokio::test] +async fn token_mint_sign_returns_invalid_action_id_error_for_mismatched_group_action_id() { + let issuer_id = Identifier::random(); + let data_contract = test_data_contract(TEST_DOCUMENT_TYPE_NAME); + let sdk = new_mock_sdk_with_contract_nonce(issuer_id, data_contract.id(), 0).await; + + let invalid_group_info = GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: 0, + action_id: Identifier::from_bytes(&[0; 32]).expect("create static action id"), + action_is_proposer: true, + }, + ); + + let result = TokenMintTransitionBuilder::new(Arc::clone(&data_contract), 0, issuer_id, 1) + .issued_to_identity_id(Identifier::random()) + .with_using_group_info(invalid_group_info) + .sign( + &sdk, + &test_identity_public_key(), + &TestSigner, + dpp::version::PlatformVersion::latest(), + ) + .await; + + assert!( + matches!( + result, + Err(Error::Protocol(ProtocolError::ConsensusError(ref consensus_error))) + if matches!(**consensus_error, ConsensusError::BasicError(BasicError::InvalidActionIdError(_))) + ), + "unexpected result: {:?}", + result + ); +} + +#[tokio::test] +async fn token_burn_sign_returns_invalid_token_amount_error_when_amount_is_zero() { + let owner_id = Identifier::random(); + let data_contract = test_data_contract("testDoc"); + let sdk = new_mock_sdk_with_contract_nonce(owner_id, data_contract.id(), 0).await; + + let result = TokenBurnTransitionBuilder::new(Arc::clone(&data_contract), 0, owner_id, 0) + .sign( + &sdk, + &test_identity_public_key(), + &TestSigner, + dpp::version::PlatformVersion::latest(), + ) + .await; + + assert!( + matches!( + result, + Err(Error::Protocol(ProtocolError::ConsensusError(ref consensus_error))) + if matches!(**consensus_error, ConsensusError::BasicError(BasicError::InvalidTokenAmountError(_))) + ), + "unexpected result: {:?}", + result + ); +} + +#[tokio::test] +async fn token_burn_sign_returns_note_too_big_error() { + let owner_id = Identifier::random(); + let data_contract = test_data_contract("testDoc"); + let sdk = new_mock_sdk_with_contract_nonce(owner_id, data_contract.id(), 0).await; + + let result = TokenBurnTransitionBuilder::new(Arc::clone(&data_contract), 0, owner_id, 1) + .with_public_note("x".repeat(2049)) + .sign( + &sdk, + &test_identity_public_key(), + &TestSigner, + dpp::version::PlatformVersion::latest(), + ) + .await; + + assert!( + matches!( + result, + Err(Error::Protocol(ProtocolError::ConsensusError(ref consensus_error))) + if matches!(**consensus_error, ConsensusError::BasicError(BasicError::InvalidTokenNoteTooBigError(_))) + ), + "unexpected result: {:?}", + result + ); +} + +#[tokio::test] +async fn token_transfer_sign_returns_invalid_token_amount_error_when_amount_is_zero() { + let sender_id = Identifier::random(); + let recipient_id = Identifier::random(); + let data_contract = test_data_contract("testDoc"); + let sdk = new_mock_sdk_with_contract_nonce(sender_id, data_contract.id(), 0).await; + + let result = TokenTransferTransitionBuilder::new( + Arc::clone(&data_contract), + 0, + sender_id, + recipient_id, + 0, + ) + .sign( + &sdk, + &test_identity_public_key(), + &TestSigner, + dpp::version::PlatformVersion::latest(), + ) + .await; + + assert!( + matches!( + result, + Err(Error::Protocol(ProtocolError::ConsensusError(ref consensus_error))) + if matches!(**consensus_error, ConsensusError::BasicError(BasicError::InvalidTokenAmountError(_))) + ), + "unexpected result: {:?}", + result + ); +} + +#[tokio::test] +async fn token_transfer_sign_returns_transfer_to_ourself_error() { + let sender_id = Identifier::random(); + let data_contract = test_data_contract("testDoc"); + let sdk = new_mock_sdk_with_contract_nonce(sender_id, data_contract.id(), 0).await; + + let result = + TokenTransferTransitionBuilder::new(Arc::clone(&data_contract), 0, sender_id, sender_id, 1) + .sign( + &sdk, + &test_identity_public_key(), + &TestSigner, + dpp::version::PlatformVersion::latest(), + ) + .await; + + assert!( + matches!( + result, + Err(Error::Protocol(ProtocolError::ConsensusError(ref consensus_error))) + if matches!(**consensus_error, ConsensusError::BasicError(BasicError::TokenTransferToOurselfError(_))) + ), + "unexpected result: {:?}", + result + ); +} + +#[tokio::test] +async fn token_transfer_sign_returns_note_too_big_error_for_public_note() { + let sender_id = Identifier::random(); + let recipient_id = Identifier::random(); + let data_contract = test_data_contract("testDoc"); + let sdk = new_mock_sdk_with_contract_nonce(sender_id, data_contract.id(), 0).await; + + let result = TokenTransferTransitionBuilder::new( + Arc::clone(&data_contract), + 0, + sender_id, + recipient_id, + 1, + ) + .with_public_note("x".repeat(2049)) + .sign( + &sdk, + &test_identity_public_key(), + &TestSigner, + dpp::version::PlatformVersion::latest(), + ) + .await; + + assert!( + matches!( + result, + Err(Error::Protocol(ProtocolError::ConsensusError(ref consensus_error))) + if matches!(**consensus_error, ConsensusError::BasicError(BasicError::InvalidTokenNoteTooBigError(_))) + ), + "unexpected result: {:?}", + result + ); +} + +#[tokio::test] +async fn token_freeze_sign_returns_note_too_big_error() { + let actor_id = Identifier::random(); + let freeze_identity_id = Identifier::random(); + let data_contract = test_data_contract("testDoc"); + let sdk = new_mock_sdk_with_contract_nonce(actor_id, data_contract.id(), 0).await; + + let result = TokenFreezeTransitionBuilder::new( + Arc::clone(&data_contract), + 0, + actor_id, + freeze_identity_id, + ) + .with_public_note("x".repeat(2049)) + .sign( + &sdk, + &test_identity_public_key(), + &TestSigner, + dpp::version::PlatformVersion::latest(), + ) + .await; + + assert!( + matches!( + result, + Err(Error::Protocol(ProtocolError::ConsensusError(ref consensus_error))) + if matches!(**consensus_error, ConsensusError::BasicError(BasicError::InvalidTokenNoteTooBigError(_))) + ), + "unexpected result: {:?}", + result + ); +} + +#[tokio::test] +async fn token_unfreeze_sign_returns_note_too_big_error() { + let actor_id = Identifier::random(); + let unfreeze_identity_id = Identifier::random(); + let data_contract = test_data_contract("testDoc"); + let sdk = new_mock_sdk_with_contract_nonce(actor_id, data_contract.id(), 0).await; + + let result = TokenUnfreezeTransitionBuilder::new( + Arc::clone(&data_contract), + 0, + actor_id, + unfreeze_identity_id, + ) + .with_public_note("x".repeat(2049)) + .sign( + &sdk, + &test_identity_public_key(), + &TestSigner, + dpp::version::PlatformVersion::latest(), + ) + .await; + + assert!( + matches!( + result, + Err(Error::Protocol(ProtocolError::ConsensusError(ref consensus_error))) + if matches!(**consensus_error, ConsensusError::BasicError(BasicError::InvalidTokenNoteTooBigError(_))) + ), + "unexpected result: {:?}", + result + ); +} + +#[tokio::test] +async fn token_destroy_sign_returns_note_too_big_error() { + let actor_id = Identifier::random(); + let frozen_identity_id = Identifier::random(); + let data_contract = test_data_contract("testDoc"); + let sdk = new_mock_sdk_with_contract_nonce(actor_id, data_contract.id(), 0).await; + + let result = TokenDestroyFrozenFundsTransitionBuilder::new( + Arc::clone(&data_contract), + 0, + actor_id, + frozen_identity_id, + ) + .with_public_note("x".repeat(2049)) + .sign( + &sdk, + &test_identity_public_key(), + &TestSigner, + dpp::version::PlatformVersion::latest(), + ) + .await; + + assert!( + matches!( + result, + Err(Error::Protocol(ProtocolError::ConsensusError(ref consensus_error))) + if matches!(**consensus_error, ConsensusError::BasicError(BasicError::InvalidTokenNoteTooBigError(_))) + ), + "unexpected result: {:?}", + result + ); +} + +#[tokio::test] +async fn token_emergency_action_sign_returns_note_too_big_error() { + let actor_id = Identifier::random(); + let data_contract = test_data_contract("testDoc"); + let sdk = new_mock_sdk_with_contract_nonce(actor_id, data_contract.id(), 0).await; + + let result = + TokenEmergencyActionTransitionBuilder::pause(Arc::clone(&data_contract), 0, actor_id) + .with_public_note("x".repeat(2049)) + .sign( + &sdk, + &test_identity_public_key(), + &TestSigner, + dpp::version::PlatformVersion::latest(), + ) + .await; + + assert!( + matches!( + result, + Err(Error::Protocol(ProtocolError::ConsensusError(ref consensus_error))) + if matches!(**consensus_error, ConsensusError::BasicError(BasicError::InvalidTokenNoteTooBigError(_))) + ), + "unexpected result: {:?}", + result + ); +} + +#[tokio::test] +async fn token_config_update_sign_returns_no_change_error() { + let owner_id = Identifier::random(); + let data_contract = test_data_contract("testDoc"); + let sdk = new_mock_sdk_with_contract_nonce(owner_id, data_contract.id(), 0).await; + + let result = TokenConfigUpdateTransitionBuilder::new( + Arc::clone(&data_contract), + 0, + owner_id, + TokenConfigurationChangeItem::TokenConfigurationNoChange, + ) + .sign( + &sdk, + &test_identity_public_key(), + &TestSigner, + dpp::version::PlatformVersion::latest(), + ) + .await; + + assert!( + matches!( + result, + Err(Error::Protocol(ProtocolError::ConsensusError(ref consensus_error))) + if matches!(**consensus_error, ConsensusError::BasicError(BasicError::InvalidTokenConfigUpdateNoChangeError(_))) + ), + "unexpected result: {:?}", + result + ); +} + +#[tokio::test] +async fn token_config_update_sign_returns_note_too_big_error() { + let owner_id = Identifier::random(); + let data_contract = test_data_contract("testDoc"); + let sdk = new_mock_sdk_with_contract_nonce(owner_id, data_contract.id(), 0).await; + + let result = TokenConfigUpdateTransitionBuilder::new( + Arc::clone(&data_contract), + 0, + owner_id, + TokenConfigurationChangeItem::MintingAllowChoosingDestination(true), + ) + .with_public_note("x".repeat(2049)) + .sign( + &sdk, + &test_identity_public_key(), + &TestSigner, + dpp::version::PlatformVersion::latest(), + ) + .await; + + assert!( + matches!( + result, + Err(Error::Protocol(ProtocolError::ConsensusError(ref consensus_error))) + if matches!(**consensus_error, ConsensusError::BasicError(BasicError::InvalidTokenNoteTooBigError(_))) + ), + "unexpected result: {:?}", + result + ); +} + +#[tokio::test] +async fn token_claim_sign_returns_note_too_big_error() { + let owner_id = Identifier::random(); + let data_contract = test_data_contract("testDoc"); + let sdk = new_mock_sdk_with_contract_nonce(owner_id, data_contract.id(), 0).await; + + let result = TokenClaimTransitionBuilder::new( + Arc::clone(&data_contract), + 0, + owner_id, + TokenDistributionType::PreProgrammed, + ) + .with_public_note("x".repeat(2049)) + .sign( + &sdk, + &test_identity_public_key(), + &TestSigner, + dpp::version::PlatformVersion::latest(), + ) + .await; + + assert!( + matches!( + result, + Err(Error::Protocol(ProtocolError::ConsensusError(ref consensus_error))) + if matches!(**consensus_error, ConsensusError::BasicError(BasicError::InvalidTokenNoteTooBigError(_))) + ), + "unexpected result: {:?}", + result + ); +} + +#[tokio::test] +async fn token_purchase_sign_returns_invalid_token_amount_error_when_amount_is_zero() { + let actor_id = Identifier::random(); + let data_contract = test_data_contract("testDoc"); + let sdk = new_mock_sdk_with_contract_nonce(actor_id, data_contract.id(), 0).await; + + let result = + TokenDirectPurchaseTransitionBuilder::new(Arc::clone(&data_contract), 0, actor_id, 0, 1000) + .sign( + &sdk, + &test_identity_public_key(), + &TestSigner, + dpp::version::PlatformVersion::latest(), + ) + .await; + + assert!( + matches!( + result, + Err(Error::Protocol(ProtocolError::ConsensusError(ref consensus_error))) + if matches!(**consensus_error, ConsensusError::BasicError(BasicError::InvalidTokenAmountError(_))) + ), + "unexpected result: {:?}", + result + ); +} + +#[tokio::test] +async fn token_set_price_sign_returns_note_too_big_error() { + let issuer_id = Identifier::random(); + let data_contract = test_data_contract("testDoc"); + let sdk = new_mock_sdk_with_contract_nonce(issuer_id, data_contract.id(), 0).await; + + let result = TokenChangeDirectPurchasePriceTransitionBuilder::new( + Arc::clone(&data_contract), + 0, + issuer_id, + ) + .with_public_note("x".repeat(2049)) + .sign( + &sdk, + &test_identity_public_key(), + &TestSigner, + dpp::version::PlatformVersion::latest(), + ) + .await; + + assert!( + matches!( + result, + Err(Error::Protocol(ProtocolError::ConsensusError(ref consensus_error))) + if matches!(**consensus_error, ConsensusError::BasicError(BasicError::InvalidTokenNoteTooBigError(_))) + ), + "unexpected result: {:?}", + result + ); +} diff --git a/packages/rs-sdk/src/platform/tokens/builders/transfer.rs b/packages/rs-sdk/src/platform/tokens/builders/transfer.rs index aa0162021d2..572a4de17fb 100644 --- a/packages/rs-sdk/src/platform/tokens/builders/transfer.rs +++ b/packages/rs-sdk/src/platform/tokens/builders/transfer.rs @@ -211,6 +211,33 @@ impl TokenTransferTransitionBuilder { self.state_transition_creation_options, )?; + // Validate the transition structure before returning + let validation_result = match &state_transition { + StateTransition::Batch(batch_transition) => { + batch_transition.validate_base_structure(platform_version)? + } + _ => { + return Err(Error::Protocol( + dpp::ProtocolError::InvalidStateTransitionType( + "expected Batch transition".to_string(), + ), + )); + } + }; + if let Some(first_error) = validation_result.errors.into_iter().next() { + return Err(Error::Protocol(dpp::ProtocolError::ConsensusError( + Box::new(first_error), + ))); + } + Ok(state_transition) } } + +#[cfg(test)] +mod validation_tests { + #[test] + fn validate_base_structure_error_case() { + super::super::tests::assert_token_transfer_validate_base_structure_error(); + } +} diff --git a/packages/rs-sdk/src/platform/tokens/builders/unfreeze.rs b/packages/rs-sdk/src/platform/tokens/builders/unfreeze.rs index e1d3f7ef2b4..c59fac979cb 100644 --- a/packages/rs-sdk/src/platform/tokens/builders/unfreeze.rs +++ b/packages/rs-sdk/src/platform/tokens/builders/unfreeze.rs @@ -185,6 +185,33 @@ impl TokenUnfreezeTransitionBuilder { self.state_transition_creation_options, )?; + // Validate the transition structure before returning + let validation_result = match &state_transition { + StateTransition::Batch(batch_transition) => { + batch_transition.validate_base_structure(platform_version)? + } + _ => { + return Err(Error::Protocol( + dpp::ProtocolError::InvalidStateTransitionType( + "expected Batch transition".to_string(), + ), + )); + } + }; + if let Some(first_error) = validation_result.errors.into_iter().next() { + return Err(Error::Protocol(dpp::ProtocolError::ConsensusError( + Box::new(first_error), + ))); + } + Ok(state_transition) } } + +#[cfg(test)] +mod validation_tests { + #[test] + fn validate_base_structure_error_case() { + super::super::tests::assert_token_unfreeze_validate_base_structure_error(); + } +} diff --git a/packages/wasm-sdk/tests/unit/fixtures/data-contract-v0-crypto-card-game.ts b/packages/wasm-sdk/tests/unit/fixtures/data-contract-v0-crypto-card-game.ts index 9ff248509f8..d24ddb602c1 100644 --- a/packages/wasm-sdk/tests/unit/fixtures/data-contract-v0-crypto-card-game.ts +++ b/packages/wasm-sdk/tests/unit/fixtures/data-contract-v0-crypto-card-game.ts @@ -52,6 +52,7 @@ const contract = { rarity: { type: 'string', description: 'Rarity level of the card', + maxLength: 9, enum: [ 'common', 'uncommon',