diff --git a/.gitignore b/.gitignore index 040d4c2079e..0675601009f 100644 --- a/.gitignore +++ b/.gitignore @@ -88,3 +88,4 @@ book/book/ # gRPC coverage report grpc-coverage-report.txt +worktrees/ diff --git a/packages/rs-sdk/Cargo.toml b/packages/rs-sdk/Cargo.toml index ea97932d737..3b8a1d96de5 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.rs b/packages/rs-sdk/src/platform.rs index 36d42444f4c..381625058da 100644 --- a/packages/rs-sdk/src/platform.rs +++ b/packages/rs-sdk/src/platform.rs @@ -20,6 +20,8 @@ pub mod identities_contract_keys_query; pub mod query; #[cfg(feature = "shielded")] pub mod shielded; +#[cfg(test)] +pub(crate) mod test_helpers; pub mod tokens; pub mod transition; pub mod trunk_branch_sync; diff --git a/packages/rs-sdk/src/platform/documents/transitions/create.rs b/packages/rs-sdk/src/platform/documents/transitions/create.rs index 450836ae167..e9f8f6168b5 100644 --- a/packages/rs-sdk/src/platform/documents/transitions/create.rs +++ b/packages/rs-sdk/src/platform/documents/transitions/create.rs @@ -1,5 +1,6 @@ use crate::platform::transition::broadcast::BroadcastStateTransition; use crate::platform::transition::put_settings::PutSettings; +use crate::platform::transition::validation::validate_batch_base_structure; use crate::{Error, Sdk}; use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::data_contract::DataContract; @@ -166,6 +167,9 @@ impl DocumentCreateTransitionBuilder { self.state_transition_creation_options, )?; + // Validate the transition structure before returning + validate_batch_base_structure(&state_transition, platform_version)?; + Ok(state_transition) } } @@ -248,3 +252,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..ba7425a512a 100644 --- a/packages/rs-sdk/src/platform/documents/transitions/delete.rs +++ b/packages/rs-sdk/src/platform/documents/transitions/delete.rs @@ -1,5 +1,6 @@ use crate::platform::transition::broadcast::BroadcastStateTransition; use crate::platform::transition::put_settings::PutSettings; +use crate::platform::transition::validation::validate_batch_base_structure; use crate::platform::Identifier; use crate::{Error, Sdk}; use dpp::data_contract::accessors::v0::DataContractV0Getters; @@ -207,6 +208,9 @@ impl DocumentDeleteTransitionBuilder { self.state_transition_creation_options, )?; + // Validate the transition structure before returning + validate_batch_base_structure(&state_transition, platform_version)?; + Ok(state_transition) } } @@ -285,3 +289,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..0e6fc745a70 100644 --- a/packages/rs-sdk/src/platform/documents/transitions/purchase.rs +++ b/packages/rs-sdk/src/platform/documents/transitions/purchase.rs @@ -1,5 +1,6 @@ use crate::platform::transition::broadcast::BroadcastStateTransition; use crate::platform::transition::put_settings::PutSettings; +use crate::platform::transition::validation::validate_batch_base_structure; use crate::platform::Identifier; use crate::{Error, Sdk}; use dpp::data_contract::accessors::v0::DataContractV0Getters; @@ -221,6 +222,9 @@ impl DocumentPurchaseTransitionBuilder { self.state_transition_creation_options, )?; + // Validate the transition structure before returning + validate_batch_base_structure(&state_transition, platform_version)?; + Ok(state_transition) } } @@ -302,3 +306,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..69d70073bdb 100644 --- a/packages/rs-sdk/src/platform/documents/transitions/replace.rs +++ b/packages/rs-sdk/src/platform/documents/transitions/replace.rs @@ -1,5 +1,6 @@ use crate::platform::transition::broadcast::BroadcastStateTransition; use crate::platform::transition::put_settings::PutSettings; +use crate::platform::transition::validation::validate_batch_base_structure; use crate::{Error, Sdk}; use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::data_contract::DataContract; @@ -160,6 +161,9 @@ impl DocumentReplaceTransitionBuilder { self.state_transition_creation_options, )?; + // Validate the transition structure before returning + validate_batch_base_structure(&state_transition, platform_version)?; + Ok(state_transition) } } @@ -248,3 +252,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..72a845299a3 100644 --- a/packages/rs-sdk/src/platform/documents/transitions/set_price.rs +++ b/packages/rs-sdk/src/platform/documents/transitions/set_price.rs @@ -1,5 +1,6 @@ use crate::platform::transition::broadcast::BroadcastStateTransition; use crate::platform::transition::put_settings::PutSettings; +use crate::platform::transition::validation::validate_batch_base_structure; use crate::platform::Identifier; use crate::{Error, Sdk}; use dpp::data_contract::accessors::v0::DataContractV0Getters; @@ -208,6 +209,9 @@ impl DocumentSetPriceTransitionBuilder { self.state_transition_creation_options, )?; + // Validate the transition structure before returning + validate_batch_base_structure(&state_transition, platform_version)?; + Ok(state_transition) } } @@ -286,3 +290,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..fb109d8aa71 --- /dev/null +++ b/packages/rs-sdk/src/platform/documents/transitions/tests.rs @@ -0,0 +1,439 @@ +use super::create::DocumentCreateTransitionBuilder; +use super::delete::DocumentDeleteTransitionBuilder; +use super::purchase::DocumentPurchaseTransitionBuilder; +use super::replace::DocumentReplaceTransitionBuilder; +use super::set_price::DocumentSetPriceTransitionBuilder; +use super::transfer::DocumentTransferTransitionBuilder; +use crate::platform::test_helpers::{ + new_mock_sdk_with_contract_nonce, test_data_contract, test_identity_public_key, + validate_transition_like_builder, TestSigner, INVALID_NONCE, TEST_DOCUMENT_TYPE_NAME, +}; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::document::{Document, DocumentV0}; +use dpp::prelude::Identifier; +use dpp::state_transition::batch_transition::methods::v0::DocumentsBatchTransitionMethodsV0; +use dpp::state_transition::batch_transition::BatchTransition; +use std::sync::Arc; + +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, + }) +} + +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:?}"); +} + +#[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() + ); +} + +#[tokio::test] +async fn document_delete_builder_sign_succeeds_for_valid_input() { + 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(), 0).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(), + "valid input should pass validation; got error: {:?}", + result.err() + ); + let st = result.unwrap(); + assert!( + st.signature().is_some_and(|sig| !sig.is_empty()), + "transition should have a non-empty signature" + ); +} + +#[tokio::test] +async fn document_create_builder_sign_succeeds_for_valid_input() { + let data_contract = test_data_contract(TEST_DOCUMENT_TYPE_NAME); + let owner_id = Identifier::random(); + let document = test_document(owner_id); + let sdk = new_mock_sdk_with_contract_nonce(owner_id, data_contract.id(), 0).await; + + let builder = DocumentCreateTransitionBuilder::new( + Arc::clone(&data_contract), + TEST_DOCUMENT_TYPE_NAME.to_string(), + document, + [7; 32], + ); + + let result = builder + .sign( + &sdk, + &test_identity_public_key(), + &TestSigner, + dpp::version::PlatformVersion::latest(), + ) + .await; + + assert!( + result.is_ok(), + "valid input should pass validation; got error: {:?}", + result.err() + ); + let st = result.unwrap(); + assert!( + st.signature().is_some_and(|sig| !sig.is_empty()), + "transition should have a non-empty signature" + ); +} + +#[tokio::test] +async fn document_replace_builder_sign_succeeds_for_valid_input() { + let data_contract = test_data_contract(TEST_DOCUMENT_TYPE_NAME); + let owner_id = Identifier::random(); + let document = test_document(owner_id); + let sdk = new_mock_sdk_with_contract_nonce(owner_id, data_contract.id(), 0).await; + + let builder = DocumentReplaceTransitionBuilder::new( + Arc::clone(&data_contract), + TEST_DOCUMENT_TYPE_NAME.to_string(), + document, + ); + + let result = builder + .sign( + &sdk, + &test_identity_public_key(), + &TestSigner, + dpp::version::PlatformVersion::latest(), + ) + .await; + + assert!( + result.is_ok(), + "valid input should pass validation; got error: {:?}", + result.err() + ); + let st = result.unwrap(); + assert!( + st.signature().is_some_and(|sig| !sig.is_empty()), + "transition should have a non-empty signature" + ); +} + +#[tokio::test] +async fn document_purchase_builder_sign_succeeds_for_valid_input() { + 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 sdk = new_mock_sdk_with_contract_nonce(purchaser_id, data_contract.id(), 0).await; + + let builder = DocumentPurchaseTransitionBuilder::new( + Arc::clone(&data_contract), + TEST_DOCUMENT_TYPE_NAME.to_string(), + document, + purchaser_id, + 100, + ); + + let result = builder + .sign( + &sdk, + &test_identity_public_key(), + &TestSigner, + dpp::version::PlatformVersion::latest(), + ) + .await; + + assert!( + result.is_ok(), + "valid input should pass validation; got error: {:?}", + result.err() + ); + let st = result.unwrap(); + assert!( + st.signature().is_some_and(|sig| !sig.is_empty()), + "transition should have a non-empty signature" + ); +} + +#[tokio::test] +async fn document_set_price_builder_sign_succeeds_for_valid_input() { + let data_contract = test_data_contract(TEST_DOCUMENT_TYPE_NAME); + let owner_id = Identifier::random(); + let document = test_document(owner_id); + let sdk = new_mock_sdk_with_contract_nonce(owner_id, data_contract.id(), 0).await; + + let builder = DocumentSetPriceTransitionBuilder::new( + Arc::clone(&data_contract), + TEST_DOCUMENT_TYPE_NAME.to_string(), + document, + 200, + ); + + let result = builder + .sign( + &sdk, + &test_identity_public_key(), + &TestSigner, + dpp::version::PlatformVersion::latest(), + ) + .await; + + assert!( + result.is_ok(), + "valid input should pass validation; got error: {:?}", + result.err() + ); + let st = result.unwrap(); + assert!( + st.signature().is_some_and(|sig| !sig.is_empty()), + "transition should have a non-empty signature" + ); +} + +#[tokio::test] +async fn document_transfer_builder_sign_succeeds_for_valid_input() { + 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 sdk = new_mock_sdk_with_contract_nonce(owner_id, data_contract.id(), 0).await; + + let builder = DocumentTransferTransitionBuilder::new( + Arc::clone(&data_contract), + TEST_DOCUMENT_TYPE_NAME.to_string(), + document, + recipient_id, + ); + + let result = builder + .sign( + &sdk, + &test_identity_public_key(), + &TestSigner, + dpp::version::PlatformVersion::latest(), + ) + .await; + + assert!( + result.is_ok(), + "valid input should pass validation; got error: {:?}", + result.err() + ); + let st = result.unwrap(); + assert!( + st.signature().is_some_and(|sig| !sig.is_empty()), + "transition should have a non-empty signature" + ); +} diff --git a/packages/rs-sdk/src/platform/documents/transitions/transfer.rs b/packages/rs-sdk/src/platform/documents/transitions/transfer.rs index ae1f2afbb04..2571c85370e 100644 --- a/packages/rs-sdk/src/platform/documents/transitions/transfer.rs +++ b/packages/rs-sdk/src/platform/documents/transitions/transfer.rs @@ -1,5 +1,6 @@ use crate::platform::transition::broadcast::BroadcastStateTransition; use crate::platform::transition::put_settings::PutSettings; +use crate::platform::transition::validation::validate_batch_base_structure; use crate::platform::Identifier; use crate::{Error, Sdk}; use dpp::data_contract::accessors::v0::DataContractV0Getters; @@ -207,6 +208,9 @@ impl DocumentTransferTransitionBuilder { self.state_transition_creation_options, )?; + // Validate the transition structure before returning + validate_batch_base_structure(&state_transition, platform_version)?; + Ok(state_transition) } } @@ -286,3 +290,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/test_helpers.rs b/packages/rs-sdk/src/platform/test_helpers.rs new file mode 100644 index 00000000000..bffa923c832 --- /dev/null +++ b/packages/rs-sdk/src/platform/test_helpers.rs @@ -0,0 +1,141 @@ +//! Shared test infrastructure for document and token transition builder tests. + +use crate::{Error, Sdk, SdkBuilder}; +use dpp::address_funds::AddressWitness; +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::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::StateTransition; +use dpp::ProtocolError; +use drive_proof_verifier::types::IdentityContractNonceFetcher; +use std::collections::BTreeMap; +use std::sync::Arc; + +use crate::platform::transition::validation::validate_batch_base_structure; + +pub(crate) const TEST_DOCUMENT_TYPE_NAME: &str = "testDoc"; +/// Exceeds the 40-bit nonce mask (MISSING_IDENTITY_REVISIONS_FILTER), triggering +/// NonceOutOfBoundsError in validate_base_structure. +pub(crate) const INVALID_NONCE: u64 = 1_u64 << 50; + +#[derive(Debug)] +pub(crate) 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 + } +} + +pub(crate) 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, + }) +} + +pub(crate) 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) +} + +pub(crate) fn validate_transition_like_builder( + state_transition: &StateTransition, +) -> Result<(), Error> { + let platform_version = dpp::version::PlatformVersion::latest(); + validate_batch_base_structure(state_transition, platform_version) +} + +pub(crate) 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 +} diff --git a/packages/rs-sdk/src/platform/tokens/builders/burn.rs b/packages/rs-sdk/src/platform/tokens/builders/burn.rs index e714a1d4a27..04d2e3fb364 100644 --- a/packages/rs-sdk/src/platform/tokens/builders/burn.rs +++ b/packages/rs-sdk/src/platform/tokens/builders/burn.rs @@ -1,4 +1,5 @@ use crate::platform::transition::put_settings::PutSettings; +use crate::platform::transition::validation::validate_batch_base_structure; use crate::platform::Identifier; use crate::{Error, Sdk}; use dpp::balances::credits::TokenAmount; @@ -179,6 +180,17 @@ impl TokenBurnTransitionBuilder { self.state_transition_creation_options, )?; + // Validate the transition structure before returning + validate_batch_base_structure(&state_transition, platform_version)?; + 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..0a14f68f7c0 100644 --- a/packages/rs-sdk/src/platform/tokens/builders/claim.rs +++ b/packages/rs-sdk/src/platform/tokens/builders/claim.rs @@ -1,4 +1,5 @@ use crate::platform::transition::put_settings::PutSettings; +use crate::platform::transition::validation::validate_batch_base_structure; use crate::platform::Identifier; use crate::{Error, Sdk}; use dpp::data_contract::accessors::v0::DataContractV0Getters; @@ -165,6 +166,17 @@ impl TokenClaimTransitionBuilder { self.state_transition_creation_options, )?; + // Validate the transition structure before returning + validate_batch_base_structure(&state_transition, platform_version)?; + 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 960ad87470c..814cb431148 100644 --- a/packages/rs-sdk/src/platform/tokens/builders/config_update.rs +++ b/packages/rs-sdk/src/platform/tokens/builders/config_update.rs @@ -1,4 +1,5 @@ use crate::platform::transition::put_settings::PutSettings; +use crate::platform::transition::validation::validate_batch_base_structure; use crate::platform::Identifier; use crate::{Error, Sdk}; use dpp::data_contract::accessors::v0::DataContractV0Getters; @@ -186,6 +187,17 @@ impl TokenConfigUpdateTransitionBuilder { self.state_transition_creation_options, )?; + // Validate the transition structure before returning + validate_batch_base_structure(&state_transition, platform_version)?; + 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 b1686785a9b..f8873b0082d 100644 --- a/packages/rs-sdk/src/platform/tokens/builders/destroy.rs +++ b/packages/rs-sdk/src/platform/tokens/builders/destroy.rs @@ -1,4 +1,5 @@ use crate::platform::transition::put_settings::PutSettings; +use crate::platform::transition::validation::validate_batch_base_structure; use crate::platform::Identifier; use crate::{Error, Sdk}; use dpp::data_contract::accessors::v0::DataContractV0Getters; @@ -185,6 +186,17 @@ impl TokenDestroyFrozenFundsTransitionBuilder { self.state_transition_creation_options, )?; + // Validate the transition structure before returning + validate_batch_base_structure(&state_transition, platform_version)?; + 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 00b052b1145..8bbad7c4f50 100644 --- a/packages/rs-sdk/src/platform/tokens/builders/emergency_action.rs +++ b/packages/rs-sdk/src/platform/tokens/builders/emergency_action.rs @@ -1,4 +1,5 @@ use crate::platform::transition::put_settings::PutSettings; +use crate::platform::transition::validation::validate_batch_base_structure; use crate::platform::Identifier; use crate::{Error, Sdk}; use dpp::data_contract::accessors::v0::DataContractV0Getters; @@ -213,6 +214,17 @@ impl TokenEmergencyActionTransitionBuilder { self.state_transition_creation_options, )?; + // Validate the transition structure before returning + validate_batch_base_structure(&state_transition, platform_version)?; + 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 12c4c9ad229..e2e618e2927 100644 --- a/packages/rs-sdk/src/platform/tokens/builders/freeze.rs +++ b/packages/rs-sdk/src/platform/tokens/builders/freeze.rs @@ -1,4 +1,5 @@ use crate::platform::transition::put_settings::PutSettings; +use crate::platform::transition::validation::validate_batch_base_structure; use crate::platform::Identifier; use crate::{Error, Sdk}; use dpp::data_contract::accessors::v0::DataContractV0Getters; @@ -185,6 +186,17 @@ impl TokenFreezeTransitionBuilder { self.state_transition_creation_options, )?; + // Validate the transition structure before returning + validate_batch_base_structure(&state_transition, platform_version)?; + 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 45614d06bd3..ba9b6d30926 100644 --- a/packages/rs-sdk/src/platform/tokens/builders/mint.rs +++ b/packages/rs-sdk/src/platform/tokens/builders/mint.rs @@ -1,4 +1,5 @@ use crate::platform::transition::put_settings::PutSettings; +use crate::platform::transition::validation::validate_batch_base_structure; use crate::platform::Identifier; use crate::{Error, Sdk}; use dpp::balances::credits::TokenAmount; @@ -206,6 +207,17 @@ impl TokenMintTransitionBuilder { self.state_transition_creation_options, )?; + // Validate the transition structure before returning + validate_batch_base_structure(&state_transition, platform_version)?; + 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 67771fc1ae9..d7fff044334 100644 --- a/packages/rs-sdk/src/platform/tokens/builders/purchase.rs +++ b/packages/rs-sdk/src/platform/tokens/builders/purchase.rs @@ -1,4 +1,5 @@ use crate::platform::transition::put_settings::PutSettings; +use crate::platform::transition::validation::validate_batch_base_structure; use crate::platform::Identifier; use crate::{Error, Sdk}; use dpp::balances::credits::TokenAmount; @@ -154,6 +155,17 @@ impl TokenDirectPurchaseTransitionBuilder { self.state_transition_creation_options, )?; + // Validate the transition structure before returning + validate_batch_base_structure(&state_transition, platform_version)?; + 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 57c298d3b61..50691fdffb7 100644 --- a/packages/rs-sdk/src/platform/tokens/builders/set_price.rs +++ b/packages/rs-sdk/src/platform/tokens/builders/set_price.rs @@ -1,4 +1,5 @@ use crate::platform::transition::put_settings::PutSettings; +use crate::platform::transition::validation::validate_batch_base_structure; use crate::platform::Identifier; use crate::{Error, Sdk}; use dpp::balances::credits::Credits; @@ -230,6 +231,17 @@ impl TokenChangeDirectPurchasePriceTransitionBuilder { self.state_transition_creation_options, )?; + // Validate the transition structure before returning + validate_batch_base_structure(&state_transition, platform_version)?; + 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..0697bc04a88 --- /dev/null +++ b/packages/rs-sdk/src/platform/tokens/builders/tests.rs @@ -0,0 +1,1141 @@ +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::platform::test_helpers::{ + new_mock_sdk_with_contract_nonce, test_data_contract, test_identity_public_key, + validate_transition_like_builder, TestSigner, INVALID_NONCE, TEST_DOCUMENT_TYPE_NAME, +}; +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::group::{GroupStateTransitionInfo, GroupStateTransitionInfoStatus}; +use dpp::prelude::Identifier; +use dpp::state_transition::batch_transition::methods::v1::DocumentsBatchTransitionMethodsV1; +use dpp::state_transition::batch_transition::BatchTransition; +use dpp::tokens::calculate_token_id; +use dpp::tokens::emergency_action::TokenEmergencyAction; +use dpp::tokens::token_pricing_schedule::TokenPricingSchedule; +use dpp::ProtocolError; +use std::sync::Arc; + +const TEST_TOKEN_POSITION: u16 = 0; + +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:?}"); +} + +#[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(crate::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(crate::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(crate::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(crate::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(crate::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(crate::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(crate::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(crate::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(crate::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(crate::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(crate::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(crate::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(crate::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(crate::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(crate::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(crate::Error::Protocol(ProtocolError::ConsensusError(ref consensus_error))) + if matches!(**consensus_error, ConsensusError::BasicError(BasicError::InvalidTokenNoteTooBigError(_))) + ), + "unexpected result: {:?}", + result + ); +} + +// ── Happy-path sign() tests ──────────────────────────────────────────── + +#[tokio::test] +async fn token_mint_sign_succeeds_for_valid_input() { + 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, 1) + .issued_to_identity_id(Identifier::random()) + .sign( + &sdk, + &test_identity_public_key(), + &TestSigner, + dpp::version::PlatformVersion::latest(), + ) + .await; + + assert!( + result.is_ok(), + "valid input should pass validation; got error: {:?}", + result.err() + ); + let st = result.unwrap(); + assert!( + st.signature().is_some_and(|sig| !sig.is_empty()), + "transition should have a non-empty signature" + ); +} + +#[tokio::test] +async fn token_burn_sign_succeeds_for_valid_input() { + let owner_id = Identifier::random(); + let data_contract = test_data_contract(TEST_DOCUMENT_TYPE_NAME); + 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) + .sign( + &sdk, + &test_identity_public_key(), + &TestSigner, + dpp::version::PlatformVersion::latest(), + ) + .await; + + assert!( + result.is_ok(), + "valid input should pass validation; got error: {:?}", + result.err() + ); + let st = result.unwrap(); + assert!( + st.signature().is_some_and(|sig| !sig.is_empty()), + "transition should have a non-empty signature" + ); +} + +#[tokio::test] +async fn token_transfer_sign_succeeds_for_valid_input() { + let sender_id = Identifier::random(); + let recipient_id = Identifier::random(); + let data_contract = test_data_contract(TEST_DOCUMENT_TYPE_NAME); + 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, + ) + .sign( + &sdk, + &test_identity_public_key(), + &TestSigner, + dpp::version::PlatformVersion::latest(), + ) + .await; + + assert!( + result.is_ok(), + "valid input should pass validation; got error: {:?}", + result.err() + ); + let st = result.unwrap(); + assert!( + st.signature().is_some_and(|sig| !sig.is_empty()), + "transition should have a non-empty signature" + ); +} + +#[tokio::test] +async fn token_freeze_sign_succeeds_for_valid_input() { + let actor_id = Identifier::random(); + let freeze_identity_id = Identifier::random(); + let data_contract = test_data_contract(TEST_DOCUMENT_TYPE_NAME); + 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, + ) + .sign( + &sdk, + &test_identity_public_key(), + &TestSigner, + dpp::version::PlatformVersion::latest(), + ) + .await; + + assert!( + result.is_ok(), + "valid input should pass validation; got error: {:?}", + result.err() + ); + let st = result.unwrap(); + assert!( + st.signature().is_some_and(|sig| !sig.is_empty()), + "transition should have a non-empty signature" + ); +} + +#[tokio::test] +async fn token_unfreeze_sign_succeeds_for_valid_input() { + let actor_id = Identifier::random(); + let unfreeze_identity_id = Identifier::random(); + let data_contract = test_data_contract(TEST_DOCUMENT_TYPE_NAME); + 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, + ) + .sign( + &sdk, + &test_identity_public_key(), + &TestSigner, + dpp::version::PlatformVersion::latest(), + ) + .await; + + assert!( + result.is_ok(), + "valid input should pass validation; got error: {:?}", + result.err() + ); + let st = result.unwrap(); + assert!( + st.signature().is_some_and(|sig| !sig.is_empty()), + "transition should have a non-empty signature" + ); +} + +#[tokio::test] +async fn token_destroy_sign_succeeds_for_valid_input() { + let actor_id = Identifier::random(); + let frozen_identity_id = Identifier::random(); + let data_contract = test_data_contract(TEST_DOCUMENT_TYPE_NAME); + 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, + ) + .sign( + &sdk, + &test_identity_public_key(), + &TestSigner, + dpp::version::PlatformVersion::latest(), + ) + .await; + + assert!( + result.is_ok(), + "valid input should pass validation; got error: {:?}", + result.err() + ); + let st = result.unwrap(); + assert!( + st.signature().is_some_and(|sig| !sig.is_empty()), + "transition should have a non-empty signature" + ); +} + +#[tokio::test] +async fn token_emergency_action_sign_succeeds_for_valid_input() { + let actor_id = Identifier::random(); + let data_contract = test_data_contract(TEST_DOCUMENT_TYPE_NAME); + 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) + .sign( + &sdk, + &test_identity_public_key(), + &TestSigner, + dpp::version::PlatformVersion::latest(), + ) + .await; + + assert!( + result.is_ok(), + "valid input should pass validation; got error: {:?}", + result.err() + ); + let st = result.unwrap(); + assert!( + st.signature().is_some_and(|sig| !sig.is_empty()), + "transition should have a non-empty signature" + ); +} + +#[tokio::test] +async fn token_config_update_sign_succeeds_for_valid_input() { + let owner_id = Identifier::random(); + let data_contract = test_data_contract(TEST_DOCUMENT_TYPE_NAME); + 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), + ) + .sign( + &sdk, + &test_identity_public_key(), + &TestSigner, + dpp::version::PlatformVersion::latest(), + ) + .await; + + assert!( + result.is_ok(), + "valid input should pass validation; got error: {:?}", + result.err() + ); + let st = result.unwrap(); + assert!( + st.signature().is_some_and(|sig| !sig.is_empty()), + "transition should have a non-empty signature" + ); +} + +#[tokio::test] +async fn token_claim_sign_succeeds_for_valid_input() { + let owner_id = Identifier::random(); + let data_contract = test_data_contract(TEST_DOCUMENT_TYPE_NAME); + 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, + ) + .sign( + &sdk, + &test_identity_public_key(), + &TestSigner, + dpp::version::PlatformVersion::latest(), + ) + .await; + + assert!( + result.is_ok(), + "valid input should pass validation; got error: {:?}", + result.err() + ); + let st = result.unwrap(); + assert!( + st.signature().is_some_and(|sig| !sig.is_empty()), + "transition should have a non-empty signature" + ); +} + +#[tokio::test] +async fn token_purchase_sign_succeeds_for_valid_input() { + let actor_id = Identifier::random(); + let data_contract = test_data_contract(TEST_DOCUMENT_TYPE_NAME); + 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, 1, 1000) + .sign( + &sdk, + &test_identity_public_key(), + &TestSigner, + dpp::version::PlatformVersion::latest(), + ) + .await; + + assert!( + result.is_ok(), + "valid input should pass validation; got error: {:?}", + result.err() + ); + let st = result.unwrap(); + assert!( + st.signature().is_some_and(|sig| !sig.is_empty()), + "transition should have a non-empty signature" + ); +} + +#[tokio::test] +async fn token_set_price_sign_succeeds_for_valid_input() { + 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 = TokenChangeDirectPurchasePriceTransitionBuilder::new( + Arc::clone(&data_contract), + 0, + issuer_id, + ) + .sign( + &sdk, + &test_identity_public_key(), + &TestSigner, + dpp::version::PlatformVersion::latest(), + ) + .await; + + assert!( + result.is_ok(), + "valid input should pass validation; got error: {:?}", + result.err() + ); + let st = result.unwrap(); + assert!( + st.signature().is_some_and(|sig| !sig.is_empty()), + "transition should have a non-empty signature" + ); +} diff --git a/packages/rs-sdk/src/platform/tokens/builders/transfer.rs b/packages/rs-sdk/src/platform/tokens/builders/transfer.rs index a8cf0514e5c..bd409de9eb4 100644 --- a/packages/rs-sdk/src/platform/tokens/builders/transfer.rs +++ b/packages/rs-sdk/src/platform/tokens/builders/transfer.rs @@ -1,4 +1,5 @@ use crate::platform::transition::put_settings::PutSettings; +use crate::platform::transition::validation::validate_batch_base_structure; use crate::platform::Identifier; use crate::{Error, Sdk}; use dpp::balances::credits::TokenAmount; @@ -211,6 +212,17 @@ impl TokenTransferTransitionBuilder { self.state_transition_creation_options, )?; + // Validate the transition structure before returning + validate_batch_base_structure(&state_transition, platform_version)?; + 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 8cf605f36a9..1a5c4b9e312 100644 --- a/packages/rs-sdk/src/platform/tokens/builders/unfreeze.rs +++ b/packages/rs-sdk/src/platform/tokens/builders/unfreeze.rs @@ -1,4 +1,5 @@ use crate::platform::transition::put_settings::PutSettings; +use crate::platform::transition::validation::validate_batch_base_structure; use crate::platform::Identifier; use crate::{Error, Sdk}; use dpp::data_contract::accessors::v0::DataContractV0Getters; @@ -185,6 +186,17 @@ impl TokenUnfreezeTransitionBuilder { self.state_transition_creation_options, )?; + // Validate the transition structure before returning + validate_batch_base_structure(&state_transition, platform_version)?; + 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/rs-sdk/src/platform/transition/validation.rs b/packages/rs-sdk/src/platform/transition/validation.rs index 846d9ddae2d..fa136744a29 100644 --- a/packages/rs-sdk/src/platform/transition/validation.rs +++ b/packages/rs-sdk/src/platform/transition/validation.rs @@ -3,8 +3,44 @@ use dpp::{ consensus::{basic::BasicError, ConsensusError}, state_transition::{StateTransition, StateTransitionStructureValidation}, version::PlatformVersion, + ProtocolError, }; +/// Validates the base structure of a Batch state transition. +/// +/// Used by document and token transition builders to validate the constructed +/// `BatchTransition` before returning it to the caller. Catches invalid +/// transitions early with clear errors instead of confusing network rejections. +pub(crate) fn validate_batch_base_structure( + state_transition: &StateTransition, + platform_version: &PlatformVersion, +) -> Result<(), Error> { + let validation_result = match state_transition { + StateTransition::Batch(batch_transition) => { + batch_transition.validate_base_structure(platform_version)? + } + _ => { + return Err(Error::Protocol(ProtocolError::InvalidStateTransitionType( + "expected Batch transition".to_string(), + ))); + } + }; + let mut errors = validation_result.errors.into_iter(); + if let Some(first_error) = errors.next() { + // Log any additional errors that won't be reported + for additional_error in errors { + tracing::warn!( + ?additional_error, + "additional validation error dropped (only first error is reported)" + ); + } + return Err(Error::Protocol(ProtocolError::ConsensusError(Box::new( + first_error, + )))); + } + Ok(()) +} + /// Checks if an error is an UnsupportedFeatureError fn is_unsupported_feature_error(error: &ConsensusError) -> bool { matches!( 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 bbbf2915719..411c0c1fdf0 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',