diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition.rs index ea5592840dc..3597106f192 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition.rs @@ -309,3 +309,313 @@ impl DocumentTransitionV0Methods for DocumentTransition { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::state_transition::batch_transition::batched_transition::document_create_transition::DocumentCreateTransitionV0; + use crate::state_transition::batch_transition::batched_transition::document_delete_transition::DocumentDeleteTransitionV0; + use crate::state_transition::batch_transition::batched_transition::document_purchase_transition::DocumentPurchaseTransitionV0; + use crate::state_transition::batch_transition::batched_transition::document_replace_transition::DocumentReplaceTransitionV0; + use crate::state_transition::batch_transition::batched_transition::document_transfer_transition::DocumentTransferTransitionV0; + use crate::state_transition::batch_transition::batched_transition::document_update_price_transition::DocumentUpdatePriceTransitionV0; + use crate::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; + use platform_value::Value; + + fn make_base() -> DocumentBaseTransition { + DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: Identifier::default(), + identity_contract_nonce: 1, + document_type_name: "test_doc".to_string(), + data_contract_id: Identifier::default(), + }) + } + + fn make_create_transition(data: BTreeMap) -> DocumentTransition { + DocumentTransition::Create(DocumentCreateTransition::V0(DocumentCreateTransitionV0 { + base: make_base(), + entropy: [0u8; 32], + data, + prefunded_voting_balance: None, + })) + } + + fn make_replace_transition(data: BTreeMap) -> DocumentTransition { + DocumentTransition::Replace(DocumentReplaceTransition::V0(DocumentReplaceTransitionV0 { + base: make_base(), + revision: 2, + data, + })) + } + + fn make_delete_transition() -> DocumentTransition { + DocumentTransition::Delete(DocumentDeleteTransition::V0(DocumentDeleteTransitionV0 { + base: make_base(), + })) + } + + fn make_transfer_transition() -> DocumentTransition { + DocumentTransition::Transfer(DocumentTransferTransition::V0( + DocumentTransferTransitionV0 { + base: make_base(), + revision: 3, + recipient_owner_id: Identifier::from([5u8; 32]), + }, + )) + } + + fn make_update_price_transition() -> DocumentTransition { + DocumentTransition::UpdatePrice(DocumentUpdatePriceTransition::V0( + DocumentUpdatePriceTransitionV0 { + base: make_base(), + revision: 4, + price: 100, + }, + )) + } + + fn make_purchase_transition() -> DocumentTransition { + DocumentTransition::Purchase(DocumentPurchaseTransition::V0( + DocumentPurchaseTransitionV0 { + base: make_base(), + revision: 5, + price: 200, + }, + )) + } + + // ----------------------------------------------------------------------- + // get_dynamic_property + // ----------------------------------------------------------------------- + + #[test] + fn get_dynamic_property_returns_value_for_create() { + let mut data = BTreeMap::new(); + data.insert("myField".to_string(), Value::Text("hello".to_string())); + let transition = make_create_transition(data); + + let result = transition.get_dynamic_property("myField"); + assert_eq!(result, Some(&Value::Text("hello".to_string()))); + } + + #[test] + fn get_dynamic_property_returns_value_for_replace() { + let mut data = BTreeMap::new(); + data.insert("count".to_string(), Value::U64(42)); + let transition = make_replace_transition(data); + + let result = transition.get_dynamic_property("count"); + assert_eq!(result, Some(&Value::U64(42))); + } + + #[test] + fn get_dynamic_property_returns_none_for_missing_key_on_create() { + let transition = make_create_transition(BTreeMap::new()); + assert!(transition.get_dynamic_property("nonexistent").is_none()); + } + + #[test] + fn get_dynamic_property_returns_none_for_delete() { + let transition = make_delete_transition(); + assert!(transition.get_dynamic_property("anything").is_none()); + } + + #[test] + fn get_dynamic_property_returns_none_for_transfer() { + let transition = make_transfer_transition(); + assert!(transition.get_dynamic_property("anything").is_none()); + } + + #[test] + fn get_dynamic_property_returns_none_for_update_price() { + let transition = make_update_price_transition(); + assert!(transition.get_dynamic_property("anything").is_none()); + } + + #[test] + fn get_dynamic_property_returns_none_for_purchase() { + let transition = make_purchase_transition(); + assert!(transition.get_dynamic_property("anything").is_none()); + } + + // ----------------------------------------------------------------------- + // entropy + // ----------------------------------------------------------------------- + + #[test] + fn entropy_returns_some_for_create() { + let transition = make_create_transition(BTreeMap::new()); + let entropy = transition.entropy(); + assert!(entropy.is_some()); + assert_eq!(entropy.unwrap().len(), 32); + } + + #[test] + fn entropy_returns_none_for_replace() { + let transition = make_replace_transition(BTreeMap::new()); + assert!(transition.entropy().is_none()); + } + + #[test] + fn entropy_returns_none_for_delete() { + let transition = make_delete_transition(); + assert!(transition.entropy().is_none()); + } + + #[test] + fn entropy_returns_none_for_transfer() { + let transition = make_transfer_transition(); + assert!(transition.entropy().is_none()); + } + + #[test] + fn entropy_returns_none_for_update_price() { + let transition = make_update_price_transition(); + assert!(transition.entropy().is_none()); + } + + #[test] + fn entropy_returns_none_for_purchase() { + let transition = make_purchase_transition(); + assert!(transition.entropy().is_none()); + } + + // ----------------------------------------------------------------------- + // data + // ----------------------------------------------------------------------- + + #[test] + fn data_returns_some_for_create() { + let mut d = BTreeMap::new(); + d.insert("key".to_string(), Value::Bool(true)); + let transition = make_create_transition(d.clone()); + assert_eq!(transition.data(), Some(&d)); + } + + #[test] + fn data_returns_some_for_replace() { + let mut d = BTreeMap::new(); + d.insert("key2".to_string(), Value::U64(99)); + let transition = make_replace_transition(d.clone()); + assert_eq!(transition.data(), Some(&d)); + } + + #[test] + fn data_returns_none_for_delete() { + assert!(make_delete_transition().data().is_none()); + } + + #[test] + fn data_returns_none_for_transfer() { + assert!(make_transfer_transition().data().is_none()); + } + + #[test] + fn data_returns_none_for_update_price() { + assert!(make_update_price_transition().data().is_none()); + } + + #[test] + fn data_returns_none_for_purchase() { + assert!(make_purchase_transition().data().is_none()); + } + + // ----------------------------------------------------------------------- + // revision + // ----------------------------------------------------------------------- + + #[test] + fn revision_returns_1_for_create() { + let transition = make_create_transition(BTreeMap::new()); + assert_eq!(transition.revision(), Some(1)); + } + + #[test] + fn revision_returns_value_for_replace() { + let transition = make_replace_transition(BTreeMap::new()); + assert_eq!(transition.revision(), Some(2)); + } + + #[test] + fn revision_returns_none_for_delete() { + assert!(make_delete_transition().revision().is_none()); + } + + #[test] + fn revision_returns_value_for_transfer() { + let transition = make_transfer_transition(); + assert_eq!(transition.revision(), Some(3)); + } + + #[test] + fn revision_returns_value_for_update_price() { + let transition = make_update_price_transition(); + assert_eq!(transition.revision(), Some(4)); + } + + #[test] + fn revision_returns_value_for_purchase() { + let transition = make_purchase_transition(); + assert_eq!(transition.revision(), Some(5)); + } + + // ----------------------------------------------------------------------- + // insert_dynamic_property (cfg(test) only) + // ----------------------------------------------------------------------- + + #[test] + fn insert_dynamic_property_works_on_create() { + let mut transition = make_create_transition(BTreeMap::new()); + transition.insert_dynamic_property("added".to_string(), Value::Bool(true)); + assert_eq!( + transition.get_dynamic_property("added"), + Some(&Value::Bool(true)) + ); + } + + #[test] + fn insert_dynamic_property_works_on_replace() { + let mut transition = make_replace_transition(BTreeMap::new()); + transition.insert_dynamic_property("added".to_string(), Value::U64(7)); + assert_eq!( + transition.get_dynamic_property("added"), + Some(&Value::U64(7)) + ); + } + + #[test] + fn insert_dynamic_property_is_noop_on_delete() { + let mut transition = make_delete_transition(); + transition.insert_dynamic_property("added".to_string(), Value::Bool(true)); + // Should still return None because delete has no data + assert!(transition.get_dynamic_property("added").is_none()); + } + + // ----------------------------------------------------------------------- + // data_mut + // ----------------------------------------------------------------------- + + #[test] + fn data_mut_returns_some_for_create_and_replace() { + let mut create = make_create_transition(BTreeMap::new()); + assert!(create.data_mut().is_some()); + + let mut replace = make_replace_transition(BTreeMap::new()); + assert!(replace.data_mut().is_some()); + } + + #[test] + fn data_mut_returns_none_for_other_variants() { + let mut delete = make_delete_transition(); + assert!(delete.data_mut().is_none()); + + let mut transfer = make_transfer_transition(); + assert!(transfer.data_mut().is_none()); + + let mut update_price = make_update_price_transition(); + assert!(update_price.data_mut().is_none()); + + let mut purchase = make_purchase_transition(); + assert!(purchase.data_mut().is_none()); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_transition/validate_structure/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_transition/validate_structure/v0/mod.rs index 3dda358b00d..8e1d07825b4 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_transition/validate_structure/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_transition/validate_structure/v0/mod.rs @@ -51,3 +51,156 @@ impl TokenBurnTransitionActionStructureValidationV0 for TokenBurnTransition { Ok(SimpleConsensusValidationResult::default()) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::group::GroupStateTransitionInfo; + use crate::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; + use crate::state_transition::batch_transition::token_burn_transition::TokenBurnTransitionV0; + use platform_value::Identifier; + + fn make_base() -> TokenBaseTransition { + TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: Identifier::default(), + token_id: Identifier::new([1u8; 32]), + using_group_info: None, + }) + } + + fn make_base_with_group(is_proposer: bool) -> TokenBaseTransition { + TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: Identifier::default(), + token_id: Identifier::new([1u8; 32]), + using_group_info: Some(GroupStateTransitionInfo { + group_contract_position: 0, + action_id: Identifier::new([10u8; 32]), + action_is_proposer: is_proposer, + }), + }) + } + + fn make_valid_burn() -> TokenBurnTransition { + TokenBurnTransition::V0(TokenBurnTransitionV0 { + base: make_base(), + burn_amount: 500, + public_note: None, + }) + } + + #[test] + fn valid_burn_passes() { + let transition = make_valid_burn(); + let result = transition.validate_structure_v0().unwrap(); + assert!(result.is_valid(), "expected no errors: {:?}", result.errors); + } + + #[test] + fn zero_burn_amount_returns_invalid_token_amount_error() { + let transition = TokenBurnTransition::V0(TokenBurnTransitionV0 { + base: make_base(), + burn_amount: 0, + public_note: None, + }); + let result = transition.validate_structure_v0().unwrap(); + assert_eq!(result.errors.len(), 1); + assert!(matches!( + &result.errors[0], + ConsensusError::BasicError(BasicError::InvalidTokenAmountError(_)) + )); + } + + #[test] + fn burn_amount_exceeding_max_returns_invalid_token_amount_error() { + let transition = TokenBurnTransition::V0(TokenBurnTransitionV0 { + base: make_base(), + burn_amount: MAX_DISTRIBUTION_PARAM + 1, + public_note: None, + }); + let result = transition.validate_structure_v0().unwrap(); + assert_eq!(result.errors.len(), 1); + assert!(matches!( + &result.errors[0], + ConsensusError::BasicError(BasicError::InvalidTokenAmountError(_)) + )); + } + + #[test] + fn burn_amount_at_max_distribution_param_passes() { + let transition = TokenBurnTransition::V0(TokenBurnTransitionV0 { + base: make_base(), + burn_amount: MAX_DISTRIBUTION_PARAM, + public_note: None, + }); + let result = transition.validate_structure_v0().unwrap(); + assert!(result.is_valid(), "expected no errors: {:?}", result.errors); + } + + #[test] + fn public_note_too_big_returns_error() { + let transition = TokenBurnTransition::V0(TokenBurnTransitionV0 { + base: make_base(), + burn_amount: 100, + public_note: Some("x".repeat(MAX_TOKEN_NOTE_LEN + 1)), + }); + let result = transition.validate_structure_v0().unwrap(); + assert_eq!(result.errors.len(), 1); + assert!(matches!( + &result.errors[0], + ConsensusError::BasicError(BasicError::InvalidTokenNoteTooBigError(_)) + )); + } + + #[test] + fn public_note_at_max_length_passes() { + let transition = TokenBurnTransition::V0(TokenBurnTransitionV0 { + base: make_base(), + burn_amount: 100, + public_note: Some("x".repeat(MAX_TOKEN_NOTE_LEN)), + }); + let result = transition.validate_structure_v0().unwrap(); + assert!(result.is_valid(), "expected no errors: {:?}", result.errors); + } + + #[test] + fn note_on_non_proposer_returns_error() { + let transition = TokenBurnTransition::V0(TokenBurnTransitionV0 { + base: make_base_with_group(false), + burn_amount: 100, + public_note: Some("a valid note".to_string()), + }); + let result = transition.validate_structure_v0().unwrap(); + assert_eq!(result.errors.len(), 1); + assert!(matches!( + &result.errors[0], + ConsensusError::BasicError(BasicError::TokenNoteOnlyAllowedWhenProposerError(_)) + )); + } + + #[test] + fn note_on_proposer_passes() { + let transition = TokenBurnTransition::V0(TokenBurnTransitionV0 { + base: make_base_with_group(true), + burn_amount: 100, + public_note: Some("a valid note".to_string()), + }); + let result = transition.validate_structure_v0().unwrap(); + assert!(result.is_valid(), "expected no errors: {:?}", result.errors); + } + + #[test] + fn note_without_group_info_passes() { + let transition = TokenBurnTransition::V0(TokenBurnTransitionV0 { + base: make_base(), + burn_amount: 100, + public_note: Some("a valid note".to_string()), + }); + let result = transition.validate_structure_v0().unwrap(); + assert!(result.is_valid(), "expected no errors: {:?}", result.errors); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_claim_transition/validate_structure/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_claim_transition/validate_structure/v0/mod.rs index 6bd29b50c81..81cdb04795a 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_claim_transition/validate_structure/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_claim_transition/validate_structure/v0/mod.rs @@ -28,3 +28,62 @@ impl TokenClaimTransitionActionStructureValidationV0 for TokenClaimTransition { Ok(SimpleConsensusValidationResult::default()) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::data_contract::associated_token::token_distribution_key::TokenDistributionType; + use crate::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; + use crate::state_transition::batch_transition::token_claim_transition::v0::TokenClaimTransitionV0; + use platform_value::Identifier; + + fn make_transition(public_note: Option) -> TokenClaimTransition { + TokenClaimTransition::V0(TokenClaimTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: Identifier::default(), + token_id: Identifier::default(), + using_group_info: None, + }), + distribution_type: TokenDistributionType::PreProgrammed, + public_note, + }) + } + + #[test] + fn should_pass_with_no_public_note() { + let transition = make_transition(None); + let result = transition.validate_structure_v0().unwrap(); + assert!(result.is_valid()); + } + + #[test] + fn should_pass_with_short_public_note() { + let transition = make_transition(Some("hello".to_string())); + let result = transition.validate_structure_v0().unwrap(); + assert!(result.is_valid()); + } + + #[test] + fn should_pass_with_note_at_max_length() { + let note = "a".repeat(MAX_TOKEN_NOTE_LEN); + let transition = make_transition(Some(note)); + let result = transition.validate_structure_v0().unwrap(); + assert!(result.is_valid()); + } + + #[test] + fn should_return_error_when_public_note_too_big() { + let long_note = "a".repeat(MAX_TOKEN_NOTE_LEN + 1); + let transition = make_transition(Some(long_note)); + let result = transition.validate_structure_v0().unwrap(); + assert!(!result.is_valid()); + let error = result.errors.first().unwrap(); + assert!(matches!( + error, + ConsensusError::BasicError(BasicError::InvalidTokenNoteTooBigError(_)) + )); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/validate_structure/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/validate_structure/v0/mod.rs index 010369f8b3a..6ffaae914f8 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/validate_structure/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/validate_structure/v0/mod.rs @@ -75,3 +75,180 @@ impl TokenConfigUpdateTransitionStructureValidationV0 for TokenConfigUpdateTrans Ok(SimpleConsensusValidationResult::default()) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::group::GroupStateTransitionInfo; + use crate::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; + use crate::state_transition::batch_transition::token_config_update_transition::TokenConfigUpdateTransitionV0; + use platform_value::Identifier; + + fn make_base() -> TokenBaseTransition { + TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: Identifier::default(), + token_id: Identifier::new([1u8; 32]), + using_group_info: None, + }) + } + + fn make_base_with_group(is_proposer: bool) -> TokenBaseTransition { + TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: Identifier::default(), + token_id: Identifier::new([1u8; 32]), + using_group_info: Some(GroupStateTransitionInfo { + group_contract_position: 0, + action_id: Identifier::new([10u8; 32]), + action_is_proposer: is_proposer, + }), + }) + } + + fn platform_version() -> &'static PlatformVersion { + PlatformVersion::latest() + } + + fn make_valid_config_update() -> TokenConfigUpdateTransition { + TokenConfigUpdateTransition::V0(TokenConfigUpdateTransitionV0 { + base: make_base(), + update_token_configuration_item: TokenConfigurationChangeItem::MaxSupply(Some( + 1_000_000, + )), + public_note: None, + }) + } + + #[test] + fn valid_config_update_passes() { + let transition = make_valid_config_update(); + let result = transition + .validate_structure_v0(platform_version()) + .unwrap(); + assert!(result.is_valid(), "expected no errors: {:?}", result.errors); + } + + #[test] + fn no_change_config_returns_error() { + let transition = TokenConfigUpdateTransition::V0(TokenConfigUpdateTransitionV0 { + base: make_base(), + update_token_configuration_item: + TokenConfigurationChangeItem::TokenConfigurationNoChange, + public_note: None, + }); + let result = transition + .validate_structure_v0(platform_version()) + .unwrap(); + assert_eq!(result.errors.len(), 1); + assert!(matches!( + &result.errors[0], + ConsensusError::BasicError(BasicError::InvalidTokenConfigUpdateNoChangeError(_)) + )); + } + + #[test] + fn perpetual_distribution_returns_unsupported_feature_error() { + let transition = TokenConfigUpdateTransition::V0(TokenConfigUpdateTransitionV0 { + base: make_base(), + update_token_configuration_item: TokenConfigurationChangeItem::PerpetualDistribution( + None, + ), + public_note: None, + }); + let result = transition + .validate_structure_v0(platform_version()) + .unwrap(); + assert_eq!(result.errors.len(), 1); + assert!(matches!( + &result.errors[0], + ConsensusError::BasicError(BasicError::UnsupportedFeatureError(_)) + )); + } + + #[test] + fn public_note_too_big_returns_error() { + let transition = TokenConfigUpdateTransition::V0(TokenConfigUpdateTransitionV0 { + base: make_base(), + update_token_configuration_item: TokenConfigurationChangeItem::MaxSupply(Some( + 1_000_000, + )), + public_note: Some("x".repeat(MAX_TOKEN_NOTE_LEN + 1)), + }); + let result = transition + .validate_structure_v0(platform_version()) + .unwrap(); + assert_eq!(result.errors.len(), 1); + assert!(matches!( + &result.errors[0], + ConsensusError::BasicError(BasicError::InvalidTokenNoteTooBigError(_)) + )); + } + + #[test] + fn public_note_at_max_length_passes() { + let transition = TokenConfigUpdateTransition::V0(TokenConfigUpdateTransitionV0 { + base: make_base(), + update_token_configuration_item: TokenConfigurationChangeItem::MaxSupply(Some( + 1_000_000, + )), + public_note: Some("x".repeat(MAX_TOKEN_NOTE_LEN)), + }); + let result = transition + .validate_structure_v0(platform_version()) + .unwrap(); + assert!(result.is_valid(), "expected no errors: {:?}", result.errors); + } + + #[test] + fn note_on_non_proposer_returns_error() { + let transition = TokenConfigUpdateTransition::V0(TokenConfigUpdateTransitionV0 { + base: make_base_with_group(false), + update_token_configuration_item: TokenConfigurationChangeItem::MaxSupply(Some( + 1_000_000, + )), + public_note: Some("a valid note".to_string()), + }); + let result = transition + .validate_structure_v0(platform_version()) + .unwrap(); + assert_eq!(result.errors.len(), 1); + assert!(matches!( + &result.errors[0], + ConsensusError::BasicError(BasicError::TokenNoteOnlyAllowedWhenProposerError(_)) + )); + } + + #[test] + fn note_on_proposer_passes() { + let transition = TokenConfigUpdateTransition::V0(TokenConfigUpdateTransitionV0 { + base: make_base_with_group(true), + update_token_configuration_item: TokenConfigurationChangeItem::MaxSupply(Some( + 1_000_000, + )), + public_note: Some("a valid note".to_string()), + }); + let result = transition + .validate_structure_v0(platform_version()) + .unwrap(); + assert!(result.is_valid(), "expected no errors: {:?}", result.errors); + } + + #[test] + fn note_without_group_info_passes() { + let transition = TokenConfigUpdateTransition::V0(TokenConfigUpdateTransitionV0 { + base: make_base(), + update_token_configuration_item: TokenConfigurationChangeItem::MaxSupply(Some( + 1_000_000, + )), + public_note: Some("a valid note".to_string()), + }); + let result = transition + .validate_structure_v0(platform_version()) + .unwrap(); + assert!(result.is_valid(), "expected no errors: {:?}", result.errors); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_destroy_frozen_funds_transition/validate_structure/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_destroy_frozen_funds_transition/validate_structure/v0/mod.rs index db0a5c08e17..12e98715c9d 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_destroy_frozen_funds_transition/validate_structure/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_destroy_frozen_funds_transition/validate_structure/v0/mod.rs @@ -42,3 +42,86 @@ impl TokenDestroyFrozenFundsTransitionStructureValidationV0 for TokenDestroyFroz Ok(SimpleConsensusValidationResult::default()) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::group::GroupStateTransitionInfo; + use crate::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; + use crate::state_transition::batch_transition::token_destroy_frozen_funds_transition::v0::TokenDestroyFrozenFundsTransitionV0; + use platform_value::Identifier; + + fn make_transition( + public_note: Option, + using_group_info: Option, + ) -> TokenDestroyFrozenFundsTransition { + TokenDestroyFrozenFundsTransition::V0(TokenDestroyFrozenFundsTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: Identifier::default(), + token_id: Identifier::default(), + using_group_info, + }), + frozen_identity_id: Identifier::default(), + public_note, + }) + } + + #[test] + fn should_pass_with_no_public_note() { + let transition = make_transition(None, None); + let result = transition.validate_structure_v0().unwrap(); + assert!(result.is_valid()); + } + + #[test] + fn should_pass_with_short_public_note_and_no_group() { + let transition = make_transition(Some("hello".to_string()), None); + let result = transition.validate_structure_v0().unwrap(); + assert!(result.is_valid()); + } + + #[test] + fn should_pass_with_public_note_and_proposer_group() { + let group_info = GroupStateTransitionInfo { + group_contract_position: 0, + action_id: Identifier::default(), + action_is_proposer: true, + }; + let transition = make_transition(Some("hello".to_string()), Some(group_info)); + let result = transition.validate_structure_v0().unwrap(); + assert!(result.is_valid()); + } + + #[test] + fn should_return_error_when_public_note_too_big() { + let long_note = "a".repeat(MAX_TOKEN_NOTE_LEN + 1); + let transition = make_transition(Some(long_note), None); + let result = transition.validate_structure_v0().unwrap(); + assert!(!result.is_valid()); + let error = result.errors.first().unwrap(); + assert!(matches!( + error, + ConsensusError::BasicError(BasicError::InvalidTokenNoteTooBigError(_)) + )); + } + + #[test] + fn should_return_error_when_note_present_but_non_proposer_in_group() { + let group_info = GroupStateTransitionInfo { + group_contract_position: 0, + action_id: Identifier::default(), + action_is_proposer: false, + }; + let transition = make_transition(Some("hello".to_string()), Some(group_info)); + let result = transition.validate_structure_v0().unwrap(); + assert!(!result.is_valid()); + let error = result.errors.first().unwrap(); + assert!(matches!( + error, + ConsensusError::BasicError(BasicError::TokenNoteOnlyAllowedWhenProposerError(_)) + )); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_direct_purchase_transition/validate_structure/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_direct_purchase_transition/validate_structure/v0/mod.rs index 13d696e1699..9b3a6e7fdda 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_direct_purchase_transition/validate_structure/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_direct_purchase_transition/validate_structure/v0/mod.rs @@ -22,3 +22,71 @@ impl TokenDirectPurchaseTransitionActionStructureValidationV0 for TokenDirectPur Ok(SimpleConsensusValidationResult::default()) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; + use crate::state_transition::batch_transition::token_direct_purchase_transition::v0::TokenDirectPurchaseTransitionV0; + use platform_value::Identifier; + + fn make_transition(token_count: u64) -> TokenDirectPurchaseTransition { + TokenDirectPurchaseTransition::V0(TokenDirectPurchaseTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: Identifier::default(), + token_id: Identifier::default(), + using_group_info: None, + }), + token_count, + total_agreed_price: 1000, + }) + } + + #[test] + fn should_pass_with_valid_token_count() { + let transition = make_transition(100); + let result = transition.validate_structure_v0().unwrap(); + assert!(result.is_valid()); + } + + #[test] + fn should_pass_with_token_count_of_one() { + let transition = make_transition(1); + let result = transition.validate_structure_v0().unwrap(); + assert!(result.is_valid()); + } + + #[test] + fn should_pass_with_token_count_at_max() { + let transition = make_transition(MAX_DISTRIBUTION_PARAM); + let result = transition.validate_structure_v0().unwrap(); + assert!(result.is_valid()); + } + + #[test] + fn should_return_error_when_token_count_is_zero() { + let transition = make_transition(0); + let result = transition.validate_structure_v0().unwrap(); + assert!(!result.is_valid()); + let error = result.errors.first().unwrap(); + assert!(matches!( + error, + ConsensusError::BasicError(BasicError::InvalidTokenAmountError(_)) + )); + } + + #[test] + fn should_return_error_when_token_count_exceeds_max() { + let transition = make_transition(MAX_DISTRIBUTION_PARAM + 1); + let result = transition.validate_structure_v0().unwrap(); + assert!(!result.is_valid()); + let error = result.errors.first().unwrap(); + assert!(matches!( + error, + ConsensusError::BasicError(BasicError::InvalidTokenAmountError(_)) + )); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_emergency_action_transition/validate_structure/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_emergency_action_transition/validate_structure/v0/mod.rs index 261522c3847..45890f31acc 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_emergency_action_transition/validate_structure/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_emergency_action_transition/validate_structure/v0/mod.rs @@ -42,3 +42,87 @@ impl TokenEmergencyActionTransitionStructureValidationV0 for TokenEmergencyActio Ok(SimpleConsensusValidationResult::default()) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::group::GroupStateTransitionInfo; + use crate::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; + use crate::state_transition::batch_transition::token_emergency_action_transition::v0::TokenEmergencyActionTransitionV0; + use crate::tokens::emergency_action::TokenEmergencyAction; + use platform_value::Identifier; + + fn make_transition( + public_note: Option, + using_group_info: Option, + ) -> TokenEmergencyActionTransition { + TokenEmergencyActionTransition::V0(TokenEmergencyActionTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: Identifier::default(), + token_id: Identifier::default(), + using_group_info, + }), + emergency_action: TokenEmergencyAction::Pause, + public_note, + }) + } + + #[test] + fn should_pass_with_no_public_note() { + let transition = make_transition(None, None); + let result = transition.validate_structure_v0().unwrap(); + assert!(result.is_valid()); + } + + #[test] + fn should_pass_with_short_public_note_and_no_group() { + let transition = make_transition(Some("hello".to_string()), None); + let result = transition.validate_structure_v0().unwrap(); + assert!(result.is_valid()); + } + + #[test] + fn should_pass_with_public_note_and_proposer_group() { + let group_info = GroupStateTransitionInfo { + group_contract_position: 0, + action_id: Identifier::default(), + action_is_proposer: true, + }; + let transition = make_transition(Some("hello".to_string()), Some(group_info)); + let result = transition.validate_structure_v0().unwrap(); + assert!(result.is_valid()); + } + + #[test] + fn should_return_error_when_public_note_too_big() { + let long_note = "a".repeat(MAX_TOKEN_NOTE_LEN + 1); + let transition = make_transition(Some(long_note), None); + let result = transition.validate_structure_v0().unwrap(); + assert!(!result.is_valid()); + let error = result.errors.first().unwrap(); + assert!(matches!( + error, + ConsensusError::BasicError(BasicError::InvalidTokenNoteTooBigError(_)) + )); + } + + #[test] + fn should_return_error_when_note_present_but_non_proposer_in_group() { + let group_info = GroupStateTransitionInfo { + group_contract_position: 0, + action_id: Identifier::default(), + action_is_proposer: false, + }; + let transition = make_transition(Some("hello".to_string()), Some(group_info)); + let result = transition.validate_structure_v0().unwrap(); + assert!(!result.is_valid()); + let error = result.errors.first().unwrap(); + assert!(matches!( + error, + ConsensusError::BasicError(BasicError::TokenNoteOnlyAllowedWhenProposerError(_)) + )); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_freeze_transition/validate_structure/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_freeze_transition/validate_structure/v0/mod.rs index e3a98a6bf4a..98729249c45 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_freeze_transition/validate_structure/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_freeze_transition/validate_structure/v0/mod.rs @@ -42,3 +42,86 @@ impl TokenFreezeTransitionStructureValidationV0 for TokenFreezeTransition { Ok(SimpleConsensusValidationResult::default()) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::group::GroupStateTransitionInfo; + use crate::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; + use crate::state_transition::batch_transition::token_freeze_transition::v0::TokenFreezeTransitionV0; + use platform_value::Identifier; + + fn make_transition( + public_note: Option, + using_group_info: Option, + ) -> TokenFreezeTransition { + TokenFreezeTransition::V0(TokenFreezeTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: Identifier::default(), + token_id: Identifier::default(), + using_group_info, + }), + identity_to_freeze_id: Identifier::default(), + public_note, + }) + } + + #[test] + fn should_pass_with_no_public_note() { + let transition = make_transition(None, None); + let result = transition.validate_structure_v0().unwrap(); + assert!(result.is_valid()); + } + + #[test] + fn should_pass_with_short_public_note_and_no_group() { + let transition = make_transition(Some("hello".to_string()), None); + let result = transition.validate_structure_v0().unwrap(); + assert!(result.is_valid()); + } + + #[test] + fn should_pass_with_public_note_and_proposer_group() { + let group_info = GroupStateTransitionInfo { + group_contract_position: 0, + action_id: Identifier::default(), + action_is_proposer: true, + }; + let transition = make_transition(Some("hello".to_string()), Some(group_info)); + let result = transition.validate_structure_v0().unwrap(); + assert!(result.is_valid()); + } + + #[test] + fn should_return_error_when_public_note_too_big() { + let long_note = "a".repeat(MAX_TOKEN_NOTE_LEN + 1); + let transition = make_transition(Some(long_note), None); + let result = transition.validate_structure_v0().unwrap(); + assert!(!result.is_valid()); + let error = result.errors.first().unwrap(); + assert!(matches!( + error, + ConsensusError::BasicError(BasicError::InvalidTokenNoteTooBigError(_)) + )); + } + + #[test] + fn should_return_error_when_note_present_but_non_proposer_in_group() { + let group_info = GroupStateTransitionInfo { + group_contract_position: 0, + action_id: Identifier::default(), + action_is_proposer: false, + }; + let transition = make_transition(Some("hello".to_string()), Some(group_info)); + let result = transition.validate_structure_v0().unwrap(); + assert!(!result.is_valid()); + let error = result.errors.first().unwrap(); + assert!(matches!( + error, + ConsensusError::BasicError(BasicError::TokenNoteOnlyAllowedWhenProposerError(_)) + )); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_transition/v0/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_transition/v0/v0_methods.rs index 41528e0022d..7a307a574bd 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_transition/v0/v0_methods.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_transition/v0/v0_methods.rs @@ -115,3 +115,180 @@ impl AllowedAsMultiPartyAction for TokenMintTransitionV0 { )) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::data_contract::associated_token::token_configuration::v0::TokenConfigurationV0; + use crate::data_contract::associated_token::token_configuration_convention::v0::TokenConfigurationConventionV0; + use crate::data_contract::associated_token::token_configuration_convention::TokenConfigurationConvention; + use crate::data_contract::associated_token::token_distribution_rules::v0::TokenDistributionRulesV0; + use crate::data_contract::associated_token::token_distribution_rules::TokenDistributionRules; + use crate::data_contract::associated_token::token_keeps_history_rules::v0::TokenKeepsHistoryRulesV0; + use crate::data_contract::associated_token::token_keeps_history_rules::TokenKeepsHistoryRules; + use crate::data_contract::associated_token::token_marketplace_rules::v0::{ + TokenMarketplaceRulesV0, TokenTradeMode, + }; + use crate::data_contract::associated_token::token_marketplace_rules::TokenMarketplaceRules; + use crate::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; + use crate::data_contract::change_control_rules::v0::ChangeControlRulesV0; + use crate::data_contract::change_control_rules::ChangeControlRules; + use crate::tokens::errors::TokenError; + use std::collections::BTreeMap; + + fn no_one_rules() -> ChangeControlRules { + ChangeControlRules::V0(ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::NoOne, + admin_action_takers: AuthorizedActionTakers::NoOne, + changing_authorized_action_takers_to_no_one_allowed: false, + changing_admin_action_takers_to_no_one_allowed: false, + self_changing_admin_action_takers_allowed: false, + }) + } + + fn make_config(new_tokens_dest: Option) -> TokenConfiguration { + TokenConfiguration::V0(TokenConfigurationV0 { + conventions: TokenConfigurationConvention::V0(TokenConfigurationConventionV0 { + localizations: BTreeMap::new(), + decimals: 8, + }), + conventions_change_rules: no_one_rules(), + base_supply: 1000, + max_supply: None, + keeps_history: TokenKeepsHistoryRules::V0(TokenKeepsHistoryRulesV0 { + keeps_transfer_history: true, + keeps_freezing_history: true, + keeps_minting_history: true, + keeps_burning_history: true, + keeps_direct_pricing_history: true, + keeps_direct_purchase_history: true, + }), + start_as_paused: false, + allow_transfer_to_frozen_balance: true, + max_supply_change_rules: no_one_rules(), + distribution_rules: TokenDistributionRules::V0(TokenDistributionRulesV0 { + perpetual_distribution: None, + perpetual_distribution_rules: no_one_rules(), + pre_programmed_distribution: None, + new_tokens_destination_identity: new_tokens_dest, + new_tokens_destination_identity_rules: no_one_rules(), + minting_allow_choosing_destination: true, + minting_allow_choosing_destination_rules: no_one_rules(), + change_direct_purchase_pricing_rules: no_one_rules(), + }), + marketplace_rules: TokenMarketplaceRules::V0(TokenMarketplaceRulesV0 { + trade_mode: TokenTradeMode::NotTradeable, + trade_mode_change_rules: no_one_rules(), + }), + manual_minting_rules: no_one_rules(), + manual_burning_rules: no_one_rules(), + freeze_rules: no_one_rules(), + unfreeze_rules: no_one_rules(), + destroy_frozen_funds_rules: no_one_rules(), + emergency_action_rules: no_one_rules(), + main_control_group: None, + main_control_group_can_be_modified: AuthorizedActionTakers::NoOne, + description: None, + }) + } + + // ----------------------------------------------------------------------- + // recipient_id — explicit recipient takes precedence + // ----------------------------------------------------------------------- + + #[test] + fn recipient_id_returns_explicit_recipient_when_set() { + let explicit_recipient = Identifier::from([20u8; 32]); + let transition = TokenMintTransitionV0 { + base: TokenBaseTransition::default(), + issued_to_identity_id: Some(explicit_recipient), + amount: 100, + public_note: None, + }; + + let config_dest = Identifier::from([30u8; 32]); + let config = make_config(Some(config_dest)); + + let result = transition + .recipient_id(&config) + .expect("should return explicit recipient"); + assert_eq!(result, explicit_recipient); + } + + // ----------------------------------------------------------------------- + // recipient_id — fallback to config destination + // ----------------------------------------------------------------------- + + #[test] + fn recipient_id_falls_back_to_config_destination_identity() { + let transition = TokenMintTransitionV0 { + base: TokenBaseTransition::default(), + issued_to_identity_id: None, + amount: 100, + public_note: None, + }; + + let config_dest = Identifier::from([30u8; 32]); + let config = make_config(Some(config_dest)); + + let result = transition + .recipient_id(&config) + .expect("should fall back to config dest"); + assert_eq!(result, config_dest); + } + + // ----------------------------------------------------------------------- + // recipient_id — error TokenNoMintingRecipient when nothing available + // ----------------------------------------------------------------------- + + #[test] + fn recipient_id_errors_with_token_no_minting_recipient() { + let transition = TokenMintTransitionV0 { + base: TokenBaseTransition::default(), + issued_to_identity_id: None, + amount: 100, + public_note: None, + }; + + let config = make_config(None); + + let result = transition.recipient_id(&config); + assert!(result.is_err()); + match result.unwrap_err() { + ProtocolError::Token(boxed_err) => { + let token_err: &TokenError = boxed_err.as_ref(); + assert!( + matches!(token_err, TokenError::TokenNoMintingRecipient), + "Expected TokenNoMintingRecipient, got {:?}", + token_err + ); + } + other => panic!("Expected ProtocolError::Token, got {:?}", other), + } + } + + // ----------------------------------------------------------------------- + // recipient_id — explicit recipient overrides config even when both set + // ----------------------------------------------------------------------- + + #[test] + fn recipient_id_prefers_explicit_over_config() { + let explicit = Identifier::from([11u8; 32]); + let config_dest = Identifier::from([22u8; 32]); + + let transition = TokenMintTransitionV0 { + base: TokenBaseTransition::default(), + issued_to_identity_id: Some(explicit), + amount: 500, + public_note: None, + }; + + let config = make_config(Some(config_dest)); + + let result = transition + .recipient_id(&config) + .expect("explicit should win"); + assert_eq!(result, explicit); + assert_ne!(result, config_dest); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_transition/validate_structure/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_transition/validate_structure/v0/mod.rs index 22e15d92449..4674fad772f 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_transition/validate_structure/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_transition/validate_structure/v0/mod.rs @@ -50,3 +50,165 @@ impl TokenMintTransitionActionStructureValidationV0 for TokenMintTransition { Ok(SimpleConsensusValidationResult::default()) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::group::GroupStateTransitionInfo; + use crate::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; + use crate::state_transition::batch_transition::token_mint_transition::TokenMintTransitionV0; + use platform_value::Identifier; + + fn make_base() -> TokenBaseTransition { + TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: Identifier::default(), + token_id: Identifier::new([1u8; 32]), + using_group_info: None, + }) + } + + fn make_base_with_group(is_proposer: bool) -> TokenBaseTransition { + TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: Identifier::default(), + token_id: Identifier::new([1u8; 32]), + using_group_info: Some(GroupStateTransitionInfo { + group_contract_position: 0, + action_id: Identifier::new([10u8; 32]), + action_is_proposer: is_proposer, + }), + }) + } + + fn make_valid_mint() -> TokenMintTransition { + TokenMintTransition::V0(TokenMintTransitionV0 { + base: make_base(), + issued_to_identity_id: None, + amount: 500, + public_note: None, + }) + } + + #[test] + fn valid_mint_passes() { + let transition = make_valid_mint(); + let result = transition.validate_structure_v0().unwrap(); + assert!(result.is_valid(), "expected no errors: {:?}", result.errors); + } + + #[test] + fn zero_amount_returns_invalid_token_amount_error() { + let transition = TokenMintTransition::V0(TokenMintTransitionV0 { + base: make_base(), + issued_to_identity_id: None, + amount: 0, + public_note: None, + }); + let result = transition.validate_structure_v0().unwrap(); + assert_eq!(result.errors.len(), 1); + assert!(matches!( + &result.errors[0], + ConsensusError::BasicError(BasicError::InvalidTokenAmountError(_)) + )); + } + + #[test] + fn amount_exceeding_max_returns_invalid_token_amount_error() { + let transition = TokenMintTransition::V0(TokenMintTransitionV0 { + base: make_base(), + issued_to_identity_id: None, + amount: MAX_DISTRIBUTION_PARAM + 1, + public_note: None, + }); + let result = transition.validate_structure_v0().unwrap(); + assert_eq!(result.errors.len(), 1); + assert!(matches!( + &result.errors[0], + ConsensusError::BasicError(BasicError::InvalidTokenAmountError(_)) + )); + } + + #[test] + fn amount_at_max_distribution_param_passes() { + let transition = TokenMintTransition::V0(TokenMintTransitionV0 { + base: make_base(), + issued_to_identity_id: None, + amount: MAX_DISTRIBUTION_PARAM, + public_note: None, + }); + let result = transition.validate_structure_v0().unwrap(); + assert!(result.is_valid(), "expected no errors: {:?}", result.errors); + } + + #[test] + fn public_note_too_big_returns_error() { + let transition = TokenMintTransition::V0(TokenMintTransitionV0 { + base: make_base(), + issued_to_identity_id: None, + amount: 100, + public_note: Some("x".repeat(MAX_TOKEN_NOTE_LEN + 1)), + }); + let result = transition.validate_structure_v0().unwrap(); + assert_eq!(result.errors.len(), 1); + assert!(matches!( + &result.errors[0], + ConsensusError::BasicError(BasicError::InvalidTokenNoteTooBigError(_)) + )); + } + + #[test] + fn public_note_at_max_length_passes() { + let transition = TokenMintTransition::V0(TokenMintTransitionV0 { + base: make_base(), + issued_to_identity_id: None, + amount: 100, + public_note: Some("x".repeat(MAX_TOKEN_NOTE_LEN)), + }); + let result = transition.validate_structure_v0().unwrap(); + assert!(result.is_valid(), "expected no errors: {:?}", result.errors); + } + + #[test] + fn note_on_non_proposer_returns_error() { + let transition = TokenMintTransition::V0(TokenMintTransitionV0 { + base: make_base_with_group(false), + issued_to_identity_id: None, + amount: 100, + public_note: Some("a valid note".to_string()), + }); + let result = transition.validate_structure_v0().unwrap(); + assert_eq!(result.errors.len(), 1); + assert!(matches!( + &result.errors[0], + ConsensusError::BasicError(BasicError::TokenNoteOnlyAllowedWhenProposerError(_)) + )); + } + + #[test] + fn note_on_proposer_passes() { + let transition = TokenMintTransition::V0(TokenMintTransitionV0 { + base: make_base_with_group(true), + issued_to_identity_id: None, + amount: 100, + public_note: Some("a valid note".to_string()), + }); + let result = transition.validate_structure_v0().unwrap(); + assert!(result.is_valid(), "expected no errors: {:?}", result.errors); + } + + #[test] + fn note_without_group_info_passes() { + let transition = TokenMintTransition::V0(TokenMintTransitionV0 { + base: make_base(), + issued_to_identity_id: None, + amount: 100, + public_note: Some("a valid note".to_string()), + }); + let result = transition.validate_structure_v0().unwrap(); + assert!(result.is_valid(), "expected no errors: {:?}", result.errors); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0/v0_methods.rs index b987d4664e2..026886db616 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0/v0_methods.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0/v0_methods.rs @@ -68,17 +68,16 @@ impl AllowedAsMultiPartyAction for TokenSetPriceForDirectPurchaseTransitionV0 { fn calculate_action_id( &self, owner_id: Identifier, - _platform_version: &PlatformVersion, + platform_version: &PlatformVersion, ) -> Result { let TokenSetPriceForDirectPurchaseTransitionV0 { base, price, .. } = self; - Ok( - TokenSetPriceForDirectPurchaseTransition::calculate_action_id_with_fields( - base.token_id().as_bytes(), - owner_id.as_bytes(), - base.identity_contract_nonce(), - price.as_ref(), - ), + TokenSetPriceForDirectPurchaseTransition::calculate_action_id_with_fields( + base.token_id().as_bytes(), + owner_id.as_bytes(), + base.identity_contract_nonce(), + price.as_ref(), + platform_version, ) } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0_methods.rs index dc838ac1e78..5d9feec15cb 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0_methods.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0_methods.rs @@ -1,14 +1,14 @@ -use platform_value::Identifier; -use platform_version::version::PlatformVersion; +use crate::errors::ProtocolError; use crate::prelude::IdentityNonce; -use crate::ProtocolError; use crate::state_transition::batch_transition::batched_transition::multi_party_action::AllowedAsMultiPartyAction; use crate::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; -use crate::state_transition::batch_transition::token_set_price_for_direct_purchase_transition::TokenSetPriceForDirectPurchaseTransition; use crate::state_transition::batch_transition::token_set_price_for_direct_purchase_transition::v0::v0_methods::TokenSetPriceForDirectPurchaseTransitionV0Methods; +use crate::state_transition::batch_transition::token_set_price_for_direct_purchase_transition::TokenSetPriceForDirectPurchaseTransition; use crate::tokens::token_pricing_schedule::TokenPricingSchedule; use crate::util::hash::hash_double; +use platform_value::Identifier; +use platform_version::version::PlatformVersion; impl TokenBaseTransitionAccessors for TokenSetPriceForDirectPurchaseTransition { fn base(&self) -> &TokenBaseTransition { @@ -84,6 +84,39 @@ impl TokenSetPriceForDirectPurchaseTransition { owner_id: &[u8; 32], identity_contract_nonce: IdentityNonce, price_per_token: Option<&TokenPricingSchedule>, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version + .dpp + .token_versions + .token_set_price_action_id_version + { + 0 => Ok(Self::calculate_action_id_with_fields_v0( + token_id, + owner_id, + identity_contract_nonce, + price_per_token, + )), + 1 => Self::calculate_action_id_with_fields_v1( + token_id, + owner_id, + identity_contract_nonce, + price_per_token, + ), + version => Err(ProtocolError::UnknownVersionMismatch { + method: "calculate_action_id_with_fields".to_string(), + known_versions: vec![0, 1], + received: version, + }), + } + } + + /// v0: hashes only minimum_purchase_amount_and_price().1 (kept for backward compat). + fn calculate_action_id_with_fields_v0( + token_id: &[u8; 32], + owner_id: &[u8; 32], + identity_contract_nonce: IdentityNonce, + price_per_token: Option<&TokenPricingSchedule>, ) -> Identifier { let mut bytes = b"action_token_set_price_for_direct_purchase".to_vec(); bytes.extend_from_slice(token_id); @@ -100,4 +133,197 @@ impl TokenSetPriceForDirectPurchaseTransition { hash_double(bytes).into() } + + /// v1: hashes the full serialized TokenPricingSchedule, preventing schedule swap. + fn calculate_action_id_with_fields_v1( + token_id: &[u8; 32], + owner_id: &[u8; 32], + identity_contract_nonce: IdentityNonce, + price_per_token: Option<&TokenPricingSchedule>, + ) -> Result { + let mut bytes = b"action_token_set_price_for_direct_purchase".to_vec(); + bytes.extend_from_slice(token_id); + bytes.extend_from_slice(owner_id); + bytes.extend_from_slice(&identity_contract_nonce.to_be_bytes()); + if let Some(price_per_token) = price_per_token { + let serialized = bincode::encode_to_vec(price_per_token, bincode::config::standard()) + .map_err(|e| ProtocolError::EncodingError(e.to_string()))?; + bytes.extend_from_slice(&serialized); + } + + Ok(hash_double(bytes).into()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::token_set_price_for_direct_purchase_transition::TokenSetPriceForDirectPurchaseTransitionV0; + use platform_version::version::PlatformVersion; + use std::collections::BTreeMap; + + fn make_transition( + price: Option, + ) -> TokenSetPriceForDirectPurchaseTransition { + TokenSetPriceForDirectPurchaseTransition::V0(TokenSetPriceForDirectPurchaseTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: Identifier::new([1u8; 32]), + token_id: Identifier::new([2u8; 32]), + using_group_info: None, + }), + price, + public_note: None, + }) + } + + #[test] + fn different_set_prices_with_same_minimum_produce_different_ids() { + // This was the vulnerability: two SetPrices schedules with the same + // minimum-tier price but different higher tiers produced identical + // action_ids when only minimum_purchase_amount_and_price().1 was hashed. + let owner_id = Identifier::new([3u8; 32]); + + let t_cheap = make_transition(Some(TokenPricingSchedule::SetPrices(BTreeMap::from([ + (1, 100), + (10, 800), + ])))); + let t_expensive = make_transition(Some(TokenPricingSchedule::SetPrices(BTreeMap::from([ + (1, 100), + (10, 9999), + ])))); + + let id_cheap = t_cheap + .calculate_action_id(owner_id, PlatformVersion::latest()) + .expect("expected action id"); + let id_expensive = t_expensive + .calculate_action_id(owner_id, PlatformVersion::latest()) + .expect("expected action id"); + + assert_ne!( + id_cheap, id_expensive, + "different pricing schedules with same minimum price must produce different action_ids" + ); + } + + #[test] + fn single_price_and_set_prices_with_same_minimum_produce_different_ids() { + // SinglePrice(100) and SetPrices({1: 100}) both have + // minimum_purchase_amount_and_price() == (1, 100), but they are + // semantically different schedules. + let owner_id = Identifier::new([3u8; 32]); + + let t_single = make_transition(Some(TokenPricingSchedule::SinglePrice(100))); + let t_set = make_transition(Some(TokenPricingSchedule::SetPrices(BTreeMap::from([( + 1, 100, + )])))); + + let id_single = t_single + .calculate_action_id(owner_id, PlatformVersion::latest()) + .expect("expected action id"); + let id_set = t_set + .calculate_action_id(owner_id, PlatformVersion::latest()) + .expect("expected action id"); + + assert_ne!( + id_single, id_set, + "SinglePrice and SetPrices with same minimum must produce different action_ids" + ); + } + + #[test] + fn identical_schedules_produce_same_id() { + let owner_id = Identifier::new([3u8; 32]); + + let t1 = make_transition(Some(TokenPricingSchedule::SetPrices(BTreeMap::from([ + (1, 100), + (10, 800), + ])))); + let t2 = make_transition(Some(TokenPricingSchedule::SetPrices(BTreeMap::from([ + (1, 100), + (10, 800), + ])))); + + let pv = PlatformVersion::latest(); + assert_eq!( + t1.calculate_action_id(owner_id, pv) + .expect("expected action id"), + t2.calculate_action_id(owner_id, pv) + .expect("expected action id"), + "identical pricing schedules must produce the same action_id" + ); + } + + #[test] + fn none_price_produces_different_id_from_some_price() { + let owner_id = Identifier::new([3u8; 32]); + let pv = PlatformVersion::latest(); + + let t_none = make_transition(None); + let t_some = make_transition(Some(TokenPricingSchedule::SinglePrice(100))); + + assert_ne!( + t_none + .calculate_action_id(owner_id, pv) + .expect("expected action id"), + t_some + .calculate_action_id(owner_id, pv) + .expect("expected action id"), + "None price and Some price must produce different action_ids" + ); + } + + #[test] + fn v0_same_minimum_produces_same_id_vulnerability() { + // Documents the v0 vulnerability: different schedules with the same + // minimum-tier price produce identical action_ids. + let owner_id = Identifier::new([3u8; 32]); + let pv = PlatformVersion::first(); + + let t_cheap = make_transition(Some(TokenPricingSchedule::SetPrices(BTreeMap::from([ + (1, 100), + (10, 800), + ])))); + let t_expensive = make_transition(Some(TokenPricingSchedule::SetPrices(BTreeMap::from([ + (1, 100), + (10, 9999), + ])))); + + let id_cheap = t_cheap + .calculate_action_id(owner_id, pv) + .expect("expected action id"); + let id_expensive = t_expensive + .calculate_action_id(owner_id, pv) + .expect("expected action id"); + + // v0: these are EQUAL -- the vulnerability + assert_eq!( + id_cheap, id_expensive, + "v0 should produce the same action_id for different schedules with same min price" + ); + } + + #[test] + fn v0_and_v1_produce_different_ids_for_same_input() { + let owner_id = Identifier::new([3u8; 32]); + + let t = make_transition(Some(TokenPricingSchedule::SetPrices(BTreeMap::from([ + (1, 100), + (10, 800), + ])))); + + let id_v0 = t + .calculate_action_id(owner_id, PlatformVersion::first()) + .expect("expected action id"); + let id_v1 = t + .calculate_action_id(owner_id, PlatformVersion::latest()) + .expect("expected action id"); + + assert_ne!( + id_v0, id_v1, + "v0 and v1 should produce different action_ids for the same schedule" + ); + } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/validate_structure/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/validate_structure/v0/mod.rs index 4574d7531fb..9eb26aef08a 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/validate_structure/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/validate_structure/v0/mod.rs @@ -45,3 +45,86 @@ impl TokenSetPriceForDirectPurchaseTransitionActionStructureValidationV0 Ok(SimpleConsensusValidationResult::default()) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::group::GroupStateTransitionInfo; + use crate::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; + use crate::state_transition::batch_transition::token_set_price_for_direct_purchase_transition::v0::TokenSetPriceForDirectPurchaseTransitionV0; + use platform_value::Identifier; + + fn make_transition( + public_note: Option, + using_group_info: Option, + ) -> TokenSetPriceForDirectPurchaseTransition { + TokenSetPriceForDirectPurchaseTransition::V0(TokenSetPriceForDirectPurchaseTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: Identifier::default(), + token_id: Identifier::default(), + using_group_info, + }), + price: None, + public_note, + }) + } + + #[test] + fn should_pass_with_no_public_note() { + let transition = make_transition(None, None); + let result = transition.validate_structure_v0().unwrap(); + assert!(result.is_valid()); + } + + #[test] + fn should_pass_with_short_public_note_and_no_group() { + let transition = make_transition(Some("hello".to_string()), None); + let result = transition.validate_structure_v0().unwrap(); + assert!(result.is_valid()); + } + + #[test] + fn should_pass_with_public_note_and_proposer_group() { + let group_info = GroupStateTransitionInfo { + group_contract_position: 0, + action_id: Identifier::default(), + action_is_proposer: true, + }; + let transition = make_transition(Some("hello".to_string()), Some(group_info)); + let result = transition.validate_structure_v0().unwrap(); + assert!(result.is_valid()); + } + + #[test] + fn should_return_error_when_public_note_too_big() { + let long_note = "a".repeat(MAX_TOKEN_NOTE_LEN + 1); + let transition = make_transition(Some(long_note), None); + let result = transition.validate_structure_v0().unwrap(); + assert!(!result.is_valid()); + let error = result.errors.first().unwrap(); + assert!(matches!( + error, + ConsensusError::BasicError(BasicError::InvalidTokenNoteTooBigError(_)) + )); + } + + #[test] + fn should_return_error_when_note_present_but_non_proposer_in_group() { + let group_info = GroupStateTransitionInfo { + group_contract_position: 0, + action_id: Identifier::default(), + action_is_proposer: false, + }; + let transition = make_transition(Some("hello".to_string()), Some(group_info)); + let result = transition.validate_structure_v0().unwrap(); + assert!(!result.is_valid()); + let error = result.errors.first().unwrap(); + assert!(matches!( + error, + ConsensusError::BasicError(BasicError::TokenNoteOnlyAllowedWhenProposerError(_)) + )); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transfer_transition/validate_structure/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transfer_transition/validate_structure/v0/mod.rs index 5ce2a430b3a..77d7460aa64 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transfer_transition/validate_structure/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transfer_transition/validate_structure/v0/mod.rs @@ -83,3 +83,189 @@ impl TokenTransferTransitionActionStructureValidationV0 for TokenTransferTransit Ok(SimpleConsensusValidationResult::default()) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; + use crate::state_transition::batch_transition::token_transfer_transition::TokenTransferTransitionV0; + + fn make_base() -> TokenBaseTransition { + TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: Identifier::default(), + token_id: Identifier::new([1u8; 32]), + using_group_info: None, + }) + } + + fn make_valid_transfer(owner_id: Identifier) -> TokenTransferTransition { + // Ensure recipient differs from owner + let mut recipient_bytes = [2u8; 32]; + if Identifier::new(recipient_bytes) == owner_id { + recipient_bytes = [3u8; 32]; + } + TokenTransferTransition::V0(TokenTransferTransitionV0 { + base: make_base(), + amount: 1000, + recipient_id: Identifier::new(recipient_bytes), + public_note: None, + shared_encrypted_note: None, + private_encrypted_note: None, + }) + } + + #[test] + fn valid_transfer_passes() { + let owner_id = Identifier::new([3u8; 32]); + let transition = make_valid_transfer(owner_id); + let result = transition.validate_structure_v0(owner_id).unwrap(); + assert!(result.is_valid(), "expected no errors: {:?}", result.errors); + } + + #[test] + fn zero_amount_returns_invalid_token_amount_error() { + let owner_id = Identifier::new([3u8; 32]); + let transition = TokenTransferTransition::V0(TokenTransferTransitionV0 { + base: make_base(), + amount: 0, + recipient_id: Identifier::new([2u8; 32]), + public_note: None, + shared_encrypted_note: None, + private_encrypted_note: None, + }); + let result = transition.validate_structure_v0(owner_id).unwrap(); + assert_eq!(result.errors.len(), 1); + assert!(matches!( + &result.errors[0], + ConsensusError::BasicError(BasicError::InvalidTokenAmountError(_)) + )); + } + + #[test] + fn amount_exceeding_max_returns_invalid_token_amount_error() { + let owner_id = Identifier::new([3u8; 32]); + let transition = TokenTransferTransition::V0(TokenTransferTransitionV0 { + base: make_base(), + amount: MAX_DISTRIBUTION_PARAM + 1, + recipient_id: Identifier::new([2u8; 32]), + public_note: None, + shared_encrypted_note: None, + private_encrypted_note: None, + }); + let result = transition.validate_structure_v0(owner_id).unwrap(); + assert_eq!(result.errors.len(), 1); + assert!(matches!( + &result.errors[0], + ConsensusError::BasicError(BasicError::InvalidTokenAmountError(_)) + )); + } + + #[test] + fn amount_at_max_distribution_param_passes() { + let owner_id = Identifier::new([3u8; 32]); + let transition = TokenTransferTransition::V0(TokenTransferTransitionV0 { + base: make_base(), + amount: MAX_DISTRIBUTION_PARAM, + recipient_id: Identifier::new([2u8; 32]), + public_note: None, + shared_encrypted_note: None, + private_encrypted_note: None, + }); + let result = transition.validate_structure_v0(owner_id).unwrap(); + assert!(result.is_valid(), "expected no errors: {:?}", result.errors); + } + + #[test] + fn transfer_to_self_returns_token_transfer_to_ourself_error() { + let owner_id = Identifier::new([5u8; 32]); + let transition = TokenTransferTransition::V0(TokenTransferTransitionV0 { + base: make_base(), + amount: 100, + recipient_id: owner_id, + public_note: None, + shared_encrypted_note: None, + private_encrypted_note: None, + }); + let result = transition.validate_structure_v0(owner_id).unwrap(); + assert_eq!(result.errors.len(), 1); + assert!(matches!( + &result.errors[0], + ConsensusError::BasicError(BasicError::TokenTransferToOurselfError(_)) + )); + } + + #[test] + fn public_note_too_big_returns_error() { + let owner_id = Identifier::new([3u8; 32]); + let transition = TokenTransferTransition::V0(TokenTransferTransitionV0 { + base: make_base(), + amount: 100, + recipient_id: Identifier::new([2u8; 32]), + public_note: Some("x".repeat(MAX_TOKEN_NOTE_LEN + 1)), + shared_encrypted_note: None, + private_encrypted_note: None, + }); + let result = transition.validate_structure_v0(owner_id).unwrap(); + assert_eq!(result.errors.len(), 1); + assert!(matches!( + &result.errors[0], + ConsensusError::BasicError(BasicError::InvalidTokenNoteTooBigError(_)) + )); + } + + #[test] + fn public_note_at_max_length_passes() { + let owner_id = Identifier::new([3u8; 32]); + let transition = TokenTransferTransition::V0(TokenTransferTransitionV0 { + base: make_base(), + amount: 100, + recipient_id: Identifier::new([2u8; 32]), + public_note: Some("x".repeat(MAX_TOKEN_NOTE_LEN)), + shared_encrypted_note: None, + private_encrypted_note: None, + }); + let result = transition.validate_structure_v0(owner_id).unwrap(); + assert!(result.is_valid(), "expected no errors: {:?}", result.errors); + } + + #[test] + fn shared_encrypted_note_too_big_returns_error() { + let owner_id = Identifier::new([3u8; 32]); + let transition = TokenTransferTransition::V0(TokenTransferTransitionV0 { + base: make_base(), + amount: 100, + recipient_id: Identifier::new([2u8; 32]), + public_note: None, + shared_encrypted_note: Some((0, 0, vec![0u8; MAX_TOKEN_NOTE_LEN + 1])), + private_encrypted_note: None, + }); + let result = transition.validate_structure_v0(owner_id).unwrap(); + assert_eq!(result.errors.len(), 1); + assert!(matches!( + &result.errors[0], + ConsensusError::BasicError(BasicError::InvalidTokenNoteTooBigError(_)) + )); + } + + #[test] + fn private_encrypted_note_too_big_returns_error() { + let owner_id = Identifier::new([3u8; 32]); + let transition = TokenTransferTransition::V0(TokenTransferTransitionV0 { + base: make_base(), + amount: 100, + recipient_id: Identifier::new([2u8; 32]), + public_note: None, + shared_encrypted_note: None, + private_encrypted_note: Some((0, 0, vec![0u8; MAX_TOKEN_NOTE_LEN + 1])), + }); + let result = transition.validate_structure_v0(owner_id).unwrap(); + assert_eq!(result.errors.len(), 1); + assert!(matches!( + &result.errors[0], + ConsensusError::BasicError(BasicError::InvalidTokenNoteTooBigError(_)) + )); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition.rs index ba3df8c6620..04f92fd9c14 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition.rs @@ -504,3 +504,953 @@ impl TokenTransitionV0Methods for TokenTransition { }) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::data_contract::associated_token::token_configuration::v0::TokenConfigurationV0; + use crate::data_contract::associated_token::token_configuration_convention::v0::TokenConfigurationConventionV0; + use crate::data_contract::associated_token::token_configuration_convention::TokenConfigurationConvention; + use crate::data_contract::associated_token::token_distribution_rules::v0::TokenDistributionRulesV0; + use crate::data_contract::associated_token::token_distribution_rules::TokenDistributionRules; + use crate::data_contract::associated_token::token_perpetual_distribution::distribution_recipient::TokenDistributionRecipient; + use crate::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; + use crate::data_contract::associated_token::token_perpetual_distribution::reward_distribution_type::RewardDistributionType; + use crate::data_contract::associated_token::token_perpetual_distribution::v0::TokenPerpetualDistributionV0; + use crate::data_contract::associated_token::token_perpetual_distribution::TokenPerpetualDistribution; + use crate::data_contract::associated_token::token_pre_programmed_distribution::v0::TokenPreProgrammedDistributionV0; + use crate::data_contract::associated_token::token_pre_programmed_distribution::TokenPreProgrammedDistribution; + use crate::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; + use crate::data_contract::change_control_rules::v0::ChangeControlRulesV0; + use crate::data_contract::change_control_rules::ChangeControlRules; + use crate::state_transition::batch_transition::batched_transition::token_burn_transition::TokenBurnTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_claim_transition::TokenClaimTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_config_update_transition::TokenConfigUpdateTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_destroy_frozen_funds_transition::TokenDestroyFrozenFundsTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_direct_purchase_transition::TokenDirectPurchaseTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_emergency_action_transition::TokenEmergencyActionTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_freeze_transition::TokenFreezeTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_mint_transition::TokenMintTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_set_price_for_direct_purchase_transition::TokenSetPriceForDirectPurchaseTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_transfer_transition::TokenTransferTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_unfreeze_transition::TokenUnfreezeTransitionV0; + use crate::data_contract::associated_token::token_configuration_item::TokenConfigurationChangeItem; + use crate::data_contract::associated_token::token_keeps_history_rules::v0::TokenKeepsHistoryRulesV0; + use crate::data_contract::associated_token::token_keeps_history_rules::TokenKeepsHistoryRules; + use crate::data_contract::associated_token::token_marketplace_rules::v0::{ + TokenMarketplaceRulesV0, TokenTradeMode, + }; + use crate::data_contract::associated_token::token_marketplace_rules::TokenMarketplaceRules; + use crate::tokens::emergency_action::TokenEmergencyAction; + use crate::tokens::token_pricing_schedule::TokenPricingSchedule; + use std::collections::BTreeMap; + + /// Helper: build a default ChangeControlRules (NoOne / NoOne). + fn no_one_rules() -> ChangeControlRules { + ChangeControlRules::V0(ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::NoOne, + admin_action_takers: AuthorizedActionTakers::NoOne, + changing_authorized_action_takers_to_no_one_allowed: false, + changing_admin_action_takers_to_no_one_allowed: false, + self_changing_admin_action_takers_allowed: false, + }) + } + + /// Helper: build a TokenConfiguration whose distribution_rules have + /// `new_tokens_destination_identity` set to the given identifier (or None). + fn make_token_config( + new_tokens_dest: Option, + pre_programmed: Option, + perpetual: Option, + ) -> TokenConfiguration { + TokenConfiguration::V0(TokenConfigurationV0 { + conventions: TokenConfigurationConvention::V0(TokenConfigurationConventionV0 { + localizations: BTreeMap::new(), + decimals: 8, + }), + conventions_change_rules: no_one_rules(), + base_supply: 1000, + max_supply: None, + keeps_history: TokenKeepsHistoryRules::V0(TokenKeepsHistoryRulesV0 { + keeps_transfer_history: true, + keeps_freezing_history: true, + keeps_minting_history: true, + keeps_burning_history: true, + keeps_direct_pricing_history: true, + keeps_direct_purchase_history: true, + }), + start_as_paused: false, + allow_transfer_to_frozen_balance: true, + max_supply_change_rules: no_one_rules(), + distribution_rules: TokenDistributionRules::V0(TokenDistributionRulesV0 { + perpetual_distribution: perpetual, + perpetual_distribution_rules: no_one_rules(), + pre_programmed_distribution: pre_programmed, + new_tokens_destination_identity: new_tokens_dest, + new_tokens_destination_identity_rules: no_one_rules(), + minting_allow_choosing_destination: true, + minting_allow_choosing_destination_rules: no_one_rules(), + change_direct_purchase_pricing_rules: no_one_rules(), + }), + marketplace_rules: TokenMarketplaceRules::V0(TokenMarketplaceRulesV0 { + trade_mode: TokenTradeMode::NotTradeable, + trade_mode_change_rules: no_one_rules(), + }), + manual_minting_rules: no_one_rules(), + manual_burning_rules: no_one_rules(), + freeze_rules: no_one_rules(), + unfreeze_rules: no_one_rules(), + destroy_frozen_funds_rules: no_one_rules(), + emergency_action_rules: no_one_rules(), + main_control_group: None, + main_control_group_can_be_modified: AuthorizedActionTakers::NoOne, + description: None, + }) + } + + // ----------------------------------------------------------------------- + // historical_document_type_name + // ----------------------------------------------------------------------- + + #[test] + fn historical_document_type_name_returns_correct_string_per_variant() { + let base = TokenBaseTransition::default(); + let cases: Vec<(TokenTransition, &str)> = vec![ + ( + TokenTransition::Burn(TokenBurnTransition::V0(TokenBurnTransitionV0 { + base: base.clone(), + burn_amount: 0, + public_note: None, + })), + "burn", + ), + ( + TokenTransition::Mint(TokenMintTransition::V0(TokenMintTransitionV0 { + base: base.clone(), + issued_to_identity_id: None, + amount: 0, + public_note: None, + })), + "mint", + ), + ( + TokenTransition::Transfer(TokenTransferTransition::V0(TokenTransferTransitionV0 { + base: base.clone(), + amount: 0, + recipient_id: Identifier::default(), + public_note: None, + shared_encrypted_note: None, + private_encrypted_note: None, + })), + "transfer", + ), + ( + TokenTransition::Freeze(TokenFreezeTransition::V0(TokenFreezeTransitionV0 { + base: base.clone(), + identity_to_freeze_id: Identifier::default(), + public_note: None, + })), + "freeze", + ), + ( + TokenTransition::Unfreeze(TokenUnfreezeTransition::V0(TokenUnfreezeTransitionV0 { + base: base.clone(), + frozen_identity_id: Identifier::default(), + public_note: None, + })), + "unfreeze", + ), + ( + TokenTransition::EmergencyAction(TokenEmergencyActionTransition::V0( + TokenEmergencyActionTransitionV0 { + base: base.clone(), + emergency_action: TokenEmergencyAction::Pause, + public_note: None, + }, + )), + "emergencyAction", + ), + ( + TokenTransition::DestroyFrozenFunds(TokenDestroyFrozenFundsTransition::V0( + TokenDestroyFrozenFundsTransitionV0 { + base: base.clone(), + frozen_identity_id: Identifier::default(), + public_note: None, + }, + )), + "destroyFrozenFunds", + ), + ( + TokenTransition::ConfigUpdate(TokenConfigUpdateTransition::V0( + TokenConfigUpdateTransitionV0 { + base: base.clone(), + update_token_configuration_item: + TokenConfigurationChangeItem::TokenConfigurationNoChange, + public_note: None, + }, + )), + "configUpdate", + ), + ( + TokenTransition::Claim(TokenClaimTransition::V0(TokenClaimTransitionV0 { + base: base.clone(), + distribution_type: TokenDistributionType::PreProgrammed, + public_note: None, + })), + "claim", + ), + ( + TokenTransition::DirectPurchase(TokenDirectPurchaseTransition::V0( + TokenDirectPurchaseTransitionV0 { + base: base.clone(), + token_count: 0, + total_agreed_price: 0, + }, + )), + "directPurchase", + ), + ( + TokenTransition::SetPriceForDirectPurchase( + TokenSetPriceForDirectPurchaseTransition::V0( + TokenSetPriceForDirectPurchaseTransitionV0 { + base: base.clone(), + price: None, + public_note: None, + }, + ), + ), + "directPricing", + ), + ]; + + for (transition, expected_name) in cases { + assert_eq!( + transition.historical_document_type_name(), + expected_name, + "Mismatch for variant that should return '{}'", + expected_name + ); + } + } + + // ----------------------------------------------------------------------- + // can_calculate_action_id + // ----------------------------------------------------------------------- + + #[test] + fn can_calculate_action_id_returns_false_for_transfer_claim_direct_purchase() { + let base = TokenBaseTransition::default(); + + let transfer = + TokenTransition::Transfer(TokenTransferTransition::V0(TokenTransferTransitionV0 { + base: base.clone(), + amount: 100, + recipient_id: Identifier::default(), + public_note: None, + shared_encrypted_note: None, + private_encrypted_note: None, + })); + assert!(!transfer.can_calculate_action_id()); + + let claim = TokenTransition::Claim(TokenClaimTransition::V0(TokenClaimTransitionV0 { + base: base.clone(), + distribution_type: TokenDistributionType::PreProgrammed, + public_note: None, + })); + assert!(!claim.can_calculate_action_id()); + + let direct_purchase = TokenTransition::DirectPurchase(TokenDirectPurchaseTransition::V0( + TokenDirectPurchaseTransitionV0 { + base: base.clone(), + token_count: 1, + total_agreed_price: 100, + }, + )); + assert!(!direct_purchase.can_calculate_action_id()); + } + + #[test] + fn can_calculate_action_id_returns_true_for_all_other_variants() { + let base = TokenBaseTransition::default(); + + let variants: Vec = vec![ + TokenTransition::Burn(TokenBurnTransition::V0(TokenBurnTransitionV0 { + base: base.clone(), + burn_amount: 50, + public_note: None, + })), + TokenTransition::Mint(TokenMintTransition::V0(TokenMintTransitionV0 { + base: base.clone(), + issued_to_identity_id: None, + amount: 100, + public_note: None, + })), + TokenTransition::Freeze(TokenFreezeTransition::V0(TokenFreezeTransitionV0 { + base: base.clone(), + identity_to_freeze_id: Identifier::default(), + public_note: None, + })), + TokenTransition::Unfreeze(TokenUnfreezeTransition::V0(TokenUnfreezeTransitionV0 { + base: base.clone(), + frozen_identity_id: Identifier::default(), + public_note: None, + })), + TokenTransition::DestroyFrozenFunds(TokenDestroyFrozenFundsTransition::V0( + TokenDestroyFrozenFundsTransitionV0 { + base: base.clone(), + frozen_identity_id: Identifier::default(), + public_note: None, + }, + )), + TokenTransition::EmergencyAction(TokenEmergencyActionTransition::V0( + TokenEmergencyActionTransitionV0 { + base: base.clone(), + emergency_action: TokenEmergencyAction::Pause, + public_note: None, + }, + )), + TokenTransition::ConfigUpdate(TokenConfigUpdateTransition::V0( + TokenConfigUpdateTransitionV0 { + base: base.clone(), + update_token_configuration_item: + TokenConfigurationChangeItem::TokenConfigurationNoChange, + public_note: None, + }, + )), + TokenTransition::SetPriceForDirectPurchase( + TokenSetPriceForDirectPurchaseTransition::V0( + TokenSetPriceForDirectPurchaseTransitionV0 { + base: base.clone(), + price: None, + public_note: None, + }, + ), + ), + ]; + + for variant in variants { + assert!( + variant.can_calculate_action_id(), + "Expected can_calculate_action_id() == true for {:?}", + variant + ); + } + } + + // ----------------------------------------------------------------------- + // calculate_action_id + // ----------------------------------------------------------------------- + + #[test] + fn calculate_action_id_returns_none_for_transfer_claim_direct_purchase() { + let base = TokenBaseTransition::default(); + let owner_id = Identifier::from([1u8; 32]); + + let transfer = + TokenTransition::Transfer(TokenTransferTransition::V0(TokenTransferTransitionV0 { + base: base.clone(), + amount: 100, + recipient_id: Identifier::default(), + public_note: None, + shared_encrypted_note: None, + private_encrypted_note: None, + })); + let pv = PlatformVersion::latest(); + assert!(transfer.calculate_action_id(owner_id, pv).is_none()); + + let claim = TokenTransition::Claim(TokenClaimTransition::V0(TokenClaimTransitionV0 { + base: base.clone(), + distribution_type: TokenDistributionType::PreProgrammed, + public_note: None, + })); + assert!(claim.calculate_action_id(owner_id, pv).is_none()); + + let direct_purchase = TokenTransition::DirectPurchase(TokenDirectPurchaseTransition::V0( + TokenDirectPurchaseTransitionV0 { + base: base.clone(), + token_count: 1, + total_agreed_price: 100, + }, + )); + assert!(direct_purchase.calculate_action_id(owner_id, pv).is_none()); + } + + #[test] + fn calculate_action_id_returns_some_for_burn() { + let base = TokenBaseTransition::default(); + let owner_id = Identifier::from([2u8; 32]); + + let burn = TokenTransition::Burn(TokenBurnTransition::V0(TokenBurnTransitionV0 { + base: base.clone(), + burn_amount: 50, + public_note: None, + })); + let pv = PlatformVersion::latest(); + assert!(matches!( + burn.calculate_action_id(owner_id, pv), + Some(Ok(_)) + )); + } + + #[test] + fn calculate_action_id_returns_some_for_mint() { + let base = TokenBaseTransition::default(); + let owner_id = Identifier::from([3u8; 32]); + + let mint = TokenTransition::Mint(TokenMintTransition::V0(TokenMintTransitionV0 { + base: base.clone(), + issued_to_identity_id: None, + amount: 100, + public_note: None, + })); + let pv = PlatformVersion::latest(); + assert!(matches!( + mint.calculate_action_id(owner_id, pv), + Some(Ok(_)) + )); + } + + // ----------------------------------------------------------------------- + // associated_token_event — Burn + // ----------------------------------------------------------------------- + + #[test] + fn associated_token_event_burn_produces_burn_event() { + let owner_id = Identifier::from([10u8; 32]); + let config = make_token_config(None, None, None); + + let transition = TokenTransition::Burn(TokenBurnTransition::V0(TokenBurnTransitionV0 { + base: TokenBaseTransition::default(), + burn_amount: 500, + public_note: Some("burn note".to_string()), + })); + + let event = transition + .associated_token_event(&config, owner_id) + .expect("should produce Burn event"); + match event { + TokenEvent::Burn(amount, burner, note) => { + assert_eq!(amount, 500); + assert_eq!(burner, owner_id); + assert_eq!(note, Some("burn note".to_string())); + } + other => panic!("Expected Burn, got {:?}", other), + } + } + + // ----------------------------------------------------------------------- + // associated_token_event — Mint (explicit recipient) + // ----------------------------------------------------------------------- + + #[test] + fn associated_token_event_mint_with_explicit_recipient() { + let owner_id = Identifier::from([10u8; 32]); + let recipient = Identifier::from([20u8; 32]); + let config = make_token_config(None, None, None); + + let transition = TokenTransition::Mint(TokenMintTransition::V0(TokenMintTransitionV0 { + base: TokenBaseTransition::default(), + issued_to_identity_id: Some(recipient), + amount: 1000, + public_note: None, + })); + + let event = transition + .associated_token_event(&config, owner_id) + .expect("should produce Mint event"); + match event { + TokenEvent::Mint(amount, recv, note) => { + assert_eq!(amount, 1000); + assert_eq!(recv, recipient); + assert!(note.is_none()); + } + other => panic!("Expected Mint, got {:?}", other), + } + } + + // ----------------------------------------------------------------------- + // associated_token_event — Mint (fallback to config destination) + // ----------------------------------------------------------------------- + + #[test] + fn associated_token_event_mint_falls_back_to_config_destination() { + let owner_id = Identifier::from([10u8; 32]); + let config_dest = Identifier::from([30u8; 32]); + let config = make_token_config(Some(config_dest), None, None); + + let transition = TokenTransition::Mint(TokenMintTransition::V0(TokenMintTransitionV0 { + base: TokenBaseTransition::default(), + issued_to_identity_id: None, + amount: 500, + public_note: None, + })); + + let event = transition + .associated_token_event(&config, owner_id) + .expect("should fall back to config destination"); + match event { + TokenEvent::Mint(amount, recv, _) => { + assert_eq!(amount, 500); + assert_eq!(recv, config_dest); + } + other => panic!("Expected Mint, got {:?}", other), + } + } + + // ----------------------------------------------------------------------- + // associated_token_event — Mint (no recipient anywhere -> error) + // ----------------------------------------------------------------------- + + #[test] + fn associated_token_event_mint_errors_when_no_recipient_available() { + let owner_id = Identifier::from([10u8; 32]); + // Config with no new_tokens_destination_identity + let config = make_token_config(None, None, None); + + let transition = TokenTransition::Mint(TokenMintTransition::V0(TokenMintTransitionV0 { + base: TokenBaseTransition::default(), + issued_to_identity_id: None, + amount: 500, + public_note: None, + })); + + let result = transition.associated_token_event(&config, owner_id); + assert!(result.is_err()); + match result.unwrap_err() { + ProtocolError::NotSupported(msg) => { + assert!(msg.contains("mint destination")); + } + other => panic!("Expected NotSupported, got {:?}", other), + } + } + + // ----------------------------------------------------------------------- + // associated_token_event — Transfer + // ----------------------------------------------------------------------- + + #[test] + fn associated_token_event_transfer_produces_transfer_event() { + let owner_id = Identifier::from([10u8; 32]); + let recipient = Identifier::from([20u8; 32]); + let config = make_token_config(None, None, None); + + let transition = + TokenTransition::Transfer(TokenTransferTransition::V0(TokenTransferTransitionV0 { + base: TokenBaseTransition::default(), + amount: 250, + recipient_id: recipient, + public_note: Some("transfer note".to_string()), + shared_encrypted_note: None, + private_encrypted_note: None, + })); + + let event = transition + .associated_token_event(&config, owner_id) + .expect("should produce Transfer event"); + match event { + TokenEvent::Transfer(recv, public_note, shared, private, amount) => { + assert_eq!(recv, recipient); + assert_eq!(amount, 250); + assert_eq!(public_note, Some("transfer note".to_string())); + assert!(shared.is_none()); + assert!(private.is_none()); + } + other => panic!("Expected Transfer, got {:?}", other), + } + } + + // ----------------------------------------------------------------------- + // associated_token_event — Freeze / Unfreeze + // ----------------------------------------------------------------------- + + #[test] + fn associated_token_event_freeze_and_unfreeze() { + let owner_id = Identifier::from([10u8; 32]); + let frozen_id = Identifier::from([40u8; 32]); + let config = make_token_config(None, None, None); + + let freeze = TokenTransition::Freeze(TokenFreezeTransition::V0(TokenFreezeTransitionV0 { + base: TokenBaseTransition::default(), + identity_to_freeze_id: frozen_id, + public_note: Some("freeze!".to_string()), + })); + let event = freeze + .associated_token_event(&config, owner_id) + .expect("should produce Freeze event"); + match event { + TokenEvent::Freeze(id, note) => { + assert_eq!(id, frozen_id); + assert_eq!(note, Some("freeze!".to_string())); + } + other => panic!("Expected Freeze, got {:?}", other), + } + + let unfreeze = + TokenTransition::Unfreeze(TokenUnfreezeTransition::V0(TokenUnfreezeTransitionV0 { + base: TokenBaseTransition::default(), + frozen_identity_id: frozen_id, + public_note: None, + })); + let event = unfreeze + .associated_token_event(&config, owner_id) + .expect("should produce Unfreeze event"); + match event { + TokenEvent::Unfreeze(id, note) => { + assert_eq!(id, frozen_id); + assert!(note.is_none()); + } + other => panic!("Expected Unfreeze, got {:?}", other), + } + } + + // ----------------------------------------------------------------------- + // associated_token_event — EmergencyAction + // ----------------------------------------------------------------------- + + #[test] + fn associated_token_event_emergency_action() { + let owner_id = Identifier::from([10u8; 32]); + let config = make_token_config(None, None, None); + + let transition = TokenTransition::EmergencyAction(TokenEmergencyActionTransition::V0( + TokenEmergencyActionTransitionV0 { + base: TokenBaseTransition::default(), + emergency_action: TokenEmergencyAction::Pause, + public_note: Some("pausing".to_string()), + }, + )); + + let event = transition + .associated_token_event(&config, owner_id) + .expect("should produce EmergencyAction event"); + match event { + TokenEvent::EmergencyAction(action, note) => { + assert_eq!(action, TokenEmergencyAction::Pause); + assert_eq!(note, Some("pausing".to_string())); + } + other => panic!("Expected EmergencyAction, got {:?}", other), + } + } + + // ----------------------------------------------------------------------- + // associated_token_event — DestroyFrozenFunds + // ----------------------------------------------------------------------- + + #[test] + fn associated_token_event_destroy_frozen_funds() { + let owner_id = Identifier::from([10u8; 32]); + let frozen_id = Identifier::from([50u8; 32]); + let config = make_token_config(None, None, None); + + let transition = TokenTransition::DestroyFrozenFunds( + TokenDestroyFrozenFundsTransition::V0(TokenDestroyFrozenFundsTransitionV0 { + base: TokenBaseTransition::default(), + frozen_identity_id: frozen_id, + public_note: None, + }), + ); + + let event = transition + .associated_token_event(&config, owner_id) + .expect("should produce DestroyFrozenFunds event"); + match event { + TokenEvent::DestroyFrozenFunds(id, amount, note) => { + assert_eq!(id, frozen_id); + assert_eq!(amount, TokenAmount::MAX); + assert!(note.is_none()); + } + other => panic!("Expected DestroyFrozenFunds, got {:?}", other), + } + } + + // ----------------------------------------------------------------------- + // associated_token_event — ConfigUpdate + // ----------------------------------------------------------------------- + + #[test] + fn associated_token_event_config_update() { + let owner_id = Identifier::from([10u8; 32]); + let config = make_token_config(None, None, None); + + let transition = TokenTransition::ConfigUpdate(TokenConfigUpdateTransition::V0( + TokenConfigUpdateTransitionV0 { + base: TokenBaseTransition::default(), + update_token_configuration_item: TokenConfigurationChangeItem::MaxSupply(Some( + 9999, + )), + public_note: Some("cap update".to_string()), + }, + )); + + let event = transition + .associated_token_event(&config, owner_id) + .expect("should produce ConfigUpdate event"); + match event { + TokenEvent::ConfigUpdate(item, note) => { + assert_eq!(item, TokenConfigurationChangeItem::MaxSupply(Some(9999))); + assert_eq!(note, Some("cap update".to_string())); + } + other => panic!("Expected ConfigUpdate, got {:?}", other), + } + } + + // ----------------------------------------------------------------------- + // associated_token_event — DirectPurchase + // ----------------------------------------------------------------------- + + #[test] + fn associated_token_event_direct_purchase() { + let owner_id = Identifier::from([10u8; 32]); + let config = make_token_config(None, None, None); + + let transition = TokenTransition::DirectPurchase(TokenDirectPurchaseTransition::V0( + TokenDirectPurchaseTransitionV0 { + base: TokenBaseTransition::default(), + token_count: 42, + total_agreed_price: 84000, + }, + )); + + let event = transition + .associated_token_event(&config, owner_id) + .expect("should produce DirectPurchase event"); + match event { + TokenEvent::DirectPurchase(count, price) => { + assert_eq!(count, 42); + assert_eq!(price, 84000); + } + other => panic!("Expected DirectPurchase, got {:?}", other), + } + } + + // ----------------------------------------------------------------------- + // associated_token_event — SetPriceForDirectPurchase + // ----------------------------------------------------------------------- + + #[test] + fn associated_token_event_set_price_for_direct_purchase() { + let owner_id = Identifier::from([10u8; 32]); + let config = make_token_config(None, None, None); + let pricing = TokenPricingSchedule::SinglePrice(5000); + + let transition = TokenTransition::SetPriceForDirectPurchase( + TokenSetPriceForDirectPurchaseTransition::V0( + TokenSetPriceForDirectPurchaseTransitionV0 { + base: TokenBaseTransition::default(), + price: Some(pricing.clone()), + public_note: Some("pricing change".to_string()), + }, + ), + ); + + let event = transition + .associated_token_event(&config, owner_id) + .expect("should produce ChangePriceForDirectPurchase event"); + match event { + TokenEvent::ChangePriceForDirectPurchase(p, note) => { + assert_eq!(p, Some(pricing)); + assert_eq!(note, Some("pricing change".to_string())); + } + other => panic!("Expected ChangePriceForDirectPurchase, got {:?}", other), + } + } + + // ----------------------------------------------------------------------- + // associated_token_event — Claim (PreProgrammed, no distribution -> error) + // ----------------------------------------------------------------------- + + #[test] + fn associated_token_event_claim_pre_programmed_errors_when_none() { + let owner_id = Identifier::from([10u8; 32]); + let config = make_token_config(None, None, None); + + let transition = TokenTransition::Claim(TokenClaimTransition::V0(TokenClaimTransitionV0 { + base: TokenBaseTransition::default(), + distribution_type: TokenDistributionType::PreProgrammed, + public_note: None, + })); + + let result = transition.associated_token_event(&config, owner_id); + assert!(result.is_err()); + match result.unwrap_err() { + ProtocolError::NotSupported(msg) => { + assert!(msg.contains("pre programmed")); + } + other => panic!("Expected NotSupported, got {:?}", other), + } + } + + // ----------------------------------------------------------------------- + // associated_token_event — Claim (PreProgrammed, distribution present) + // ----------------------------------------------------------------------- + + #[test] + fn associated_token_event_claim_pre_programmed_succeeds_when_present() { + let owner_id = Identifier::from([10u8; 32]); + let pre_prog = TokenPreProgrammedDistribution::V0(TokenPreProgrammedDistributionV0 { + distributions: BTreeMap::new(), + }); + let config = make_token_config(None, Some(pre_prog), None); + + let transition = TokenTransition::Claim(TokenClaimTransition::V0(TokenClaimTransitionV0 { + base: TokenBaseTransition::default(), + distribution_type: TokenDistributionType::PreProgrammed, + public_note: Some("claiming".to_string()), + })); + + let event = transition + .associated_token_event(&config, owner_id) + .expect("should produce Claim event"); + match event { + TokenEvent::Claim(dist_type, amount, note) => { + assert_eq!( + dist_type, + TokenDistributionTypeWithResolvedRecipient::PreProgrammed(owner_id) + ); + assert_eq!(amount, TokenAmount::MAX); + assert_eq!(note, Some("claiming".to_string())); + } + other => panic!("Expected Claim, got {:?}", other), + } + } + + // ----------------------------------------------------------------------- + // associated_token_event — Claim (Perpetual, no distribution -> error) + // ----------------------------------------------------------------------- + + #[test] + fn associated_token_event_claim_perpetual_errors_when_none() { + let owner_id = Identifier::from([10u8; 32]); + let config = make_token_config(None, None, None); + + let transition = TokenTransition::Claim(TokenClaimTransition::V0(TokenClaimTransitionV0 { + base: TokenBaseTransition::default(), + distribution_type: TokenDistributionType::Perpetual, + public_note: None, + })); + + let result = transition.associated_token_event(&config, owner_id); + assert!(result.is_err()); + match result.unwrap_err() { + ProtocolError::NotSupported(msg) => { + assert!(msg.contains("perpetual")); + } + other => panic!("Expected NotSupported, got {:?}", other), + } + } + + // ----------------------------------------------------------------------- + // associated_token_event — Claim (Perpetual, ContractOwner recipient) + // ----------------------------------------------------------------------- + + #[test] + fn associated_token_event_claim_perpetual_contract_owner_recipient() { + let owner_id = Identifier::from([10u8; 32]); + let perpetual = TokenPerpetualDistribution::V0(TokenPerpetualDistributionV0 { + distribution_type: RewardDistributionType::BlockBasedDistribution { + interval: 10, + function: DistributionFunction::FixedAmount { amount: 100 }, + }, + distribution_recipient: TokenDistributionRecipient::ContractOwner, + }); + let config = make_token_config(None, None, Some(perpetual)); + + let transition = TokenTransition::Claim(TokenClaimTransition::V0(TokenClaimTransitionV0 { + base: TokenBaseTransition::default(), + distribution_type: TokenDistributionType::Perpetual, + public_note: None, + })); + + let event = transition + .associated_token_event(&config, owner_id) + .expect("should produce Claim event"); + match event { + TokenEvent::Claim(dist_type, _, _) => { + assert_eq!( + dist_type, + TokenDistributionTypeWithResolvedRecipient::Perpetual( + TokenDistributionResolvedRecipient::ContractOwnerIdentity(owner_id) + ) + ); + } + other => panic!("Expected Claim, got {:?}", other), + } + } + + // ----------------------------------------------------------------------- + // associated_token_event — Claim (Perpetual, Identity recipient) + // ----------------------------------------------------------------------- + + #[test] + fn associated_token_event_claim_perpetual_identity_recipient() { + let owner_id = Identifier::from([10u8; 32]); + let specific_id = Identifier::from([77u8; 32]); + let perpetual = TokenPerpetualDistribution::V0(TokenPerpetualDistributionV0 { + distribution_type: RewardDistributionType::BlockBasedDistribution { + interval: 10, + function: DistributionFunction::FixedAmount { amount: 100 }, + }, + distribution_recipient: TokenDistributionRecipient::Identity(specific_id), + }); + let config = make_token_config(None, None, Some(perpetual)); + + let transition = TokenTransition::Claim(TokenClaimTransition::V0(TokenClaimTransitionV0 { + base: TokenBaseTransition::default(), + distribution_type: TokenDistributionType::Perpetual, + public_note: None, + })); + + let event = transition + .associated_token_event(&config, owner_id) + .expect("should produce Claim event"); + match event { + TokenEvent::Claim(dist_type, _, _) => { + assert_eq!( + dist_type, + TokenDistributionTypeWithResolvedRecipient::Perpetual( + TokenDistributionResolvedRecipient::Identity(specific_id) + ) + ); + } + other => panic!("Expected Claim, got {:?}", other), + } + } + + // ----------------------------------------------------------------------- + // associated_token_event — Claim (Perpetual, EvonodesByParticipation) + // ----------------------------------------------------------------------- + + #[test] + fn associated_token_event_claim_perpetual_evonodes_recipient() { + let owner_id = Identifier::from([10u8; 32]); + let perpetual = TokenPerpetualDistribution::V0(TokenPerpetualDistributionV0 { + distribution_type: RewardDistributionType::BlockBasedDistribution { + interval: 10, + function: DistributionFunction::FixedAmount { amount: 100 }, + }, + distribution_recipient: TokenDistributionRecipient::EvonodesByParticipation, + }); + let config = make_token_config(None, None, Some(perpetual)); + + let transition = TokenTransition::Claim(TokenClaimTransition::V0(TokenClaimTransitionV0 { + base: TokenBaseTransition::default(), + distribution_type: TokenDistributionType::Perpetual, + public_note: None, + })); + + let event = transition + .associated_token_event(&config, owner_id) + .expect("should produce Claim event"); + match event { + TokenEvent::Claim(dist_type, _, _) => { + assert_eq!( + dist_type, + TokenDistributionTypeWithResolvedRecipient::Perpetual( + TokenDistributionResolvedRecipient::Evonode(owner_id) + ) + ); + } + other => panic!("Expected Claim, got {:?}", other), + } + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unfreeze_transition/validate_structure/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unfreeze_transition/validate_structure/v0/mod.rs index f5f0bf550dd..7c3a8fa2cf8 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unfreeze_transition/validate_structure/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unfreeze_transition/validate_structure/v0/mod.rs @@ -42,3 +42,86 @@ impl TokenUnfreezeTransitionStructureValidationV0 for TokenUnfreezeTransition { Ok(SimpleConsensusValidationResult::default()) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::group::GroupStateTransitionInfo; + use crate::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; + use crate::state_transition::batch_transition::token_unfreeze_transition::v0::TokenUnfreezeTransitionV0; + use platform_value::Identifier; + + fn make_transition( + public_note: Option, + using_group_info: Option, + ) -> TokenUnfreezeTransition { + TokenUnfreezeTransition::V0(TokenUnfreezeTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: Identifier::default(), + token_id: Identifier::default(), + using_group_info, + }), + frozen_identity_id: Identifier::default(), + public_note, + }) + } + + #[test] + fn should_pass_with_no_public_note() { + let transition = make_transition(None, None); + let result = transition.validate_structure_v0().unwrap(); + assert!(result.is_valid()); + } + + #[test] + fn should_pass_with_short_public_note_and_no_group() { + let transition = make_transition(Some("hello".to_string()), None); + let result = transition.validate_structure_v0().unwrap(); + assert!(result.is_valid()); + } + + #[test] + fn should_pass_with_public_note_and_proposer_group() { + let group_info = GroupStateTransitionInfo { + group_contract_position: 0, + action_id: Identifier::default(), + action_is_proposer: true, + }; + let transition = make_transition(Some("hello".to_string()), Some(group_info)); + let result = transition.validate_structure_v0().unwrap(); + assert!(result.is_valid()); + } + + #[test] + fn should_return_error_when_public_note_too_big() { + let long_note = "a".repeat(MAX_TOKEN_NOTE_LEN + 1); + let transition = make_transition(Some(long_note), None); + let result = transition.validate_structure_v0().unwrap(); + assert!(!result.is_valid()); + let error = result.errors.first().unwrap(); + assert!(matches!( + error, + ConsensusError::BasicError(BasicError::InvalidTokenNoteTooBigError(_)) + )); + } + + #[test] + fn should_return_error_when_note_present_but_non_proposer_in_group() { + let group_info = GroupStateTransitionInfo { + group_contract_position: 0, + action_id: Identifier::default(), + action_is_proposer: false, + }; + let transition = make_transition(Some("hello".to_string()), Some(group_info)); + let result = transition.validate_structure_v0().unwrap(); + assert!(!result.is_valid()); + let error = result.errors.first().unwrap(); + assert!(matches!( + error, + ConsensusError::BasicError(BasicError::TokenNoteOnlyAllowedWhenProposerError(_)) + )); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/common_validation.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/common_validation.rs index 9daf7c0e598..3cf21276f38 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/common_validation.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/common_validation.rs @@ -49,3 +49,126 @@ pub fn validate_anchor_not_zero(anchor: &[u8; 32]) -> SimpleConsensusValidationR SimpleConsensusValidationResult::new() } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::consensus::ConsensusError; + use assert_matches::assert_matches; + + fn dummy_action() -> SerializedAction { + SerializedAction { + nullifier: [1u8; 32], + rk: [2u8; 32], + cmx: [3u8; 32], + encrypted_note: vec![4u8; 216], + cv_net: [5u8; 32], + spend_auth_sig: [6u8; 64], + } + } + + // --- validate_actions_count --- + + #[test] + fn validate_actions_count_should_reject_empty_actions() { + let result = validate_actions_count(&[], 100); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedNoActionsError(_) + )] + ); + } + + #[test] + fn validate_actions_count_should_accept_single_action() { + let actions = vec![dummy_action()]; + let result = validate_actions_count(&actions, 100); + assert!( + result.is_valid(), + "Expected valid, got: {:?}", + result.errors + ); + } + + #[test] + fn validate_actions_count_should_accept_exactly_max_actions() { + let actions = vec![dummy_action(); 5]; + let result = validate_actions_count(&actions, 5); + assert!( + result.is_valid(), + "Expected valid, got: {:?}", + result.errors + ); + } + + #[test] + fn validate_actions_count_should_reject_more_than_max_actions() { + let actions = vec![dummy_action(); 6]; + let result = validate_actions_count(&actions, 5); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedTooManyActionsError(_) + )] + ); + } + + // --- validate_proof_not_empty --- + + #[test] + fn validate_proof_not_empty_should_reject_empty_proof() { + let result = validate_proof_not_empty(&[]); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedEmptyProofError(_) + )] + ); + } + + #[test] + fn validate_proof_not_empty_should_accept_non_empty_proof() { + let result = validate_proof_not_empty(&[1u8; 100]); + assert!( + result.is_valid(), + "Expected valid, got: {:?}", + result.errors + ); + } + + // --- validate_anchor_not_zero --- + + #[test] + fn validate_anchor_not_zero_should_reject_all_zero_anchor() { + let result = validate_anchor_not_zero(&[0u8; 32]); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedZeroAnchorError(_) + )] + ); + } + + #[test] + fn validate_anchor_not_zero_should_accept_non_zero_anchor() { + let result = validate_anchor_not_zero(&[7u8; 32]); + assert!( + result.is_valid(), + "Expected valid, got: {:?}", + result.errors + ); + } + + #[test] + fn validate_anchor_not_zero_should_accept_single_bit_set() { + let mut anchor = [0u8; 32]; + anchor[31] = 1; + let result = validate_anchor_not_zero(&anchor); + assert!( + result.is_valid(), + "Expected valid, got: {:?}", + result.errors + ); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v0/state_transition_validation.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v0/state_transition_validation.rs index d29bf1316c5..eb00a805dee 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v0/state_transition_validation.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v0/state_transition_validation.rs @@ -63,3 +63,161 @@ impl StateTransitionStructureValidation for ShieldFromAssetLockTransitionV0 { SimpleConsensusValidationResult::new() } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::consensus::ConsensusError; + use crate::identity::state_transition::asset_lock_proof::chain::ChainAssetLockProof; + use crate::identity::state_transition::asset_lock_proof::AssetLockProof; + use assert_matches::assert_matches; + use dashcore::OutPoint; + use platform_value::BinaryData; + + fn dummy_action() -> crate::shielded::SerializedAction { + crate::shielded::SerializedAction { + nullifier: [1u8; 32], + rk: [2u8; 32], + cmx: [3u8; 32], + encrypted_note: vec![4u8; 216], + cv_net: [5u8; 32], + spend_auth_sig: [6u8; 64], + } + } + + fn valid_shield_from_asset_lock_transition() -> ShieldFromAssetLockTransitionV0 { + let chain_proof = ChainAssetLockProof { + core_chain_locked_height: 100, + out_point: OutPoint::from([11u8; 36]), + }; + + ShieldFromAssetLockTransitionV0 { + asset_lock_proof: AssetLockProof::Chain(chain_proof), + actions: vec![dummy_action()], + value_balance: 1_000_000u64, + anchor: [7u8; 32], + proof: vec![8u8; 100], + binding_signature: [9u8; 64], + signature: BinaryData::new(vec![10u8; 65]), + } + } + + #[test] + fn should_validate_a_valid_transition() { + let platform_version = PlatformVersion::latest(); + let transition = valid_shield_from_asset_lock_transition(); + let result = transition.validate_structure(platform_version); + assert!( + result.is_valid(), + "Expected valid result, got errors: {:?}", + result.errors + ); + } + + #[test] + fn should_reject_empty_actions() { + let platform_version = PlatformVersion::latest(); + let mut transition = valid_shield_from_asset_lock_transition(); + transition.actions.clear(); + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedNoActionsError(_) + )] + ); + } + + #[test] + fn should_reject_too_many_actions() { + let platform_version = PlatformVersion::latest(); + let max = platform_version + .system_limits + .max_shielded_transition_actions; + let mut transition = valid_shield_from_asset_lock_transition(); + transition.actions = vec![dummy_action(); max as usize + 1]; + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedTooManyActionsError(_) + )] + ); + } + + #[test] + fn should_reject_zero_value_balance() { + let platform_version = PlatformVersion::latest(); + let mut transition = valid_shield_from_asset_lock_transition(); + transition.value_balance = 0; + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedInvalidValueBalanceError(_) + )] + ); + } + + #[test] + fn should_reject_value_balance_exceeding_i64_max() { + let platform_version = PlatformVersion::latest(); + let mut transition = valid_shield_from_asset_lock_transition(); + transition.value_balance = i64::MAX as u64 + 1; + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedInvalidValueBalanceError(_) + )] + ); + } + + #[test] + fn should_accept_value_balance_at_i64_max() { + let platform_version = PlatformVersion::latest(); + let mut transition = valid_shield_from_asset_lock_transition(); + transition.value_balance = i64::MAX as u64; + + let result = transition.validate_structure(platform_version); + assert!( + result.is_valid(), + "Expected valid result, got errors: {:?}", + result.errors + ); + } + + #[test] + fn should_reject_empty_proof() { + let platform_version = PlatformVersion::latest(); + let mut transition = valid_shield_from_asset_lock_transition(); + transition.proof.clear(); + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedEmptyProofError(_) + )] + ); + } + + #[test] + fn should_reject_zero_anchor() { + let platform_version = PlatformVersion::latest(); + let mut transition = valid_shield_from_asset_lock_transition(); + transition.anchor = [0u8; 32]; + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedZeroAnchorError(_) + )] + ); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_transition/v0/state_transition_validation.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_transition/v0/state_transition_validation.rs index f48d9d252b3..1b92ffad1c9 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_transition/v0/state_transition_validation.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_transition/v0/state_transition_validation.rs @@ -166,3 +166,337 @@ impl StateTransitionStructureValidation for ShieldTransitionV0 { SimpleConsensusValidationResult::new() } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::address_funds::{AddressFundsFeeStrategyStep, AddressWitness, PlatformAddress}; + use crate::consensus::ConsensusError; + use assert_matches::assert_matches; + use std::collections::BTreeMap; + + fn dummy_action() -> crate::shielded::SerializedAction { + crate::shielded::SerializedAction { + nullifier: [1u8; 32], + rk: [2u8; 32], + cmx: [3u8; 32], + encrypted_note: vec![4u8; 216], + cv_net: [5u8; 32], + spend_auth_sig: [6u8; 64], + } + } + + /// Creates a valid ShieldTransitionV0 that passes all validation checks. + fn valid_shield_transition() -> ShieldTransitionV0 { + let mut inputs = BTreeMap::new(); + inputs.insert(PlatformAddress::P2pkh([1u8; 20]), (0u32, 1_000_000u64)); + + ShieldTransitionV0 { + inputs, + actions: vec![dummy_action()], + amount: 500_000u64, + anchor: [7u8; 32], + proof: vec![8u8; 100], + binding_signature: [9u8; 64], + fee_strategy: vec![AddressFundsFeeStrategyStep::DeductFromInput(0)], + user_fee_increase: 0u16, + input_witnesses: vec![AddressWitness::P2pkh { + signature: vec![0u8; 65].into(), + }], + } + } + + #[test] + fn should_validate_a_valid_shield_transition() { + let platform_version = PlatformVersion::latest(); + let transition = valid_shield_transition(); + let result = transition.validate_structure(platform_version); + assert!( + result.is_valid(), + "Expected valid result, got errors: {:?}", + result.errors + ); + } + + #[test] + fn should_reject_empty_actions() { + let platform_version = PlatformVersion::latest(); + let mut transition = valid_shield_transition(); + transition.actions.clear(); + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedNoActionsError(_) + )] + ); + } + + #[test] + fn should_reject_too_many_actions() { + let platform_version = PlatformVersion::latest(); + let max = platform_version + .system_limits + .max_shielded_transition_actions; + let mut transition = valid_shield_transition(); + transition.actions = vec![dummy_action(); max as usize + 1]; + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedTooManyActionsError(_) + )] + ); + } + + #[test] + fn should_reject_empty_inputs() { + let platform_version = PlatformVersion::latest(); + let mut transition = valid_shield_transition(); + transition.inputs.clear(); + transition.input_witnesses.clear(); + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::TransitionNoInputsError(_) + )] + ); + } + + #[test] + fn should_reject_witness_count_mismatch() { + let platform_version = PlatformVersion::latest(); + let mut transition = valid_shield_transition(); + transition.input_witnesses.clear(); + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::InputWitnessCountMismatchError(_) + )] + ); + } + + #[test] + fn should_reject_input_below_minimum() { + let platform_version = PlatformVersion::latest(); + let mut transition = valid_shield_transition(); + // Set the input amount to 1 (below minimum of 100_000) + transition.inputs.clear(); + transition + .inputs + .insert(PlatformAddress::P2pkh([1u8; 20]), (0u32, 1u64)); + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::InputBelowMinimumError(_) + )] + ); + } + + #[test] + fn should_reject_zero_amount() { + let platform_version = PlatformVersion::latest(); + let mut transition = valid_shield_transition(); + transition.amount = 0; + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedInvalidValueBalanceError(_) + )] + ); + } + + #[test] + fn should_reject_amount_exceeding_i64_max() { + let platform_version = PlatformVersion::latest(); + let mut transition = valid_shield_transition(); + transition.amount = i64::MAX as u64 + 1; + // Also make input sum large enough + transition.inputs.clear(); + transition + .inputs + .insert(PlatformAddress::P2pkh([1u8; 20]), (0u32, u64::MAX)); + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedInvalidValueBalanceError(_) + )] + ); + } + + #[test] + fn should_reject_input_sum_less_than_shield_amount() { + let platform_version = PlatformVersion::latest(); + let mut transition = valid_shield_transition(); + // Input = 1_000_000 but amount = 2_000_000 + transition.amount = 2_000_000; + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedInvalidValueBalanceError(_) + )] + ); + } + + #[test] + fn should_reject_input_sum_overflow() { + let platform_version = PlatformVersion::latest(); + let mut transition = valid_shield_transition(); + transition.inputs.clear(); + transition + .inputs + .insert(PlatformAddress::P2pkh([1u8; 20]), (0u32, u64::MAX)); + transition + .inputs + .insert(PlatformAddress::P2pkh([2u8; 20]), (0u32, u64::MAX)); + transition.input_witnesses = vec![ + AddressWitness::P2pkh { + signature: vec![0u8; 65].into(), + }, + AddressWitness::P2pkh { + signature: vec![0u8; 65].into(), + }, + ]; + transition.amount = 1_000_000; + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedInvalidValueBalanceError(_) + )] + ); + } + + #[test] + fn should_reject_empty_proof() { + let platform_version = PlatformVersion::latest(); + let mut transition = valid_shield_transition(); + transition.proof.clear(); + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedEmptyProofError(_) + )] + ); + } + + #[test] + fn should_reject_zero_anchor() { + let platform_version = PlatformVersion::latest(); + let mut transition = valid_shield_transition(); + transition.anchor = [0u8; 32]; + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedZeroAnchorError(_) + )] + ); + } + + #[test] + fn should_reject_empty_fee_strategy() { + let platform_version = PlatformVersion::latest(); + let mut transition = valid_shield_transition(); + transition.fee_strategy.clear(); + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::FeeStrategyEmptyError(_) + )] + ); + } + + #[test] + fn should_reject_too_many_fee_strategy_steps() { + let platform_version = PlatformVersion::latest(); + let max = platform_version + .dpp + .state_transitions + .max_address_fee_strategies; + let mut transition = valid_shield_transition(); + // Build enough inputs to support the indices + transition.inputs.clear(); + transition.input_witnesses.clear(); + transition.fee_strategy.clear(); + for i in 0..=(max as usize) { + let mut hash = [0u8; 20]; + hash[0] = (i & 0xFF) as u8; + hash[1] = ((i >> 8) & 0xFF) as u8; + transition + .inputs + .insert(PlatformAddress::P2pkh(hash), (0u32, 1_000_000u64)); + transition.input_witnesses.push(AddressWitness::P2pkh { + signature: vec![0u8; 65].into(), + }); + transition + .fee_strategy + .push(AddressFundsFeeStrategyStep::DeductFromInput(i as u16)); + } + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::FeeStrategyTooManyStepsError(_) + )] + ); + } + + #[test] + fn should_reject_duplicate_fee_strategy_steps() { + let platform_version = PlatformVersion::latest(); + let mut transition = valid_shield_transition(); + transition.fee_strategy = vec![ + AddressFundsFeeStrategyStep::DeductFromInput(0), + AddressFundsFeeStrategyStep::DeductFromInput(0), + ]; + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::FeeStrategyDuplicateError(_) + )] + ); + } + + #[test] + fn should_accept_input_sum_exactly_equal_to_amount() { + let platform_version = PlatformVersion::latest(); + let mut transition = valid_shield_transition(); + // Set input exactly equal to amount + transition.inputs.clear(); + transition + .inputs + .insert(PlatformAddress::P2pkh([1u8; 20]), (0u32, 500_000u64)); + transition.amount = 500_000; + + let result = transition.validate_structure(platform_version); + assert!( + result.is_valid(), + "Expected valid result, got errors: {:?}", + result.errors + ); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_transfer_transition/v0/state_transition_validation.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_transfer_transition/v0/state_transition_validation.rs index 091cead328c..f034e9ea8ba 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_transfer_transition/v0/state_transition_validation.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_transfer_transition/v0/state_transition_validation.rs @@ -63,3 +63,150 @@ impl StateTransitionStructureValidation for ShieldedTransferTransitionV0 { SimpleConsensusValidationResult::new() } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::consensus::ConsensusError; + use assert_matches::assert_matches; + + fn dummy_action() -> crate::shielded::SerializedAction { + crate::shielded::SerializedAction { + nullifier: [1u8; 32], + rk: [2u8; 32], + cmx: [3u8; 32], + encrypted_note: vec![4u8; 216], + cv_net: [5u8; 32], + spend_auth_sig: [6u8; 64], + } + } + + fn valid_shielded_transfer_transition() -> ShieldedTransferTransitionV0 { + ShieldedTransferTransitionV0 { + actions: vec![dummy_action()], + value_balance: 10_000u64, + anchor: [7u8; 32], + proof: vec![8u8; 100], + binding_signature: [9u8; 64], + } + } + + #[test] + fn should_validate_a_valid_transition() { + let platform_version = PlatformVersion::latest(); + let transition = valid_shielded_transfer_transition(); + let result = transition.validate_structure(platform_version); + assert!( + result.is_valid(), + "Expected valid result, got errors: {:?}", + result.errors + ); + } + + #[test] + fn should_reject_empty_actions() { + let platform_version = PlatformVersion::latest(); + let mut transition = valid_shielded_transfer_transition(); + transition.actions.clear(); + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedNoActionsError(_) + )] + ); + } + + #[test] + fn should_reject_too_many_actions() { + let platform_version = PlatformVersion::latest(); + let max = platform_version + .system_limits + .max_shielded_transition_actions; + let mut transition = valid_shielded_transfer_transition(); + transition.actions = vec![dummy_action(); max as usize + 1]; + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedTooManyActionsError(_) + )] + ); + } + + #[test] + fn should_reject_zero_value_balance() { + let platform_version = PlatformVersion::latest(); + let mut transition = valid_shielded_transfer_transition(); + transition.value_balance = 0; + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedInvalidValueBalanceError(_) + )] + ); + } + + #[test] + fn should_reject_value_balance_exceeding_i64_max() { + let platform_version = PlatformVersion::latest(); + let mut transition = valid_shielded_transfer_transition(); + transition.value_balance = i64::MAX as u64 + 1; + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedInvalidValueBalanceError(_) + )] + ); + } + + #[test] + fn should_accept_value_balance_at_i64_max() { + let platform_version = PlatformVersion::latest(); + let mut transition = valid_shielded_transfer_transition(); + transition.value_balance = i64::MAX as u64; + + let result = transition.validate_structure(platform_version); + assert!( + result.is_valid(), + "Expected valid result, got errors: {:?}", + result.errors + ); + } + + #[test] + fn should_reject_empty_proof() { + let platform_version = PlatformVersion::latest(); + let mut transition = valid_shielded_transfer_transition(); + transition.proof.clear(); + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedEmptyProofError(_) + )] + ); + } + + #[test] + fn should_reject_zero_anchor() { + let platform_version = PlatformVersion::latest(); + let mut transition = valid_shielded_transfer_transition(); + transition.anchor = [0u8; 32]; + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedZeroAnchorError(_) + )] + ); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_withdrawal_transition/v0/state_transition_validation.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_withdrawal_transition/v0/state_transition_validation.rs index 41821b3e258..a5c39469f0e 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_withdrawal_transition/v0/state_transition_validation.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_withdrawal_transition/v0/state_transition_validation.rs @@ -63,3 +63,155 @@ impl StateTransitionStructureValidation for ShieldedWithdrawalTransitionV0 { SimpleConsensusValidationResult::new() } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::consensus::ConsensusError; + use crate::identity::core_script::CoreScript; + use crate::withdrawal::Pooling; + use assert_matches::assert_matches; + + fn dummy_action() -> crate::shielded::SerializedAction { + crate::shielded::SerializedAction { + nullifier: [1u8; 32], + rk: [2u8; 32], + cmx: [3u8; 32], + encrypted_note: vec![4u8; 216], + cv_net: [5u8; 32], + spend_auth_sig: [6u8; 64], + } + } + + fn valid_shielded_withdrawal_transition() -> ShieldedWithdrawalTransitionV0 { + ShieldedWithdrawalTransitionV0 { + actions: vec![dummy_action()], + unshielding_amount: 1_000_000u64, + anchor: [7u8; 32], + proof: vec![8u8; 100], + binding_signature: [9u8; 64], + core_fee_per_byte: 1u32, + pooling: Pooling::Never, + output_script: CoreScript::new_p2pkh([11u8; 20]), + } + } + + #[test] + fn should_validate_a_valid_transition() { + let platform_version = PlatformVersion::latest(); + let transition = valid_shielded_withdrawal_transition(); + let result = transition.validate_structure(platform_version); + assert!( + result.is_valid(), + "Expected valid result, got errors: {:?}", + result.errors + ); + } + + #[test] + fn should_reject_empty_actions() { + let platform_version = PlatformVersion::latest(); + let mut transition = valid_shielded_withdrawal_transition(); + transition.actions.clear(); + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedNoActionsError(_) + )] + ); + } + + #[test] + fn should_reject_too_many_actions() { + let platform_version = PlatformVersion::latest(); + let max = platform_version + .system_limits + .max_shielded_transition_actions; + let mut transition = valid_shielded_withdrawal_transition(); + transition.actions = vec![dummy_action(); max as usize + 1]; + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedTooManyActionsError(_) + )] + ); + } + + #[test] + fn should_reject_zero_unshielding_amount() { + let platform_version = PlatformVersion::latest(); + let mut transition = valid_shielded_withdrawal_transition(); + transition.unshielding_amount = 0; + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedInvalidValueBalanceError(_) + )] + ); + } + + #[test] + fn should_reject_unshielding_amount_exceeding_i64_max() { + let platform_version = PlatformVersion::latest(); + let mut transition = valid_shielded_withdrawal_transition(); + transition.unshielding_amount = i64::MAX as u64 + 1; + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedInvalidValueBalanceError(_) + )] + ); + } + + #[test] + fn should_accept_unshielding_amount_at_i64_max() { + let platform_version = PlatformVersion::latest(); + let mut transition = valid_shielded_withdrawal_transition(); + transition.unshielding_amount = i64::MAX as u64; + + let result = transition.validate_structure(platform_version); + assert!( + result.is_valid(), + "Expected valid result, got errors: {:?}", + result.errors + ); + } + + #[test] + fn should_reject_empty_proof() { + let platform_version = PlatformVersion::latest(); + let mut transition = valid_shielded_withdrawal_transition(); + transition.proof.clear(); + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedEmptyProofError(_) + )] + ); + } + + #[test] + fn should_reject_zero_anchor() { + let platform_version = PlatformVersion::latest(); + let mut transition = valid_shielded_withdrawal_transition(); + transition.anchor = [0u8; 32]; + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedZeroAnchorError(_) + )] + ); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/unshield_transition/v0/state_transition_validation.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/unshield_transition/v0/state_transition_validation.rs index 8715b82cd67..04a34b616e3 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/unshield_transition/v0/state_transition_validation.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/unshield_transition/v0/state_transition_validation.rs @@ -62,3 +62,152 @@ impl StateTransitionStructureValidation for UnshieldTransitionV0 { SimpleConsensusValidationResult::new() } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::address_funds::PlatformAddress; + use crate::consensus::ConsensusError; + use assert_matches::assert_matches; + + fn dummy_action() -> crate::shielded::SerializedAction { + crate::shielded::SerializedAction { + nullifier: [1u8; 32], + rk: [2u8; 32], + cmx: [3u8; 32], + encrypted_note: vec![4u8; 216], + cv_net: [5u8; 32], + spend_auth_sig: [6u8; 64], + } + } + + fn valid_unshield_transition() -> UnshieldTransitionV0 { + UnshieldTransitionV0 { + output_address: PlatformAddress::P2pkh([1u8; 20]), + actions: vec![dummy_action()], + unshielding_amount: 1_000_000u64, + anchor: [7u8; 32], + proof: vec![8u8; 100], + binding_signature: [9u8; 64], + } + } + + #[test] + fn should_validate_a_valid_transition() { + let platform_version = PlatformVersion::latest(); + let transition = valid_unshield_transition(); + let result = transition.validate_structure(platform_version); + assert!( + result.is_valid(), + "Expected valid result, got errors: {:?}", + result.errors + ); + } + + #[test] + fn should_reject_empty_actions() { + let platform_version = PlatformVersion::latest(); + let mut transition = valid_unshield_transition(); + transition.actions.clear(); + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedNoActionsError(_) + )] + ); + } + + #[test] + fn should_reject_too_many_actions() { + let platform_version = PlatformVersion::latest(); + let max = platform_version + .system_limits + .max_shielded_transition_actions; + let mut transition = valid_unshield_transition(); + transition.actions = vec![dummy_action(); max as usize + 1]; + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedTooManyActionsError(_) + )] + ); + } + + #[test] + fn should_reject_zero_unshielding_amount() { + let platform_version = PlatformVersion::latest(); + let mut transition = valid_unshield_transition(); + transition.unshielding_amount = 0; + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedInvalidValueBalanceError(_) + )] + ); + } + + #[test] + fn should_reject_unshielding_amount_exceeding_i64_max() { + let platform_version = PlatformVersion::latest(); + let mut transition = valid_unshield_transition(); + transition.unshielding_amount = i64::MAX as u64 + 1; + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedInvalidValueBalanceError(_) + )] + ); + } + + #[test] + fn should_accept_unshielding_amount_at_i64_max() { + let platform_version = PlatformVersion::latest(); + let mut transition = valid_unshield_transition(); + transition.unshielding_amount = i64::MAX as u64; + + let result = transition.validate_structure(platform_version); + assert!( + result.is_valid(), + "Expected valid result, got errors: {:?}", + result.errors + ); + } + + #[test] + fn should_reject_empty_proof() { + let platform_version = PlatformVersion::latest(); + let mut transition = valid_unshield_transition(); + transition.proof.clear(); + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedEmptyProofError(_) + )] + ); + } + + #[test] + fn should_reject_zero_anchor() { + let platform_version = PlatformVersion::latest(); + let mut transition = valid_unshield_transition(); + transition.anchor = [0u8; 32]; + + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedZeroAnchorError(_) + )] + ); + } +} diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/mod.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/mod.rs index ed2283c859a..ede323f1d33 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/mod.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/mod.rs @@ -12,4 +12,8 @@ pub struct DPPTokenVersions { /// v0: uses only the u8 discriminant of the config change item (vulnerable to value swap) /// v1: includes the full serialized config change item in the hash pub token_config_update_action_id_version: FeatureVersion, + /// Version for the set-price-for-direct-purchase action_id calculation. + /// v0: uses only minimum_purchase_amount_and_price().1 (vulnerable to schedule swap) + /// v1: includes the full serialized TokenPricingSchedule in the hash + pub token_set_price_action_id_version: FeatureVersion, } diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v1.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v1.rs index 4e3e8e3861a..e5114478c72 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v1.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v1.rs @@ -5,4 +5,5 @@ pub const TOKEN_VERSIONS_V1: DPPTokenVersions = DPPTokenVersions { identity_token_status_default_structure_version: 0, token_contract_info_default_structure_version: 0, token_config_update_action_id_version: 0, + token_set_price_action_id_version: 0, }; diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v2.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v2.rs index 32c2c0fdfa1..c9f0cc893e0 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v2.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v2.rs @@ -5,4 +5,5 @@ pub const TOKEN_VERSIONS_V2: DPPTokenVersions = DPPTokenVersions { identity_token_status_default_structure_version: 0, token_contract_info_default_structure_version: 0, token_config_update_action_id_version: 1, + token_set_price_action_id_version: 1, }; diff --git a/packages/rs-platform-version/src/version/v12.rs b/packages/rs-platform-version/src/version/v12.rs index 394260d2b49..fbe019d3afd 100644 --- a/packages/rs-platform-version/src/version/v12.rs +++ b/packages/rs-platform-version/src/version/v12.rs @@ -55,7 +55,7 @@ pub const PLATFORM_V12: PlatformVersion = PlatformVersion { document_versions: DOCUMENT_VERSIONS_V3, identity_versions: IDENTITY_VERSIONS_V1, voting_versions: VOTING_VERSION_V2, - token_versions: TOKEN_VERSIONS_V2, // fixes issue with token config update action id + token_versions: TOKEN_VERSIONS_V2, // fixes action_id vote-swap for config update + set price asset_lock_versions: DPP_ASSET_LOCK_VERSIONS_V1, methods: DPP_METHOD_VERSIONS_V2, factory_versions: DPP_FACTORY_VERSIONS_V1,