diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_base_transition_action/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_base_transition_action/mod.rs index 7ff40ac8bbc..c7ddeaac010 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_base_transition_action/mod.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_base_transition_action/mod.rs @@ -84,3 +84,205 @@ impl TokenBaseTransitionActionAccessorsV0 for TokenBaseTransitionAction { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::drive::contract::DataContractFetchInfo; + use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionActionV0; + use dpp::data_contract::accessors::v0::DataContractV0Getters; + use dpp::data_contract::associated_token::token_configuration::v0::TokenConfigurationV0; + use dpp::data_contract::associated_token::token_configuration::TokenConfiguration; + use dpp::data_contract::config::v0::DataContractConfigV0; + use dpp::data_contract::config::DataContractConfig; + use dpp::data_contract::group::v0::GroupV0; + use dpp::data_contract::group::Group; + use dpp::data_contract::v1::DataContractV1; + use dpp::group::action_event::GroupActionEvent; + use dpp::group::group_action::v0::GroupActionV0; + use dpp::group::group_action::GroupAction; + use dpp::group::GroupStateTransitionResolvedInfo; + use dpp::prelude::DataContract; + use dpp::tokens::token_event::TokenEvent; + use grovedb_costs::OperationCost; + use std::collections::BTreeMap; + + fn build_contract_with_token_positions(positions: &[u16]) -> DataContract { + let mut tokens = BTreeMap::new(); + for p in positions { + tokens.insert( + *p, + TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()), + ); + } + DataContract::V1(DataContractV1 { + id: Identifier::new([9u8; 32]), + version: 1, + owner_id: Identifier::new([7u8; 32]), + document_types: Default::default(), + config: DataContractConfig::V0(DataContractConfigV0 { + can_be_deleted: false, + readonly: false, + keeps_history: false, + documents_keep_history_contract_default: false, + documents_mutable_contract_default: false, + documents_can_be_deleted_contract_default: false, + requires_identity_encryption_bounded_key: None, + requires_identity_decryption_bounded_key: None, + }), + schema_defs: None, + created_at: None, + updated_at: None, + created_at_block_height: None, + updated_at_block_height: None, + created_at_epoch: None, + updated_at_epoch: None, + groups: Default::default(), + tokens, + keywords: Vec::new(), + description: None, + }) + } + + fn enum_action_with( + position: u16, + store_in_group: Option<(GroupStateTransitionResolvedInfo, Option)>, + perform_action: bool, + ) -> TokenBaseTransitionAction { + let contract = build_contract_with_token_positions(&[0, position]); + let v0 = TokenBaseTransitionActionV0 { + token_id: Identifier::new([21u8; 32]), + identity_contract_nonce: 77, + token_contract_position: position, + data_contract: Arc::new(DataContractFetchInfo { + contract, + storage_flags: None, + cost: OperationCost::default(), + fee: None, + }), + store_in_group, + perform_action, + }; + TokenBaseTransitionAction::V0(v0) + } + + #[test] + fn enum_wrapper_forwards_simple_accessors_to_v0() { + let action = enum_action_with(5, None, true); + + assert_eq!(action.token_position(), 5); + assert_eq!(action.token_id(), Identifier::new([21u8; 32])); + assert_eq!(action.identity_contract_nonce(), 77); + assert!(action.perform_action()); + assert!(action.store_in_group().is_none()); + assert!(action.original_group_action().is_none()); + + // data_contract_id() dispatches through the V0 arm to contract.id() + let expected_id = action.data_contract_fetch_info_ref().contract.id(); + assert_eq!(action.data_contract_id(), expected_id); + } + + #[test] + fn enum_wrapper_data_contract_fetch_info_variants_share_pointer() { + let action = enum_action_with(0, None, true); + let reference = action.data_contract_fetch_info_ref(); + let cloned = action.data_contract_fetch_info(); + assert!(Arc::ptr_eq(reference, &cloned)); + } + + #[test] + fn enum_wrapper_token_configuration_success_and_error() { + // Success: position 3 is present in the contract. + let ok_action = enum_action_with(3, None, true); + assert!(ok_action.token_configuration().is_ok()); + + // Construct by hand with a position NOT present in the built contract. + let contract = build_contract_with_token_positions(&[0]); + let v0 = TokenBaseTransitionActionV0 { + token_id: Identifier::new([22u8; 32]), + identity_contract_nonce: 1, + token_contract_position: 44, + data_contract: Arc::new(DataContractFetchInfo { + contract, + storage_flags: None, + cost: OperationCost::default(), + fee: None, + }), + store_in_group: None, + perform_action: true, + }; + let err_action = TokenBaseTransitionAction::V0(v0); + let err = err_action.token_configuration().expect_err("must error"); + assert!(format!("{err:?}").contains("44")); + } + + #[test] + fn enum_wrapper_store_in_group_and_original_group_action() { + let resolved = GroupStateTransitionResolvedInfo { + group_contract_position: 2, + group: { + let mut members = BTreeMap::new(); + members.insert(Identifier::new([8u8; 32]), 5u32); + Group::V0(GroupV0 { + members, + required_power: 5, + }) + }, + action_id: Identifier::new([99u8; 32]), + action_is_proposer: false, + signer_power: 5, + }; + let original = GroupAction::V0(GroupActionV0 { + contract_id: Identifier::new([1u8; 32]), + proposer_id: Identifier::new([2u8; 32]), + token_contract_position: 0, + event: GroupActionEvent::TokenEvent(TokenEvent::Mint( + 7, + Identifier::new([3u8; 32]), + None, + )), + }); + + let action = enum_action_with(0, Some((resolved.clone(), Some(original.clone()))), false); + let store = action + .store_in_group() + .expect("wrapper must forward Some(resolved)"); + assert_eq!(store.group_contract_position, 2); + assert_eq!(store.action_id, Identifier::new([99u8; 32])); + + let orig = action + .original_group_action() + .expect("wrapper must forward Some(GroupAction)"); + assert_eq!(orig, &original); + assert!(!action.perform_action()); + } + + #[test] + fn enum_from_v0_conversion_preserves_fields() { + // The derive_more::From impl should yield the same enum as manual wrapping. + let contract = build_contract_with_token_positions(&[0]); + let v0 = TokenBaseTransitionActionV0 { + token_id: Identifier::new([30u8; 32]), + identity_contract_nonce: 9, + token_contract_position: 0, + data_contract: Arc::new(DataContractFetchInfo { + contract, + storage_flags: None, + cost: OperationCost::default(), + fee: None, + }), + store_in_group: None, + perform_action: true, + }; + + let wrapped: TokenBaseTransitionAction = v0.clone().into(); + match &wrapped { + TokenBaseTransitionAction::V0(inner) => { + assert_eq!(inner.token_id, v0.token_id); + assert_eq!(inner.identity_contract_nonce, v0.identity_contract_nonce); + assert_eq!(inner.token_contract_position, v0.token_contract_position); + assert!(Arc::ptr_eq(&inner.data_contract, &v0.data_contract)); + } + } + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_base_transition_action/v0/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_base_transition_action/v0/mod.rs index fde27c6ae1b..64fdfb2956e 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_base_transition_action/v0/mod.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_base_transition_action/v0/mod.rs @@ -123,3 +123,227 @@ impl TokenBaseTransitionActionAccessorsV0 for TokenBaseTransitionActionV0 { self.perform_action } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::drive::contract::DataContractFetchInfo; + use dpp::data_contract::associated_token::token_configuration::v0::TokenConfigurationV0; + use dpp::data_contract::associated_token::token_configuration::TokenConfiguration; + use dpp::data_contract::config::v0::DataContractConfigV0; + use dpp::data_contract::config::DataContractConfig; + use dpp::data_contract::group::v0::GroupV0; + use dpp::data_contract::group::Group; + use dpp::data_contract::v1::DataContractV1; + use dpp::group::action_event::GroupActionEvent; + use dpp::group::group_action::v0::GroupActionV0; + use dpp::group::group_action::GroupAction; + use dpp::group::GroupStateTransitionResolvedInfo; + use dpp::prelude::DataContract; + use dpp::tokens::token_event::TokenEvent; + use grovedb_costs::OperationCost; + use std::collections::BTreeMap; + + /// Build a DataContract with one token at position 0 (and optionally more positions). + fn build_contract_with_token_positions(positions: &[u16]) -> DataContract { + let mut tokens = BTreeMap::new(); + for p in positions { + tokens.insert( + *p, + TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()), + ); + } + DataContract::V1(DataContractV1 { + id: Identifier::new([9u8; 32]), + version: 1, + owner_id: Identifier::new([7u8; 32]), + document_types: Default::default(), + config: DataContractConfig::V0(DataContractConfigV0 { + can_be_deleted: false, + readonly: false, + keeps_history: false, + documents_keep_history_contract_default: false, + documents_mutable_contract_default: false, + documents_can_be_deleted_contract_default: false, + requires_identity_encryption_bounded_key: None, + requires_identity_decryption_bounded_key: None, + }), + schema_defs: None, + created_at: None, + updated_at: None, + created_at_block_height: None, + updated_at_block_height: None, + created_at_epoch: None, + updated_at_epoch: None, + groups: Default::default(), + tokens, + keywords: Vec::new(), + description: None, + }) + } + + fn fetch_info(contract: DataContract) -> Arc { + Arc::new(DataContractFetchInfo { + contract, + storage_flags: None, + cost: OperationCost::default(), + fee: None, + }) + } + + fn sample_group(signer_id: Identifier) -> Group { + let mut members = BTreeMap::new(); + members.insert(signer_id, 3u32); + Group::V0(GroupV0 { + members, + required_power: 4, + }) + } + + fn sample_group_action() -> GroupAction { + GroupAction::V0(GroupActionV0 { + contract_id: Identifier::new([2u8; 32]), + proposer_id: Identifier::new([3u8; 32]), + token_contract_position: 0, + event: GroupActionEvent::TokenEvent(TokenEvent::Mint( + 100, + Identifier::new([4u8; 32]), + Some("original note".to_string()), + )), + }) + } + + fn action_with( + contract: DataContract, + position: u16, + store_in_group: Option<(GroupStateTransitionResolvedInfo, OriginalGroupAction)>, + perform_action: bool, + ) -> TokenBaseTransitionActionV0 { + TokenBaseTransitionActionV0 { + token_id: Identifier::new([11u8; 32]), + identity_contract_nonce: 42, + token_contract_position: position, + data_contract: fetch_info(contract), + store_in_group, + perform_action, + } + } + + #[test] + fn accessors_return_struct_fields() { + let contract = build_contract_with_token_positions(&[0]); + let expected_contract_id = contract.id(); + let action = action_with(contract, 0, None, true); + + assert_eq!(action.token_position(), 0); + assert_eq!(action.token_id(), Identifier::new([11u8; 32])); + // data_contract_id() delegates to the contract, NOT the token_id or any struct field + assert_eq!(action.data_contract_id(), expected_contract_id); + assert_eq!(action.identity_contract_nonce(), 42); + assert!(action.perform_action()); + assert!(action.store_in_group().is_none()); + assert!(action.original_group_action().is_none()); + } + + #[test] + fn data_contract_fetch_info_ref_and_clone_share_pointer() { + let contract = build_contract_with_token_positions(&[0]); + let action = action_with(contract, 0, None, true); + + let reference = action.data_contract_fetch_info_ref(); + let clone = action.data_contract_fetch_info(); + + // Both Arc values point to the same allocation as the struct field + assert!(Arc::ptr_eq(reference, &clone)); + assert!(Arc::ptr_eq(reference, &action.data_contract)); + // Ensure clone bumped the strong count (original + reference + clone) + assert!(Arc::strong_count(&clone) >= 2); + } + + #[test] + fn token_configuration_returns_config_when_position_present() { + let contract = build_contract_with_token_positions(&[0, 5]); + let action = action_with(contract, 5, None, true); + + let config = action + .token_configuration() + .expect("token_configuration should succeed for a present position"); + // Sanity: returned reference must be one of our constructed entries. + assert!(matches!(config, TokenConfiguration::V0(_))); + } + + #[test] + fn token_configuration_returns_corrupted_code_execution_for_missing_position() { + let contract = build_contract_with_token_positions(&[0]); + // Build an action pointing at a position the contract does NOT expose. + let action = action_with(contract, 99, None, true); + + let err = action + .token_configuration() + .expect_err("missing token position must produce an error"); + let msg = format!("{err:?}"); + assert!( + msg.contains("CorruptedCodeExecution"), + "expected CorruptedCodeExecution variant, got: {msg}" + ); + assert!( + msg.contains("99"), + "error message should mention the missing position, got: {msg}" + ); + } + + #[test] + fn store_in_group_returns_resolved_info_without_original_action() { + let owner_id = Identifier::new([8u8; 32]); + let resolved = GroupStateTransitionResolvedInfo { + group_contract_position: 1, + group: sample_group(owner_id), + action_id: Identifier::new([5u8; 32]), + action_is_proposer: true, + signer_power: 3, + }; + let action = action_with( + build_contract_with_token_positions(&[0]), + 0, + Some((resolved.clone(), None)), + true, + ); + + let got = action + .store_in_group() + .expect("store_in_group should return Some when set"); + assert_eq!(got.group_contract_position, 1); + assert_eq!(got.action_id, Identifier::new([5u8; 32])); + assert!(got.action_is_proposer); + assert_eq!(got.signer_power, 3); + // No original group action in this variant + assert!(action.original_group_action().is_none()); + } + + #[test] + fn original_group_action_returns_inner_group_action_when_present() { + let owner_id = Identifier::new([8u8; 32]); + let resolved = GroupStateTransitionResolvedInfo { + group_contract_position: 0, + group: sample_group(owner_id), + action_id: Identifier::new([6u8; 32]), + action_is_proposer: false, + signer_power: 3, + }; + let group_action = sample_group_action(); + let action = action_with( + build_contract_with_token_positions(&[0]), + 0, + Some((resolved, Some(group_action.clone()))), + false, + ); + + let returned = action + .original_group_action() + .expect("original_group_action must propagate inner Some"); + // Same variant / same data + assert_eq!(returned, &group_action); + // perform_action was set to false in this fixture — preserve it + assert!(!action.perform_action()); + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_burn_transition_action/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_burn_transition_action/mod.rs index 7d75c5ab511..223e1d99cd0 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_burn_transition_action/mod.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_burn_transition_action/mod.rs @@ -71,3 +71,189 @@ impl TokenBurnTransitionActionAccessorsV0 for TokenBurnTransitionAction { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::drive::contract::DataContractFetchInfo; + use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::{ + TokenBaseTransitionAction, TokenBaseTransitionActionAccessorsV0, + TokenBaseTransitionActionV0, + }; + use dpp::data_contract::accessors::v0::DataContractV0Getters; + use dpp::tests::fixtures::get_data_contract_fixture; + use dpp::version::PlatformVersion; + use std::sync::Arc; + + const TEST_OWNER_ID: [u8; 32] = [0xAA; 32]; + const TEST_TOKEN_ID: [u8; 32] = [0xCC; 32]; + const TEST_BURN_FROM_ID: [u8; 32] = [0xDD; 32]; + const TEST_NONCE: u64 = 9; + const TEST_TOKEN_POSITION: u16 = 0; + const TEST_BURN_AMOUNT: u64 = 12_345; + + fn make_base_action() -> TokenBaseTransitionAction { + let platform_version = PlatformVersion::latest(); + let contract = get_data_contract_fixture( + Some(Identifier::from(TEST_OWNER_ID)), + 0, + platform_version.protocol_version, + ) + .data_contract_owned(); + let contract_id = contract.id(); + let fetch_info = Arc::new(DataContractFetchInfo { + contract, + storage_flags: None, + cost: Default::default(), + fee: None, + }); + let v0 = TokenBaseTransitionActionV0 { + token_id: Identifier::from(TEST_TOKEN_ID), + identity_contract_nonce: TEST_NONCE, + token_contract_position: TEST_TOKEN_POSITION, + data_contract: fetch_info, + store_in_group: None, + perform_action: true, + }; + let _ = contract_id; // keep for potential future assertions + TokenBaseTransitionAction::V0(v0) + } + + fn make_v0() -> TokenBurnTransitionActionV0 { + TokenBurnTransitionActionV0 { + base: make_base_action(), + burn_from_identifier: Identifier::from(TEST_BURN_FROM_ID), + burn_amount: TEST_BURN_AMOUNT, + public_note: Some("initial note".to_string()), + } + } + + #[test] + fn from_v0_wraps_in_enum() { + let v0 = make_v0(); + let action: TokenBurnTransitionAction = v0.into(); + assert!(matches!(action, TokenBurnTransitionAction::V0(_))); + } + + #[test] + fn enum_base_returns_v0_base() { + let action = TokenBurnTransitionAction::V0(make_v0()); + let base = action.base(); + assert_eq!(base.token_id(), Identifier::from(TEST_TOKEN_ID)); + assert_eq!(base.identity_contract_nonce(), TEST_NONCE); + assert_eq!(base.token_position(), TEST_TOKEN_POSITION); + } + + #[test] + fn enum_base_owned_consumes_and_returns_base() { + let action = TokenBurnTransitionAction::V0(make_v0()); + let base = action.base_owned(); + assert_eq!(base.token_id(), Identifier::from(TEST_TOKEN_ID)); + } + + #[test] + fn enum_burn_from_identifier_returns_v0_value() { + let action = TokenBurnTransitionAction::V0(make_v0()); + assert_eq!( + action.burn_from_identifier(), + Identifier::from(TEST_BURN_FROM_ID) + ); + } + + #[test] + fn enum_set_burn_from_identifier_updates_v0() { + let mut action = TokenBurnTransitionAction::V0(make_v0()); + let new_id = Identifier::from([0xFE; 32]); + action.set_burn_from_identifier(new_id); + assert_eq!(action.burn_from_identifier(), new_id); + } + + #[test] + fn enum_burn_amount_returns_v0_amount() { + let action = TokenBurnTransitionAction::V0(make_v0()); + assert_eq!(action.burn_amount(), TEST_BURN_AMOUNT); + } + + #[test] + fn enum_set_burn_amount_updates_v0() { + let mut action = TokenBurnTransitionAction::V0(make_v0()); + action.set_burn_amount(99); + assert_eq!(action.burn_amount(), 99); + // Also round-trip with zero + action.set_burn_amount(0); + assert_eq!(action.burn_amount(), 0); + } + + #[test] + fn enum_public_note_returns_ref() { + let action = TokenBurnTransitionAction::V0(make_v0()); + assert_eq!(action.public_note(), Some(&"initial note".to_string())); + } + + #[test] + fn enum_public_note_when_none() { + let mut v0 = make_v0(); + v0.public_note = None; + let action = TokenBurnTransitionAction::V0(v0); + assert_eq!(action.public_note(), None); + } + + #[test] + fn enum_public_note_owned_consumes_and_returns_string() { + let action = TokenBurnTransitionAction::V0(make_v0()); + assert_eq!(action.public_note_owned(), Some("initial note".to_string())); + } + + #[test] + fn enum_set_public_note_replaces_value() { + let mut action = TokenBurnTransitionAction::V0(make_v0()); + action.set_public_note(Some("updated".to_string())); + assert_eq!(action.public_note(), Some(&"updated".to_string())); + action.set_public_note(None); + assert_eq!(action.public_note(), None); + } + + // ---- V0 struct accessors ---- + + #[test] + fn v0_accessors_roundtrip() { + let mut v0 = make_v0(); + assert_eq!(v0.burn_amount(), TEST_BURN_AMOUNT); + assert_eq!( + v0.burn_from_identifier(), + Identifier::from(TEST_BURN_FROM_ID) + ); + assert_eq!(v0.public_note(), Some(&"initial note".to_string())); + + v0.set_burn_amount(7); + assert_eq!(v0.burn_amount(), 7); + + let new_id = Identifier::from([0x11; 32]); + v0.set_burn_from_identifier(new_id); + assert_eq!(v0.burn_from_identifier(), new_id); + + v0.set_public_note(Some("new".to_string())); + assert_eq!(v0.public_note(), Some(&"new".to_string())); + + // public_note_owned moves self + let owned = v0.clone().public_note_owned(); + assert_eq!(owned, Some("new".to_string())); + + // base_owned moves self + let base = v0.base_owned(); + assert_eq!(base.token_id(), Identifier::from(TEST_TOKEN_ID)); + } + + #[test] + fn v0_base_ref_and_delegations() { + let v0 = make_v0(); + // Accessor methods from the trait that delegate to base() + assert_eq!(v0.token_position(), TEST_TOKEN_POSITION); + assert_eq!(v0.token_id(), Identifier::from(TEST_TOKEN_ID)); + assert_eq!(v0.identity_contract_nonce(), TEST_NONCE); + let fetched_arc = v0.data_contract_fetch_info(); + let fetched_ref = v0.data_contract_fetch_info_ref(); + assert!(Arc::ptr_eq(&fetched_arc, fetched_ref)); + assert_eq!(v0.data_contract_id(), fetched_ref.contract.id()); + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs index 205a2f6be72..81347a32877 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs @@ -579,3 +579,446 @@ impl TokenClaimTransitionActionV0 { )) } } + +#[cfg(test)] +mod tests { + //! Unit tests for the logic fragments of `try_from_borrowed_token_claim_transition_with_contract_lookup` + //! that can be exercised in isolation (without wiring up a full `Drive` instance). + //! + //! These cover: + //! * the pre-programmed distribution filtering + "distribution after last paid" lookup + //! * the `wrong_claimant_error` resolution logic + //! * `RewardRatio` computation used inside the `EvonodesByParticipation` closure + //! * recipient resolution for each `TokenDistributionInfo` variant + //! * the `From for TokenDistributionRecipient` roundtrip + //! * `ClaimAction` variant dispatch / clone / enum wrapper accessors + use dpp::balances::credits::TokenAmount; + use dpp::block::epoch::EpochIndex; + use dpp::data_contract::associated_token::token_distribution_key::TokenDistributionInfo; + use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::reward_ratio::RewardRatio; + use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_recipient::{ + TokenDistributionRecipient, TokenDistributionResolvedRecipient, + }; + use dpp::data_contract::associated_token::token_perpetual_distribution::reward_distribution_moment::RewardDistributionMoment; + use dpp::identifier::Identifier; + use dpp::prelude::TimestampMillis; + use std::collections::BTreeMap; + + /// Replica of the pre-programmed distribution filter used inside the + /// transformer: exclude future timestamps and keep only entries that + /// actually target `owner_id`. + fn filter_past_for_owner( + times: &BTreeMap>, + owner_id: Identifier, + now_ms: TimestampMillis, + ) -> BTreeMap { + times + .iter() + .filter_map(|(timestamp, distribution)| { + if *timestamp > now_ms { + None + } else { + distribution + .get(&owner_id) + .map(|amount| (*timestamp, *amount)) + } + }) + .collect() + } + + fn owner() -> Identifier { + Identifier::from([0xAB; 32]) + } + + fn stranger() -> Identifier { + Identifier::from([0xCD; 32]) + } + + #[test] + fn pre_programmed_filter_excludes_future_timestamps() { + let me = owner(); + let mut past = BTreeMap::new(); + past.insert(me, 100u64); + let mut future = BTreeMap::new(); + future.insert(me, 999u64); + + let mut times: BTreeMap> = + BTreeMap::new(); + times.insert(500, past); + times.insert(2_000, future); + + let now = 1_000u64; + let result = filter_past_for_owner(×, me, now); + assert_eq!(result.len(), 1); + assert_eq!(result.get(&500), Some(&100)); + } + + #[test] + fn pre_programmed_filter_ignores_non_owner_entries() { + let me = owner(); + let other = stranger(); + let mut only_other = BTreeMap::new(); + only_other.insert(other, 42u64); + let mut has_me = BTreeMap::new(); + has_me.insert(other, 7u64); + has_me.insert(me, 13u64); + + let mut times: BTreeMap> = + BTreeMap::new(); + times.insert(100, only_other); + times.insert(200, has_me); + + let result = filter_past_for_owner(×, me, 1_000); + assert_eq!(result.len(), 1); + assert_eq!(result.get(&200), Some(&13)); + } + + #[test] + fn pre_programmed_filter_keeps_entries_exactly_at_now() { + // The filter uses strict `> now`, so entries with `timestamp == now` must be kept. + let me = owner(); + let mut dist = BTreeMap::new(); + dist.insert(me, 11u64); + + let mut times: BTreeMap> = + BTreeMap::new(); + times.insert(1_000, dist); + + let result = filter_past_for_owner(×, me, 1_000); + assert_eq!(result.get(&1_000), Some(&11)); + } + + #[test] + fn distribution_after_last_paid_uses_first_when_never_paid() { + // Mirrors `if let Some(last_paid) ... else { first_key_value() }` branch. + let mut distributions: BTreeMap = BTreeMap::new(); + distributions.insert(100, 5); + distributions.insert(200, 7); + distributions.insert(300, 9); + + let last_paid: Option = None; + let picked = if let Some(last) = last_paid { + distributions + .range((last + 1)..) + .next() + .map(|(ts, amount)| (*ts, *amount)) + } else { + distributions + .first_key_value() + .map(|(ts, amount)| (*ts, *amount)) + }; + assert_eq!(picked, Some((100, 5))); + } + + #[test] + fn distribution_after_last_paid_returns_next_entry_strictly_after() { + let mut distributions: BTreeMap = BTreeMap::new(); + distributions.insert(100, 5); + distributions.insert(200, 7); + distributions.insert(300, 9); + + // last_paid == 100 => earliest AFTER it is 200. + let last_paid: Option = Some(100); + let picked = distributions + .range((last_paid.unwrap() + 1)..) + .next() + .map(|(ts, amount)| (*ts, *amount)); + assert_eq!(picked, Some((200, 7))); + } + + #[test] + fn distribution_after_last_paid_returns_none_when_all_claimed() { + // When last_paid is at/above every available timestamp, the lookup returns None, + // which in the transformer yields `InvalidTokenClaimNoCurrentRewards`. + let mut distributions: BTreeMap = BTreeMap::new(); + distributions.insert(100, 5); + distributions.insert(200, 7); + + let last_paid = Some(200u64); + let picked = distributions + .range((last_paid.unwrap() + 1)..) + .next() + .map(|(ts, amount)| (*ts, *amount)); + assert!(picked.is_none()); + } + + #[test] + fn wrong_claimant_error_contract_owner_mismatch() { + // Replicates the wrong_claimant_error match in the Perpetual branch: + // ContractOwner recipient + owner_id != contract.owner_id => Some(contract.owner_id) + let contract_owner = stranger(); + let caller = owner(); + let recipient = TokenDistributionRecipient::ContractOwner; + let wrong_claimant_error = match recipient { + TokenDistributionRecipient::ContractOwner if contract_owner != caller => { + Some(contract_owner) + } + TokenDistributionRecipient::Identity(identifier) if identifier != caller => { + Some(identifier) + } + _ => None, + }; + assert_eq!(wrong_claimant_error, Some(contract_owner)); + } + + #[test] + fn wrong_claimant_error_contract_owner_match_returns_none() { + let caller = owner(); + let recipient = TokenDistributionRecipient::ContractOwner; + let wrong = match recipient { + TokenDistributionRecipient::ContractOwner if caller != caller => Some(caller), + TokenDistributionRecipient::Identity(identifier) if identifier != caller => { + Some(identifier) + } + _ => None, + }; + assert!(wrong.is_none()); + } + + #[test] + fn wrong_claimant_error_identity_mismatch() { + let caller = owner(); + let expected = stranger(); + let recipient = TokenDistributionRecipient::Identity(expected); + let wrong = match recipient { + TokenDistributionRecipient::ContractOwner => None, + TokenDistributionRecipient::Identity(identifier) if identifier != caller => { + Some(identifier) + } + _ => None, + }; + assert_eq!(wrong, Some(expected)); + } + + #[test] + fn wrong_claimant_error_identity_match_returns_none() { + let caller = owner(); + let recipient = TokenDistributionRecipient::Identity(caller); + let wrong = match recipient { + TokenDistributionRecipient::ContractOwner => None, + TokenDistributionRecipient::Identity(identifier) if identifier != caller => { + Some(identifier) + } + _ => None, + }; + assert!(wrong.is_none()); + } + + #[test] + fn wrong_claimant_error_evonodes_by_participation_is_always_none() { + // EvonodesByParticipation isn't treated as a wrong-claimant error: it falls through + // to the `_ => None` arm in the match. + let caller = owner(); + let recipient = TokenDistributionRecipient::EvonodesByParticipation; + let wrong = match recipient { + TokenDistributionRecipient::ContractOwner if caller != caller => Some(caller), + TokenDistributionRecipient::Identity(identifier) if identifier != caller => { + Some(identifier) + } + _ => None, + }; + assert!(wrong.is_none()); + } + + #[test] + fn distribution_info_pre_programmed_recipient_resolution() { + // Mirrors `TokenClaimTransitionActionV0::recipient` for the PreProgrammed variant. + let id = Identifier::from([0xEE; 32]); + let info = TokenDistributionInfo::PreProgrammed(1_000, id); + let recipient = match &info { + TokenDistributionInfo::PreProgrammed(_, identifier) => { + TokenDistributionRecipient::Identity(*identifier) + } + TokenDistributionInfo::Perpetual(_, resolved_recipient) => resolved_recipient.into(), + }; + assert_eq!(recipient, TokenDistributionRecipient::Identity(id)); + } + + #[test] + fn distribution_info_perpetual_contract_owner_recipient() { + let id = Identifier::from([0x1A; 32]); + let info = TokenDistributionInfo::Perpetual( + RewardDistributionMoment::TimeBasedMoment(42), + TokenDistributionResolvedRecipient::ContractOwnerIdentity(id), + ); + let recipient = match &info { + TokenDistributionInfo::PreProgrammed(_, identifier) => { + TokenDistributionRecipient::Identity(*identifier) + } + TokenDistributionInfo::Perpetual(_, resolved_recipient) => resolved_recipient.into(), + }; + assert_eq!(recipient, TokenDistributionRecipient::ContractOwner); + } + + #[test] + fn distribution_info_perpetual_identity_recipient() { + let id = Identifier::from([0x2B; 32]); + let info = TokenDistributionInfo::Perpetual( + RewardDistributionMoment::BlockBasedMoment(7), + TokenDistributionResolvedRecipient::Identity(id), + ); + let recipient = match &info { + TokenDistributionInfo::PreProgrammed(_, identifier) => { + TokenDistributionRecipient::Identity(*identifier) + } + TokenDistributionInfo::Perpetual(_, resolved_recipient) => resolved_recipient.into(), + }; + assert_eq!(recipient, TokenDistributionRecipient::Identity(id)); + } + + #[test] + fn distribution_info_perpetual_evonode_recipient() { + let id = Identifier::from([0x3C; 32]); + let info = TokenDistributionInfo::Perpetual( + RewardDistributionMoment::EpochBasedMoment(9), + TokenDistributionResolvedRecipient::Evonode(id), + ); + let recipient = match &info { + TokenDistributionInfo::PreProgrammed(_, identifier) => { + TokenDistributionRecipient::Identity(*identifier) + } + TokenDistributionInfo::Perpetual(_, resolved_recipient) => resolved_recipient.into(), + }; + assert_eq!( + recipient, + TokenDistributionRecipient::EvonodesByParticipation + ); + } + + /// Reimplements the closure passed to `rewards_in_interval` inside the + /// `EvonodesByParticipation` branch and checks its single-epoch output + /// matches the blocks proposed by the owner. + #[test] + fn evonodes_reward_ratio_single_epoch() { + let me = owner(); + let other = stranger(); + let mut block_proposers: BTreeMap = BTreeMap::new(); + block_proposers.insert(me, 7); + block_proposers.insert(other, 3); + let total_blocks_in_epoch = 10u64; + + let ratio = RewardRatio { + numerator: block_proposers.get(&me).copied().unwrap_or_default(), + denominator: total_blocks_in_epoch, + }; + assert_eq!(ratio.numerator, 7); + assert_eq!(ratio.denominator, 10); + } + + /// Multi-epoch branch should sum totals and only count blocks proposed by the owner. + #[test] + fn evonodes_reward_ratio_multi_epoch_sum() { + let me = owner(); + let mut epochs: BTreeMap)> = BTreeMap::new(); + + let mut bp1 = BTreeMap::new(); + bp1.insert(me, 5); + epochs.insert(0, (20, bp1)); + + let mut bp2 = BTreeMap::new(); + bp2.insert(me, 2); + epochs.insert(1, (30, bp2)); + + let mut total_blocks = 0u64; + let mut total_proposed_blocks = 0u64; + for idx in 0u16..=1u16 { + if let Some((blocks, proposers)) = epochs.get(&idx) { + total_blocks += *blocks; + total_proposed_blocks += proposers.get(&me).copied().unwrap_or_default(); + } + } + assert_eq!(total_blocks, 50); + assert_eq!(total_proposed_blocks, 7); + let ratio = if total_blocks > 0 { + Some(RewardRatio { + numerator: total_proposed_blocks, + denominator: total_blocks, + }) + } else { + None + }; + assert_eq!( + ratio, + Some(RewardRatio { + numerator: 7, + denominator: 50, + }) + ); + } + + /// With no epochs in range, total_blocks remains 0 so the ratio is None. + #[test] + fn evonodes_reward_ratio_empty_range_returns_none() { + let total_blocks = 0u64; + let total_proposed_blocks = 0u64; + let ratio = if total_blocks > 0 { + Some(RewardRatio { + numerator: total_proposed_blocks, + denominator: total_blocks, + }) + } else { + None + }; + assert!(ratio.is_none()); + } + + /// When an epoch index exists but the owner never proposed, numerator is 0. + #[test] + fn evonodes_reward_ratio_owner_has_no_proposed_blocks() { + let me = owner(); + let other = stranger(); + let mut bp: BTreeMap = BTreeMap::new(); + bp.insert(other, 4); + let total_blocks = 10u64; + + let ratio = RewardRatio { + numerator: bp.get(&me).copied().unwrap_or_default(), + denominator: total_blocks, + }; + assert_eq!(ratio.numerator, 0); + assert_eq!(ratio.denominator, 10); + } + + /// The `last_paid_moment.unwrap_or(contract_creation_cycle_start)` fallback + /// should pick the explicit last-paid if set, otherwise the cycle start. + #[test] + fn start_from_moment_fallback_uses_cycle_start_when_never_paid() { + let cycle_start = RewardDistributionMoment::TimeBasedMoment(500); + let last_paid: Option = None; + let start = last_paid.unwrap_or(cycle_start); + assert_eq!(start, RewardDistributionMoment::TimeBasedMoment(500)); + } + + #[test] + fn start_from_moment_fallback_uses_last_paid_when_present() { + let cycle_start = RewardDistributionMoment::TimeBasedMoment(500); + let last_paid = Some(RewardDistributionMoment::TimeBasedMoment(1_000)); + let start = last_paid.unwrap_or(cycle_start); + assert_eq!(start, RewardDistributionMoment::TimeBasedMoment(1_000)); + } + + /// The `From` impl converting `TokenDistributionResolvedRecipient` back to + /// `TokenDistributionRecipient` is used inside the `recipient()` accessor. + #[test] + fn resolved_recipient_into_recipient_contract_owner_identity() { + let r = + TokenDistributionResolvedRecipient::ContractOwnerIdentity(Identifier::from([1u8; 32])); + let into: TokenDistributionRecipient = (&r).into(); + assert_eq!(into, TokenDistributionRecipient::ContractOwner); + } + + #[test] + fn resolved_recipient_into_recipient_evonode() { + let r = TokenDistributionResolvedRecipient::Evonode(Identifier::from([2u8; 32])); + let into: TokenDistributionRecipient = (&r).into(); + assert_eq!(into, TokenDistributionRecipient::EvonodesByParticipation); + } + + #[test] + fn resolved_recipient_into_recipient_identity_preserves_identifier() { + let id = Identifier::from([3u8; 32]); + let r = TokenDistributionResolvedRecipient::Identity(id); + let into: TokenDistributionRecipient = (&r).into(); + assert_eq!(into, TokenDistributionRecipient::Identity(id)); + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_config_update_transition_action/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_config_update_transition_action/mod.rs index b40ecc23105..e8a3d48423f 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_config_update_transition_action/mod.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_config_update_transition_action/mod.rs @@ -64,3 +64,205 @@ impl TokenConfigUpdateTransitionActionAccessorsV0 for TokenConfigUpdateTransitio } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::drive::contract::DataContractFetchInfo; + use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::{ + TokenBaseTransitionAction, TokenBaseTransitionActionAccessorsV0, + TokenBaseTransitionActionV0, + }; + use dpp::data_contract::accessors::v0::DataContractV0Getters; + use dpp::identifier::Identifier; + use dpp::tests::fixtures::get_data_contract_fixture; + use dpp::version::PlatformVersion; + use std::sync::Arc; + + const TEST_OWNER_ID: [u8; 32] = [0xAA; 32]; + const TEST_TOKEN_ID: [u8; 32] = [0xCC; 32]; + const TEST_NONCE: u64 = 5; + const TEST_TOKEN_POSITION: u16 = 0; + + fn make_base_action() -> TokenBaseTransitionAction { + let platform_version = PlatformVersion::latest(); + let contract = get_data_contract_fixture( + Some(Identifier::from(TEST_OWNER_ID)), + 0, + platform_version.protocol_version, + ) + .data_contract_owned(); + let fetch_info = Arc::new(DataContractFetchInfo { + contract, + storage_flags: None, + cost: Default::default(), + fee: None, + }); + TokenBaseTransitionAction::V0(TokenBaseTransitionActionV0 { + token_id: Identifier::from(TEST_TOKEN_ID), + identity_contract_nonce: TEST_NONCE, + token_contract_position: TEST_TOKEN_POSITION, + data_contract: fetch_info, + store_in_group: None, + perform_action: true, + }) + } + + fn make_v0_with_item( + item: TokenConfigurationChangeItem, + ) -> TokenConfigUpdateTransitionActionV0 { + TokenConfigUpdateTransitionActionV0 { + base: make_base_action(), + update_token_configuration_item: item, + public_note: Some("cfg-update".to_string()), + } + } + + fn make_v0() -> TokenConfigUpdateTransitionActionV0 { + make_v0_with_item(TokenConfigurationChangeItem::MaxSupply(Some(1_000))) + } + + #[test] + fn from_v0_wraps_in_enum() { + let v0 = make_v0(); + let action: TokenConfigUpdateTransitionAction = v0.into(); + assert!(matches!(action, TokenConfigUpdateTransitionAction::V0(_))); + } + + #[test] + fn enum_base_returns_v0_base() { + let action = TokenConfigUpdateTransitionAction::V0(make_v0()); + let base = action.base(); + assert_eq!(base.token_id(), Identifier::from(TEST_TOKEN_ID)); + assert_eq!(base.identity_contract_nonce(), TEST_NONCE); + assert_eq!(base.token_position(), TEST_TOKEN_POSITION); + } + + #[test] + fn enum_base_owned_consumes() { + let action = TokenConfigUpdateTransitionAction::V0(make_v0()); + let base = action.base_owned(); + assert_eq!(base.token_id(), Identifier::from(TEST_TOKEN_ID)); + } + + #[test] + fn enum_update_token_configuration_item_returns_ref() { + let action = TokenConfigUpdateTransitionAction::V0(make_v0()); + match action.update_token_configuration_item() { + TokenConfigurationChangeItem::MaxSupply(Some(v)) => assert_eq!(*v, 1_000), + other => panic!("unexpected item variant: {:?}", other), + } + } + + #[test] + fn enum_set_update_token_configuration_item_replaces_value() { + let mut action = TokenConfigUpdateTransitionAction::V0(make_v0()); + action.set_update_token_configuration_item( + TokenConfigurationChangeItem::TokenConfigurationNoChange, + ); + assert!(matches!( + action.update_token_configuration_item(), + TokenConfigurationChangeItem::TokenConfigurationNoChange + )); + // Replace again with a different variant to exercise overwrite. + action.set_update_token_configuration_item(TokenConfigurationChangeItem::MaxSupply(None)); + assert!(matches!( + action.update_token_configuration_item(), + TokenConfigurationChangeItem::MaxSupply(None) + )); + } + + #[test] + fn enum_public_note_returns_ref() { + let action = TokenConfigUpdateTransitionAction::V0(make_v0()); + assert_eq!(action.public_note(), Some(&"cfg-update".to_string())); + } + + #[test] + fn enum_public_note_returns_none_when_unset() { + let mut v0 = make_v0(); + v0.public_note = None; + let action = TokenConfigUpdateTransitionAction::V0(v0); + assert_eq!(action.public_note(), None); + } + + #[test] + fn enum_public_note_owned_consumes() { + let action = TokenConfigUpdateTransitionAction::V0(make_v0()); + assert_eq!(action.public_note_owned(), Some("cfg-update".to_string())); + } + + #[test] + fn enum_set_public_note_replaces_value() { + let mut action = TokenConfigUpdateTransitionAction::V0(make_v0()); + action.set_public_note(Some("newer".to_string())); + assert_eq!(action.public_note(), Some(&"newer".to_string())); + action.set_public_note(None); + assert_eq!(action.public_note(), None); + } + + #[test] + fn v0_accessors_roundtrip() { + let mut v0 = make_v0(); + assert!(matches!( + v0.update_token_configuration_item(), + TokenConfigurationChangeItem::MaxSupply(Some(1_000)) + )); + assert_eq!(v0.public_note(), Some(&"cfg-update".to_string())); + + v0.set_update_token_configuration_item( + TokenConfigurationChangeItem::TokenConfigurationNoChange, + ); + assert!(matches!( + v0.update_token_configuration_item(), + TokenConfigurationChangeItem::TokenConfigurationNoChange + )); + + v0.set_public_note(None); + assert_eq!(v0.public_note(), None); + v0.set_public_note(Some("set-again".to_string())); + assert_eq!(v0.public_note(), Some(&"set-again".to_string())); + + let owned = v0.clone().public_note_owned(); + assert_eq!(owned, Some("set-again".to_string())); + + let base = v0.base_owned(); + assert_eq!(base.token_id(), Identifier::from(TEST_TOKEN_ID)); + } + + #[test] + fn v0_base_delegations() { + let v0 = make_v0(); + assert_eq!(v0.token_position(), TEST_TOKEN_POSITION); + assert_eq!(v0.token_id(), Identifier::from(TEST_TOKEN_ID)); + let fetched_arc = v0.data_contract_fetch_info(); + let fetched_ref = v0.data_contract_fetch_info_ref(); + assert!(Arc::ptr_eq(&fetched_arc, fetched_ref)); + assert_eq!(v0.data_contract_id(), fetched_ref.contract.id()); + } + + #[test] + fn v0_stores_various_change_item_variants() { + // Exercise a few TokenConfigurationChangeItem variants to ensure the + // field is held and retrievable by reference for several kinds of + // payloads the transformer might receive. + let v_nochange = + make_v0_with_item(TokenConfigurationChangeItem::TokenConfigurationNoChange); + assert!(matches!( + v_nochange.update_token_configuration_item(), + TokenConfigurationChangeItem::TokenConfigurationNoChange + )); + + let v_max_none = make_v0_with_item(TokenConfigurationChangeItem::MaxSupply(None)); + assert!(matches!( + v_max_none.update_token_configuration_item(), + TokenConfigurationChangeItem::MaxSupply(None) + )); + + let v_max_some = make_v0_with_item(TokenConfigurationChangeItem::MaxSupply(Some(12_345))); + match v_max_some.update_token_configuration_item() { + TokenConfigurationChangeItem::MaxSupply(Some(v)) => assert_eq!(*v, 12_345), + other => panic!("unexpected variant: {:?}", other), + } + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_destroy_frozen_funds_transition_action/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_destroy_frozen_funds_transition_action/mod.rs index b3074af9481..e6ff7805326 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_destroy_frozen_funds_transition_action/mod.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_destroy_frozen_funds_transition_action/mod.rs @@ -74,3 +74,181 @@ impl TokenDestroyFrozenFundsTransitionActionAccessorsV0 } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::drive::contract::DataContractFetchInfo; + use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::{ + TokenBaseTransitionAction, TokenBaseTransitionActionAccessorsV0, + TokenBaseTransitionActionV0, + }; + use dpp::data_contract::accessors::v0::DataContractV0Getters; + use dpp::tests::fixtures::get_data_contract_fixture; + use dpp::version::PlatformVersion; + use std::sync::Arc; + + const TEST_OWNER_ID: [u8; 32] = [0xAA; 32]; + const TEST_TOKEN_ID: [u8; 32] = [0xCC; 32]; + const TEST_FROZEN_ID: [u8; 32] = [0xEE; 32]; + const TEST_NONCE: u64 = 4; + const TEST_TOKEN_POSITION: u16 = 0; + const TEST_AMOUNT: TokenAmount = 999; + + fn make_base_action() -> TokenBaseTransitionAction { + let platform_version = PlatformVersion::latest(); + let contract = get_data_contract_fixture( + Some(Identifier::from(TEST_OWNER_ID)), + 0, + platform_version.protocol_version, + ) + .data_contract_owned(); + let fetch_info = Arc::new(DataContractFetchInfo { + contract, + storage_flags: None, + cost: Default::default(), + fee: None, + }); + TokenBaseTransitionAction::V0(TokenBaseTransitionActionV0 { + token_id: Identifier::from(TEST_TOKEN_ID), + identity_contract_nonce: TEST_NONCE, + token_contract_position: TEST_TOKEN_POSITION, + data_contract: fetch_info, + store_in_group: None, + perform_action: true, + }) + } + + fn make_v0() -> TokenDestroyFrozenFundsTransitionActionV0 { + TokenDestroyFrozenFundsTransitionActionV0 { + base: make_base_action(), + frozen_identity_id: Identifier::from(TEST_FROZEN_ID), + amount: TEST_AMOUNT, + public_note: Some("destroyed".to_string()), + } + } + + #[test] + fn from_v0_wraps_in_enum() { + let v0 = make_v0(); + let action: TokenDestroyFrozenFundsTransitionAction = v0.into(); + assert!(matches!( + action, + TokenDestroyFrozenFundsTransitionAction::V0(_) + )); + } + + #[test] + fn enum_base_returns_v0_base() { + let action = TokenDestroyFrozenFundsTransitionAction::V0(make_v0()); + let base = action.base(); + assert_eq!(base.token_id(), Identifier::from(TEST_TOKEN_ID)); + assert_eq!(base.identity_contract_nonce(), TEST_NONCE); + assert_eq!(base.token_position(), TEST_TOKEN_POSITION); + } + + #[test] + fn enum_base_owned_consumes() { + let action = TokenDestroyFrozenFundsTransitionAction::V0(make_v0()); + let base = action.base_owned(); + assert_eq!(base.token_id(), Identifier::from(TEST_TOKEN_ID)); + } + + #[test] + fn enum_frozen_identity_id_returns_v0_value() { + let action = TokenDestroyFrozenFundsTransitionAction::V0(make_v0()); + assert_eq!( + action.frozen_identity_id(), + Identifier::from(TEST_FROZEN_ID) + ); + } + + #[test] + fn enum_set_frozen_identity_id_updates_v0() { + let mut action = TokenDestroyFrozenFundsTransitionAction::V0(make_v0()); + let new_id = Identifier::from([0x22; 32]); + action.set_frozen_identity_id(new_id); + assert_eq!(action.frozen_identity_id(), new_id); + } + + #[test] + fn enum_amount_returns_v0_amount() { + let action = TokenDestroyFrozenFundsTransitionAction::V0(make_v0()); + assert_eq!(action.amount(), TEST_AMOUNT); + } + + #[test] + fn enum_set_amount_updates_v0() { + let mut action = TokenDestroyFrozenFundsTransitionAction::V0(make_v0()); + action.set_amount(42); + assert_eq!(action.amount(), 42); + action.set_amount(0); + assert_eq!(action.amount(), 0); + } + + #[test] + fn enum_public_note_returns_ref() { + let action = TokenDestroyFrozenFundsTransitionAction::V0(make_v0()); + assert_eq!(action.public_note(), Some(&"destroyed".to_string())); + } + + #[test] + fn enum_public_note_returns_none_when_unset() { + let mut v0 = make_v0(); + v0.public_note = None; + let action = TokenDestroyFrozenFundsTransitionAction::V0(v0); + assert_eq!(action.public_note(), None); + } + + #[test] + fn enum_public_note_owned_consumes() { + let action = TokenDestroyFrozenFundsTransitionAction::V0(make_v0()); + assert_eq!(action.public_note_owned(), Some("destroyed".to_string())); + } + + #[test] + fn enum_set_public_note_replaces_value() { + let mut action = TokenDestroyFrozenFundsTransitionAction::V0(make_v0()); + action.set_public_note(Some("revised".to_string())); + assert_eq!(action.public_note(), Some(&"revised".to_string())); + action.set_public_note(None); + assert_eq!(action.public_note(), None); + } + + #[test] + fn v0_accessors_roundtrip() { + let mut v0 = make_v0(); + assert_eq!(v0.amount(), TEST_AMOUNT); + assert_eq!(v0.frozen_identity_id(), Identifier::from(TEST_FROZEN_ID)); + assert_eq!(v0.public_note(), Some(&"destroyed".to_string())); + + v0.set_amount(3); + assert_eq!(v0.amount(), 3); + + let new_id = Identifier::from([0x33; 32]); + v0.set_frozen_identity_id(new_id); + assert_eq!(v0.frozen_identity_id(), new_id); + + v0.set_public_note(None); + assert_eq!(v0.public_note(), None); + v0.set_public_note(Some("again".to_string())); + assert_eq!(v0.public_note(), Some(&"again".to_string())); + + let owned = v0.clone().public_note_owned(); + assert_eq!(owned, Some("again".to_string())); + + let base = v0.base_owned(); + assert_eq!(base.token_id(), Identifier::from(TEST_TOKEN_ID)); + } + + #[test] + fn v0_base_delegations() { + let v0 = make_v0(); + assert_eq!(v0.token_position(), TEST_TOKEN_POSITION); + assert_eq!(v0.token_id(), Identifier::from(TEST_TOKEN_ID)); + let fetched_arc = v0.data_contract_fetch_info(); + let fetched_ref = v0.data_contract_fetch_info_ref(); + assert!(Arc::ptr_eq(&fetched_arc, fetched_ref)); + assert_eq!(v0.data_contract_id(), fetched_ref.contract.id()); + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/v0/transformer.rs index eadff0858f4..ec05268b0b2 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/v0/transformer.rs @@ -300,6 +300,7 @@ impl TokenDirectPurchaseTransitionActionV0 { mod tests { use dpp::balances::credits::TokenAmount; use dpp::fee::Credits; + use dpp::tokens::token_pricing_schedule::TokenPricingSchedule; use std::collections::BTreeMap; /// Verifies that `checked_mul` correctly detects overflow for the values @@ -406,4 +407,219 @@ mod tests { let attacker_price: Credits = 1_000_000; assert!(attacker_price < required_price); } + + // ========================================================================= + // Additional coverage for branches that the above tests do not exercise: + // - SinglePrice branch success path (user pays at least required_price) + // - SinglePrice branch with zero token_count + // - SetPrices branch: exact-match lookup + // - SetPrices branch: range find picks the highest applicable tier + // - SetPrices branch: token_count below all defined thresholds -> None branch + // - required_total user-price gating on the SetPrices branch + // - Zero price boundary (SinglePrice price_per_token = 0) + // ========================================================================= + + /// Happy path: SinglePrice(p) * token_count succeeds and any user who agrees to + /// pay exactly that amount passes the price check (>= required_price). + #[test] + fn single_price_happy_path_exact_payment() { + let price_per_token: Credits = 250; + let token_count: TokenAmount = 4; + let required_price = price_per_token.saturating_mul(token_count); + assert_eq!(required_price, 1_000); + + // An agreed_price equal to required_price satisfies `!(total_agreed_price < required_price)`. + let total_agreed_price: Credits = 1_000; + assert!(!(total_agreed_price < required_price)); + } + + /// A user overpayment is accepted. The transformer records `required_price`, not the agreed amount. + #[test] + fn single_price_overpayment_is_accepted_and_required_price_is_stored() { + let price_per_token: Credits = 10; + let token_count: TokenAmount = 7; + let required_price = price_per_token.saturating_mul(token_count); + assert_eq!(required_price, 70); + + let total_agreed_price: Credits = 100; + assert!(!(total_agreed_price < required_price)); + // Transformer would store `required_price`, not `total_agreed_price`. + } + + /// A user underpayment is rejected via `TokenDirectPurchaseUserPriceTooLow`. + #[test] + fn single_price_underpayment_triggers_price_too_low() { + let price_per_token: Credits = 100; + let token_count: TokenAmount = 5; + let required_price = price_per_token.saturating_mul(token_count); + assert_eq!(required_price, 500); + + let total_agreed_price: Credits = 499; + assert!(total_agreed_price < required_price); + } + + /// SinglePrice where price_per_token is 0 means tokens are free. + /// required_price is 0 for any token_count, so any non-negative agreed_price is accepted. + #[test] + fn single_price_zero_per_token_requires_zero_total() { + let price_per_token: Credits = 0; + let token_count: TokenAmount = 1_000_000; + let required_price = price_per_token.saturating_mul(token_count); + assert_eq!(required_price, 0); + + // Even a user offering 0 is accepted. + let total_agreed_price: Credits = 0; + assert!(!(total_agreed_price < required_price)); + } + + /// SetPrices happy path with tiered lookup: + /// tiers at {1: 100, 10: 80, 100: 50}. Buying 50 should match tier 10 with price 80. + #[test] + fn set_prices_tiered_lookup_picks_highest_applicable_tier() { + let mut set_prices = BTreeMap::::new(); + set_prices.insert(1, 100); + set_prices.insert(10, 80); + set_prices.insert(100, 50); + let token_count: TokenAmount = 50; + + let (matched_quantity, matched_price) = set_prices + .range(..=token_count) + .next_back() + .expect("tier should be found"); + assert_eq!(*matched_quantity, 10); + assert_eq!(*matched_price, 80); + + let required_total = matched_price.checked_mul(token_count).expect("no overflow"); + assert_eq!(required_total, 4_000); + } + + /// SetPrices exact-match lookup — asking for exactly the tier boundary should hit that tier. + #[test] + fn set_prices_exact_tier_boundary_match() { + let mut set_prices = BTreeMap::::new(); + set_prices.insert(1, 100); + set_prices.insert(10, 80); + let token_count: TokenAmount = 10; + + let (matched_quantity, matched_price) = set_prices + .range(..=token_count) + .next_back() + .expect("tier should be found"); + assert_eq!(*matched_quantity, 10); + assert_eq!(*matched_price, 80); + } + + /// SetPrices with token_count above the highest tier uses the highest tier. + #[test] + fn set_prices_count_above_all_tiers_uses_top_tier() { + let mut set_prices = BTreeMap::::new(); + set_prices.insert(1, 100); + set_prices.insert(10, 80); + set_prices.insert(100, 50); + let token_count: TokenAmount = 10_000; + + let (matched_quantity, matched_price) = set_prices + .range(..=token_count) + .next_back() + .expect("tier should be found"); + assert_eq!(*matched_quantity, 100); + assert_eq!(*matched_price, 50); + } + + /// Below-minimum-tier token_count: `range(..=token_count).next_back()` returns `None`, + /// triggering the `TokenAmountUnderMinimumSaleAmount` consensus error. + #[test] + fn set_prices_below_minimum_tier_returns_none() { + let mut set_prices = BTreeMap::::new(); + set_prices.insert(5, 200); + set_prices.insert(10, 150); + let token_count: TokenAmount = 2; + + let matched = set_prices.range(..=token_count).next_back(); + assert!(matched.is_none()); + + // The transformer then reads `set_prices.keys().next()` for the minimum threshold. + let min_threshold = *set_prices.keys().next().expect("non-empty"); + assert_eq!(min_threshold, 5); + } + + /// SetPrices underpayment: matched_price * token_count computes to required_total but + /// the user agreed to less, so the transformer returns `TokenDirectPurchaseUserPriceTooLow`. + #[test] + fn set_prices_underpayment_triggers_price_too_low() { + let mut set_prices = BTreeMap::::new(); + set_prices.insert(1, 100); + set_prices.insert(10, 50); + let token_count: TokenAmount = 20; + + let matched_price = *set_prices + .range(..=token_count) + .next_back() + .expect("tier found") + .1; + let required_total = matched_price.checked_mul(token_count).expect("no overflow"); + assert_eq!(required_total, 1_000); + + let total_agreed_price: Credits = 999; + assert!(total_agreed_price < required_total); + } + + /// SetPrices exact-payment accepted. + #[test] + fn set_prices_exact_payment_accepted() { + let mut set_prices = BTreeMap::::new(); + set_prices.insert(1, 10); + let token_count: TokenAmount = 5; + + let matched_price = *set_prices + .range(..=token_count) + .next_back() + .expect("tier found") + .1; + let required_total = matched_price.checked_mul(token_count).expect("no overflow"); + assert_eq!(required_total, 50); + + let total_agreed_price: Credits = 50; + assert!(!(total_agreed_price < required_total)); + } + + /// SetPrices with a single tier that maps to price 0 — free up to any amount >= threshold. + #[test] + fn set_prices_free_tier_allows_any_agreed_price() { + let mut set_prices = BTreeMap::::new(); + set_prices.insert(1, 0); + let token_count: TokenAmount = 100; + let matched_price = *set_prices + .range(..=token_count) + .next_back() + .expect("tier") + .1; + let required_total = matched_price.checked_mul(token_count).expect("no overflow"); + assert_eq!(required_total, 0); + // Any user-agreed price >= 0 trivially satisfies the comparison. + assert!(!(0u64 < required_total)); + } + + /// When both SinglePrice and SetPrices are represented as enum variants, ensure + /// pattern matching distinguishes them correctly (the transformer branches on this). + #[test] + fn pricing_schedule_enum_dispatch() { + let single = TokenPricingSchedule::SinglePrice(500); + let mut map = BTreeMap::new(); + map.insert(1u64, 50u64); + let multi = TokenPricingSchedule::SetPrices(map); + + assert!(matches!(single, TokenPricingSchedule::SinglePrice(_))); + assert!(matches!(multi, TokenPricingSchedule::SetPrices(_))); + } + + /// SinglePrice(price) * 0 tokens yields 0 required_price. Any agreed price >= 0 clears it. + /// This documents the behavior of the edge case where `token_count == 0`. + #[test] + fn single_price_zero_token_count_yields_zero_required_price() { + let price_per_token: Credits = 1_000; + let token_count: TokenAmount = 0; + let required_price = price_per_token.saturating_mul(token_count); + assert_eq!(required_price, 0); + } } diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_emergency_action_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_emergency_action_transition_action/v0/transformer.rs index c58c69f7fa8..b41763c3d43 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_emergency_action_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_emergency_action_transition_action/v0/transformer.rs @@ -232,3 +232,146 @@ impl TokenEmergencyActionTransitionActionV0 { )) } } + +#[cfg(test)] +mod tests { + use crate::drive::contract::DataContractFetchInfo; + use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::{ + TokenBaseTransitionAction, TokenBaseTransitionActionAccessorsV0, + TokenBaseTransitionActionV0, + }; + use crate::state_transition_action::batch::batched_transition::token_transition::token_emergency_action_transition_action::{ + TokenEmergencyActionTransitionAction, TokenEmergencyActionTransitionActionAccessorsV0, + TokenEmergencyActionTransitionActionV0, + }; + use dpp::data_contract::accessors::v0::DataContractV0Getters; + use dpp::identifier::Identifier; + use dpp::tokens::emergency_action::TokenEmergencyAction; + use platform_version::version::PlatformVersion; + use std::sync::Arc; + + fn make_base() -> TokenBaseTransitionAction { + let fetch_info = DataContractFetchInfo::dpns_contract_fixture( + PlatformVersion::latest().protocol_version, + ); + TokenBaseTransitionAction::V0(TokenBaseTransitionActionV0 { + token_id: Identifier::new([9u8; 32]), + identity_contract_nonce: 7, + token_contract_position: 0, + data_contract: Arc::new(fetch_info), + store_in_group: None, + perform_action: true, + }) + } + + fn make_action_v0( + action: TokenEmergencyAction, + note: Option<&str>, + ) -> TokenEmergencyActionTransitionActionV0 { + TokenEmergencyActionTransitionActionV0 { + base: make_base(), + emergency_action: action, + public_note: note.map(|s| s.to_string()), + } + } + + #[test] + fn v0_accessors_return_emergency_action_and_note() { + let v0 = make_action_v0(TokenEmergencyAction::Pause, Some("pause-note")); + assert_eq!(v0.emergency_action(), TokenEmergencyAction::Pause); + assert_eq!(v0.public_note(), Some(&"pause-note".to_string())); + } + + #[test] + fn v0_set_emergency_action_swaps_variant() { + let mut v0 = make_action_v0(TokenEmergencyAction::Pause, None); + assert!(v0.emergency_action().paused()); + v0.set_emergency_action(TokenEmergencyAction::Resume); + assert_eq!(v0.emergency_action(), TokenEmergencyAction::Resume); + assert!(!v0.emergency_action().paused()); + } + + #[test] + fn v0_set_public_note_updates_note() { + let mut v0 = make_action_v0(TokenEmergencyAction::Resume, None); + assert!(v0.public_note().is_none()); + v0.set_public_note(Some("hi".to_string())); + assert_eq!(v0.public_note(), Some(&"hi".to_string())); + v0.set_public_note(None); + assert!(v0.public_note().is_none()); + } + + #[test] + fn v0_public_note_owned_consumes_self() { + let v0 = make_action_v0(TokenEmergencyAction::Pause, Some("owned")); + let owned: Option = v0.public_note_owned(); + assert_eq!(owned, Some("owned".to_string())); + } + + #[test] + fn v0_base_ref_and_base_owned_preserve_token_id() { + let v0 = make_action_v0(TokenEmergencyAction::Pause, None); + assert_eq!(v0.base().token_id(), Identifier::new([9u8; 32])); + let base = v0.base_owned(); + assert_eq!(base.token_id(), Identifier::new([9u8; 32])); + } + + #[test] + fn v0_default_accessors_delegate_to_base() { + let v0 = make_action_v0(TokenEmergencyAction::Pause, None); + assert_eq!(v0.token_id(), Identifier::new([9u8; 32])); + assert_eq!(v0.token_position(), 0); + // data_contract_id comes from the dpns contract fixture + let fetched = v0.data_contract_fetch_info(); + assert_eq!(v0.data_contract_id(), fetched.contract.id()); + // ref and owned return same id + assert_eq!( + v0.data_contract_fetch_info_ref().contract.id(), + fetched.contract.id() + ); + } + + #[test] + fn enum_from_v0_produces_v0_variant() { + let v0 = make_action_v0(TokenEmergencyAction::Resume, Some("n")); + let action: TokenEmergencyActionTransitionAction = v0.into(); + assert_eq!(action.emergency_action(), TokenEmergencyAction::Resume); + assert_eq!(action.public_note(), Some(&"n".to_string())); + } + + #[test] + fn enum_accessors_route_through_v0() { + let v0 = make_action_v0(TokenEmergencyAction::Pause, Some("start")); + let mut action: TokenEmergencyActionTransitionAction = v0.into(); + assert_eq!(action.emergency_action(), TokenEmergencyAction::Pause); + + action.set_emergency_action(TokenEmergencyAction::Resume); + assert_eq!(action.emergency_action(), TokenEmergencyAction::Resume); + + action.set_public_note(Some("replaced".to_string())); + assert_eq!(action.public_note(), Some(&"replaced".to_string())); + + let base = action.clone().base_owned(); + assert_eq!(base.token_id(), Identifier::new([9u8; 32])); + + assert_eq!(action.base().token_id(), Identifier::new([9u8; 32])); + + let note = action.public_note_owned(); + assert_eq!(note, Some("replaced".to_string())); + } + + #[test] + fn enum_set_public_note_none_clears() { + let mut action: TokenEmergencyActionTransitionAction = + make_action_v0(TokenEmergencyAction::Pause, Some("x")).into(); + action.set_public_note(None); + assert!(action.public_note().is_none()); + } + + #[test] + fn emergency_action_paused_helper_matches_variant() { + // Sanity check the upstream TokenEmergencyAction helper used by this module. + assert!(TokenEmergencyAction::Pause.paused()); + assert!(!TokenEmergencyAction::Resume.paused()); + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_freeze_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_freeze_transition_action/v0/transformer.rs index 0eb3dcaa4a9..ab0a29979ed 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_freeze_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_freeze_transition_action/v0/transformer.rs @@ -232,3 +232,152 @@ impl TokenFreezeTransitionActionV0 { )) } } + +#[cfg(test)] +mod tests { + use crate::drive::contract::DataContractFetchInfo; + use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::{ + TokenBaseTransitionAction, TokenBaseTransitionActionAccessorsV0, + TokenBaseTransitionActionV0, + }; + use crate::state_transition_action::batch::batched_transition::token_transition::token_freeze_transition_action::{ + TokenFreezeTransitionAction, TokenFreezeTransitionActionAccessorsV0, + TokenFreezeTransitionActionV0, + }; + use dpp::data_contract::accessors::v0::DataContractV0Getters; + use dpp::identifier::Identifier; + use platform_version::version::PlatformVersion; + use std::sync::Arc; + + fn make_base(token_position: u16) -> TokenBaseTransitionAction { + let fetch_info = DataContractFetchInfo::dpns_contract_fixture( + PlatformVersion::latest().protocol_version, + ); + TokenBaseTransitionAction::V0(TokenBaseTransitionActionV0 { + token_id: Identifier::new([11u8; 32]), + identity_contract_nonce: 3, + token_contract_position: token_position, + data_contract: Arc::new(fetch_info), + store_in_group: None, + perform_action: true, + }) + } + + fn make_action_v0(target: Identifier, note: Option<&str>) -> TokenFreezeTransitionActionV0 { + TokenFreezeTransitionActionV0 { + base: make_base(0), + identity_to_freeze_id: target, + public_note: note.map(|s| s.to_string()), + } + } + + #[test] + fn v0_identity_to_freeze_id_returns_stored_id() { + let target = Identifier::new([42u8; 32]); + let v0 = make_action_v0(target, None); + assert_eq!(v0.identity_to_freeze_id(), target); + } + + #[test] + fn v0_set_identity_to_freeze_id_updates_field() { + let mut v0 = make_action_v0(Identifier::new([1u8; 32]), None); + let new_id = Identifier::new([99u8; 32]); + v0.set_identity_to_freeze_id(new_id); + assert_eq!(v0.identity_to_freeze_id(), new_id); + } + + #[test] + fn v0_public_note_accessors_return_reference_and_owned() { + let v0 = make_action_v0(Identifier::new([1u8; 32]), Some("frozen")); + assert_eq!(v0.public_note(), Some(&"frozen".to_string())); + + let owned = v0.public_note_owned(); + assert_eq!(owned, Some("frozen".to_string())); + } + + #[test] + fn v0_set_public_note_swaps_contents() { + let mut v0 = make_action_v0(Identifier::new([1u8; 32]), Some("old")); + v0.set_public_note(Some("new".to_string())); + assert_eq!(v0.public_note(), Some(&"new".to_string())); + v0.set_public_note(None); + assert!(v0.public_note().is_none()); + } + + #[test] + fn v0_default_accessors_route_through_base() { + let v0 = make_action_v0(Identifier::new([1u8; 32]), None); + assert_eq!(v0.token_id(), Identifier::new([11u8; 32])); + assert_eq!(v0.token_position(), 0); + let fetch = v0.data_contract_fetch_info(); + assert_eq!(v0.data_contract_id(), fetch.contract.id()); + assert_eq!( + v0.data_contract_fetch_info_ref().contract.id(), + fetch.contract.id() + ); + } + + #[test] + fn v0_non_zero_token_position_propagates() { + let v0 = TokenFreezeTransitionActionV0 { + base: make_base(17), + identity_to_freeze_id: Identifier::new([1u8; 32]), + public_note: None, + }; + assert_eq!(v0.token_position(), 17); + // And via the enum wrapper too + let wrapped: TokenFreezeTransitionAction = v0.into(); + assert_eq!(wrapped.base().token_position(), 17); + } + + #[test] + fn v0_base_ref_and_base_owned_are_consistent() { + let v0 = make_action_v0(Identifier::new([1u8; 32]), None); + let id_from_ref = v0.base().token_id(); + let base = v0.base_owned(); + assert_eq!(id_from_ref, base.token_id()); + } + + #[test] + fn enum_from_v0_preserves_identity_and_note() { + let v0 = make_action_v0(Identifier::new([7u8; 32]), Some("note")); + let wrapped: TokenFreezeTransitionAction = v0.into(); + assert_eq!(wrapped.identity_to_freeze_id(), Identifier::new([7u8; 32])); + assert_eq!(wrapped.public_note(), Some(&"note".to_string())); + } + + #[test] + fn enum_setters_mutate_underlying_v0() { + let mut wrapped: TokenFreezeTransitionAction = + make_action_v0(Identifier::new([1u8; 32]), None).into(); + assert!(wrapped.public_note().is_none()); + + wrapped.set_identity_to_freeze_id(Identifier::new([55u8; 32])); + assert_eq!(wrapped.identity_to_freeze_id(), Identifier::new([55u8; 32])); + + wrapped.set_public_note(Some("added".to_string())); + assert_eq!(wrapped.public_note(), Some(&"added".to_string())); + + // public_note_owned consumes the action + let owned = wrapped.public_note_owned(); + assert_eq!(owned, Some("added".to_string())); + } + + #[test] + fn enum_base_methods_delegate_to_v0() { + let wrapped: TokenFreezeTransitionAction = + make_action_v0(Identifier::new([1u8; 32]), None).into(); + let from_ref_id = wrapped.base().token_id(); + let from_owned_id = wrapped.base_owned().token_id(); + assert_eq!(from_ref_id, from_owned_id); + assert_eq!(from_ref_id, Identifier::new([11u8; 32])); + } + + #[test] + fn enum_set_public_note_none_clears_existing_note() { + let mut wrapped: TokenFreezeTransitionAction = + make_action_v0(Identifier::new([1u8; 32]), Some("to_be_cleared")).into(); + wrapped.set_public_note(None); + assert!(wrapped.public_note().is_none()); + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_mint_transition_action/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_mint_transition_action/mod.rs index 2c4b75de5e7..0d26c593749 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_mint_transition_action/mod.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_mint_transition_action/mod.rs @@ -71,3 +71,178 @@ impl TokenMintTransitionActionAccessorsV0 for TokenMintTransitionAction { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::drive::contract::DataContractFetchInfo; + use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::{ + TokenBaseTransitionAction, TokenBaseTransitionActionV0, + }; + use dpp::data_contract::associated_token::token_configuration::v0::TokenConfigurationV0; + use dpp::data_contract::associated_token::token_configuration::TokenConfiguration; + use dpp::data_contract::config::v0::DataContractConfigV0; + use dpp::data_contract::config::DataContractConfig; + use dpp::data_contract::v1::DataContractV1; + use dpp::prelude::DataContract; + use grovedb_costs::OperationCost; + use std::collections::BTreeMap; + use std::sync::Arc; + + fn build_contract() -> DataContract { + let mut tokens = BTreeMap::new(); + tokens.insert( + 0u16, + TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()), + ); + DataContract::V1(DataContractV1 { + id: Identifier::new([15u8; 32]), + version: 1, + owner_id: Identifier::new([16u8; 32]), + document_types: Default::default(), + config: DataContractConfig::V0(DataContractConfigV0 { + can_be_deleted: false, + readonly: false, + keeps_history: false, + documents_keep_history_contract_default: false, + documents_mutable_contract_default: false, + documents_can_be_deleted_contract_default: false, + requires_identity_encryption_bounded_key: None, + requires_identity_decryption_bounded_key: None, + }), + schema_defs: None, + created_at: None, + updated_at: None, + created_at_block_height: None, + updated_at_block_height: None, + created_at_epoch: None, + updated_at_epoch: None, + groups: Default::default(), + tokens, + keywords: Vec::new(), + description: None, + }) + } + + fn build_mint_enum( + token_id: Identifier, + holder: Identifier, + mint_amount: u64, + public_note: Option, + ) -> TokenMintTransitionAction { + let base_v0 = TokenBaseTransitionActionV0 { + token_id, + identity_contract_nonce: 5, + token_contract_position: 0, + data_contract: Arc::new(DataContractFetchInfo { + contract: build_contract(), + storage_flags: None, + cost: OperationCost::default(), + fee: None, + }), + store_in_group: None, + perform_action: true, + }; + let v0 = TokenMintTransitionActionV0 { + base: TokenBaseTransitionAction::V0(base_v0), + mint_amount, + identity_balance_holder_id: holder, + public_note, + }; + TokenMintTransitionAction::V0(v0) + } + + #[test] + fn enum_getters_forward_to_v0() { + let token_id = Identifier::new([100u8; 32]); + let holder = Identifier::new([101u8; 32]); + let action = build_mint_enum(token_id, holder, 1234, Some("hello".to_string())); + + // base() returns a reference into the V0 variant + match action.base() { + TokenBaseTransitionAction::V0(v0) => { + assert_eq!(v0.token_id, token_id); + assert_eq!(v0.token_contract_position, 0); + } + } + assert_eq!(action.mint_amount(), 1234); + assert_eq!(action.identity_balance_holder_id(), holder); + assert_eq!(action.public_note(), Some(&"hello".to_string())); + } + + #[test] + fn enum_setters_mutate_inner_v0() { + let mut action = build_mint_enum( + Identifier::new([1u8; 32]), + Identifier::new([2u8; 32]), + 10, + None, + ); + + action.set_mint_amount(500); + assert_eq!(action.mint_amount(), 500); + + let new_holder = Identifier::new([77u8; 32]); + action.set_identity_balance_holder_id(new_holder); + assert_eq!(action.identity_balance_holder_id(), new_holder); + + action.set_public_note(Some("set via enum".to_string())); + assert_eq!(action.public_note(), Some(&"set via enum".to_string())); + + action.set_public_note(None); + assert!(action.public_note().is_none()); + } + + #[test] + fn enum_base_owned_and_public_note_owned_consume_self() { + let action = build_mint_enum( + Identifier::new([3u8; 32]), + Identifier::new([4u8; 32]), + 42, + Some("owned".to_string()), + ); + + // Clone to verify both owned methods without borrow issues + let base = action.clone().base_owned(); + match base { + TokenBaseTransitionAction::V0(v0) => { + assert_eq!(v0.token_id, Identifier::new([3u8; 32])); + } + } + + let note = action.public_note_owned(); + assert_eq!(note, Some("owned".to_string())); + } + + #[test] + fn enum_from_v0_conversion_preserves_fields() { + let base_v0 = TokenBaseTransitionActionV0 { + token_id: Identifier::new([60u8; 32]), + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract: Arc::new(DataContractFetchInfo { + contract: build_contract(), + storage_flags: None, + cost: OperationCost::default(), + fee: None, + }), + store_in_group: None, + perform_action: true, + }; + let v0 = TokenMintTransitionActionV0 { + base: TokenBaseTransitionAction::V0(base_v0), + mint_amount: 555, + identity_balance_holder_id: Identifier::new([61u8; 32]), + public_note: Some("derive_more From".to_string()), + }; + + // The derive_more::From impl must yield an equivalent enum + let wrapped: TokenMintTransitionAction = v0.into(); + assert_eq!(wrapped.mint_amount(), 555); + assert_eq!( + wrapped.identity_balance_holder_id(), + Identifier::new([61u8; 32]) + ); + assert_eq!(wrapped.public_note(), Some(&"derive_more From".to_string())); + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_mint_transition_action/v0/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_mint_transition_action/v0/mod.rs index 812949c03ca..61a9af08f8d 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_mint_transition_action/v0/mod.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_mint_transition_action/v0/mod.rs @@ -110,3 +110,149 @@ impl TokenMintTransitionActionAccessorsV0 for TokenMintTransitionActionV0 { self.public_note = public_note; } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::drive::contract::DataContractFetchInfo; + use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionActionV0; + use dpp::data_contract::associated_token::token_configuration::v0::TokenConfigurationV0; + use dpp::data_contract::associated_token::token_configuration::TokenConfiguration; + use dpp::data_contract::config::v0::DataContractConfigV0; + use dpp::data_contract::config::DataContractConfig; + use dpp::data_contract::v1::DataContractV1; + use dpp::prelude::DataContract; + use grovedb_costs::OperationCost; + use std::collections::BTreeMap; + + fn build_contract() -> DataContract { + let mut tokens = BTreeMap::new(); + tokens.insert( + 0u16, + TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()), + ); + DataContract::V1(DataContractV1 { + id: Identifier::new([12u8; 32]), + version: 1, + owner_id: Identifier::new([13u8; 32]), + document_types: Default::default(), + config: DataContractConfig::V0(DataContractConfigV0 { + can_be_deleted: false, + readonly: false, + keeps_history: false, + documents_keep_history_contract_default: false, + documents_mutable_contract_default: false, + documents_can_be_deleted_contract_default: false, + requires_identity_encryption_bounded_key: None, + requires_identity_decryption_bounded_key: None, + }), + schema_defs: None, + created_at: None, + updated_at: None, + created_at_block_height: None, + updated_at_block_height: None, + created_at_epoch: None, + updated_at_epoch: None, + groups: Default::default(), + tokens, + keywords: Vec::new(), + description: None, + }) + } + + fn fetch_info(contract: DataContract) -> Arc { + Arc::new(DataContractFetchInfo { + contract, + storage_flags: None, + cost: OperationCost::default(), + fee: None, + }) + } + + fn build_mint_v0(position: u16, mint_amount: u64) -> TokenMintTransitionActionV0 { + use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionAction; + let base_v0 = TokenBaseTransitionActionV0 { + token_id: Identifier::new([50u8; 32]), + identity_contract_nonce: 17, + token_contract_position: position, + data_contract: fetch_info(build_contract()), + store_in_group: None, + perform_action: true, + }; + TokenMintTransitionActionV0 { + base: TokenBaseTransitionAction::V0(base_v0), + mint_amount, + identity_balance_holder_id: Identifier::new([51u8; 32]), + public_note: Some("mint note".to_string()), + } + } + + #[test] + fn getters_return_struct_fields_and_delegate_to_base() { + let action = build_mint_v0(0, 1_000); + + assert_eq!(action.mint_amount(), 1_000); + assert_eq!( + action.identity_balance_holder_id(), + Identifier::new([51u8; 32]) + ); + assert_eq!(action.public_note(), Some(&"mint note".to_string())); + + // Default-impl delegations into base + assert_eq!(action.token_position(), 0); + assert_eq!(action.token_id(), Identifier::new([50u8; 32])); + assert_eq!(action.data_contract_id(), action.base().data_contract_id()); + } + + #[test] + fn data_contract_fetch_info_variants_share_pointer_through_base() { + let action = build_mint_v0(0, 1); + let reference = action.data_contract_fetch_info_ref(); + let cloned = action.data_contract_fetch_info(); + assert!(Arc::ptr_eq(reference, &cloned)); + } + + #[test] + fn setters_mutate_the_corresponding_fields() { + let mut action = build_mint_v0(0, 1); + + action.set_mint_amount(9_999); + assert_eq!(action.mint_amount(), 9_999); + + let new_holder = Identifier::new([91u8; 32]); + action.set_identity_balance_holder_id(new_holder); + assert_eq!(action.identity_balance_holder_id(), new_holder); + + action.set_public_note(None); + assert!(action.public_note().is_none()); + + action.set_public_note(Some("updated".to_string())); + assert_eq!(action.public_note(), Some(&"updated".to_string())); + } + + #[test] + fn public_note_owned_moves_the_string_out() { + let action = build_mint_v0(0, 1); + let owned = action.public_note_owned(); + assert_eq!(owned, Some("mint note".to_string())); + + let action_none = TokenMintTransitionActionV0 { + base: build_mint_v0(0, 1).base, + mint_amount: 0, + identity_balance_holder_id: Identifier::new([0u8; 32]), + public_note: None, + }; + assert!(action_none.public_note_owned().is_none()); + } + + #[test] + fn base_owned_consumes_self_and_returns_base() { + let action = build_mint_v0(0, 7); + let expected_position = action.base().token_position(); + let expected_token_id = action.base().token_id(); + + let base = action.base_owned(); + assert_eq!(base.token_position(), expected_position); + assert_eq!(base.token_id(), expected_token_id); + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_set_price_for_direct_purchase_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_set_price_for_direct_purchase_transition_action/v0/transformer.rs index bfa755a10f3..6fd06f33d8c 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_set_price_for_direct_purchase_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_set_price_for_direct_purchase_transition_action/v0/transformer.rs @@ -236,3 +236,175 @@ impl TokenSetPriceForDirectPurchaseTransitionActionV0 { )) } } + +#[cfg(test)] +mod tests { + //! Unit tests for the logic fragments of + //! `try_from_{borrowed_,}token_set_price_for_direct_purchase_transition_with_contract_lookup` + //! that can be exercised without wiring up a full `Drive`. + //! + //! These cover: + //! * the `change_note.unwrap_or(public_note)` priority rule (owned and borrowed shapes) + //! * cloning semantics of `Option` + //! * cloning semantics of `Option` for the public note + //! * each `TokenPricingSchedule` variant surviving a round-trip clone + //! * `TokenSetPriceForDirectPurchaseTransitionActionV0` field construction + //! * enum / variant conversion into `TokenSetPriceForDirectPurchaseTransitionAction` + use super::*; + use dpp::tokens::token_pricing_schedule::TokenPricingSchedule; + use std::collections::BTreeMap; + + /// `change_note.unwrap_or(public_note)` must prefer the `change_note` when it + /// is `Some`: this is how group-action-resolved notes override user notes. + /// Note that the resolved note type is `Option>` so the outer + /// `Some` case can itself carry `None` (an explicit "clear the note" signal). + #[test] + fn change_note_takes_precedence_over_public_note_owned() { + let change_note: Option> = Some(Some("group resolved".to_string())); + let public_note: Option = Some("user submitted".to_string()); + let result = change_note.unwrap_or(public_note); + assert_eq!(result, Some("group resolved".to_string())); + } + + /// When the outer change-note is `Some(None)`, the action note is explicitly cleared, + /// overriding any user-supplied note. + #[test] + fn change_note_of_some_none_clears_user_note() { + let change_note: Option> = Some(None); + let public_note: Option = Some("user submitted".to_string()); + let result = change_note.unwrap_or(public_note); + assert!(result.is_none()); + } + + /// When there is no change note, the user's note is kept as-is (including `None`). + #[test] + fn falls_back_to_public_note_when_change_note_is_none() { + let change_note: Option> = None; + let public_note: Option = Some("user submitted".to_string()); + let result = change_note.unwrap_or(public_note); + assert_eq!(result, Some("user submitted".to_string())); + } + + /// A `None` change note combined with a `None` public note keeps the action note empty. + #[test] + fn none_change_note_and_none_public_note_yields_none() { + let change_note: Option> = None; + let public_note: Option = None; + let result = change_note.unwrap_or(public_note); + assert!(result.is_none()); + } + + /// The borrowed transformer clones the note rather than moves it. Verify that behaviour. + #[test] + fn borrowed_branch_clones_public_note_reference() { + let change_note: Option> = None; + let public_note: Option = Some("the note".to_string()); + let result = change_note.unwrap_or(public_note.clone()); + assert_eq!(result, Some("the note".to_string())); + // The original remains untouched (no move). + assert_eq!(public_note, Some("the note".to_string())); + } + + /// The borrowed transformer also clones the `Option`. + #[test] + fn borrowed_branch_clones_single_price_pricing_schedule() { + let price: Option = Some(TokenPricingSchedule::SinglePrice(1_234)); + let cloned = price.clone(); + assert_eq!(cloned, price); + assert_eq!( + cloned, + Some(TokenPricingSchedule::SinglePrice(1_234)), + "SinglePrice should survive a clone exactly" + ); + } + + #[test] + fn borrowed_branch_clones_set_prices_pricing_schedule() { + let mut map = BTreeMap::new(); + map.insert(1u64, 500u64); + map.insert(10, 4_500); + map.insert(100, 40_000); + let price: Option = Some(TokenPricingSchedule::SetPrices(map)); + let cloned = price.clone(); + assert_eq!(cloned, price); + } + + #[test] + fn setting_price_to_none_disables_purchasability() { + // Documented semantics: `price: None` means the token is no longer purchasable. + let price: Option = None; + let cloned = price.clone(); + assert!(cloned.is_none()); + } + + /// Exercise the V0 struct constructor and all accessors the transformer populates. + #[test] + fn action_v0_field_construction_from_transformer_output_shape() { + use crate::drive::contract::DataContractFetchInfo; + use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::{ + TokenBaseTransitionAction, TokenBaseTransitionActionAccessorsV0, + TokenBaseTransitionActionV0, + }; + use dpp::identifier::Identifier; + use dpp::version::PlatformVersion; + use std::sync::Arc; + + let base = TokenBaseTransitionAction::V0(TokenBaseTransitionActionV0 { + token_id: Identifier::from([0x10; 32]), + identity_contract_nonce: 3, + token_contract_position: 0, + data_contract: Arc::new(DataContractFetchInfo::dpns_contract_fixture( + PlatformVersion::latest().protocol_version, + )), + store_in_group: None, + perform_action: true, + }); + + let price = Some(TokenPricingSchedule::SinglePrice(777)); + let public_note = Some("hello".to_string()); + + let action = TokenSetPriceForDirectPurchaseTransitionActionV0 { + base, + price: price.clone(), + public_note: public_note.clone(), + }; + assert_eq!(action.price, price); + assert_eq!(action.public_note, public_note); + assert_eq!(action.base.token_id(), Identifier::from([0x10; 32])); + } + + /// The enum-wrapper `From` is how + /// the transformer constructs the final batched action. Verify it preserves state. + #[test] + fn enum_conversion_from_v0_preserves_state() { + use crate::drive::contract::DataContractFetchInfo; + use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::{ + TokenBaseTransitionAction, TokenBaseTransitionActionV0, + }; + use crate::state_transition_action::batch::batched_transition::token_transition::token_set_price_for_direct_purchase_transition_action::TokenSetPriceForDirectPurchaseTransitionAction; + use dpp::identifier::Identifier; + use dpp::version::PlatformVersion; + use std::sync::Arc; + + let base = TokenBaseTransitionAction::V0(TokenBaseTransitionActionV0 { + token_id: Identifier::from([0x20; 32]), + identity_contract_nonce: 11, + token_contract_position: 0, + data_contract: Arc::new(DataContractFetchInfo::dpns_contract_fixture( + PlatformVersion::latest().protocol_version, + )), + store_in_group: None, + perform_action: true, + }); + let v0 = TokenSetPriceForDirectPurchaseTransitionActionV0 { + base, + price: Some(TokenPricingSchedule::SinglePrice(42)), + public_note: None, + }; + let wrapped: TokenSetPriceForDirectPurchaseTransitionAction = v0.into(); + assert!(matches!( + wrapped, + TokenSetPriceForDirectPurchaseTransitionAction::V0(_) + )); + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_transfer_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_transfer_transition_action/v0/transformer.rs index 0f92b809fa1..64739f3d3a9 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_transfer_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_transfer_transition_action/v0/transformer.rs @@ -180,3 +180,328 @@ impl TokenTransferTransitionActionV0 { )) } } + +#[cfg(test)] +mod tests { + use crate::drive::contract::DataContractFetchInfo; + use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::{ + TokenBaseTransitionAction, TokenBaseTransitionActionAccessorsV0, + TokenBaseTransitionActionV0, + }; + use crate::state_transition_action::batch::batched_transition::token_transition::token_transfer_transition_action::{ + TokenTransferTransitionAction, TokenTransferTransitionActionAccessorsV0, + TokenTransferTransitionActionV0, + }; + use dpp::data_contract::accessors::v0::DataContractV0Getters; + use dpp::identifier::Identifier; + use dpp::tokens::{PrivateEncryptedNote, SharedEncryptedNote}; + use platform_version::version::PlatformVersion; + use std::sync::Arc; + + fn make_base() -> TokenBaseTransitionAction { + let fetch_info = DataContractFetchInfo::dpns_contract_fixture( + PlatformVersion::latest().protocol_version, + ); + TokenBaseTransitionAction::V0(TokenBaseTransitionActionV0 { + token_id: Identifier::new([21u8; 32]), + identity_contract_nonce: 11, + token_contract_position: 0, + data_contract: Arc::new(fetch_info), + store_in_group: None, + perform_action: true, + }) + } + + fn sample_shared() -> SharedEncryptedNote { + // SharedEncryptedNote = (SenderKeyIndex, RecipientKeyIndex, Vec) + (1, 2, vec![0xaa, 0xbb]) + } + + fn sample_private() -> PrivateEncryptedNote { + // PrivateEncryptedNote = (RootEncryptionKeyIndex, DerivationEncryptionKeyIndex, Vec) + (3, 4, vec![0xee, 0xff]) + } + + fn make_action_v0( + amount: u64, + recipient: Identifier, + public_note: Option<&str>, + shared: Option, + private_: Option, + ) -> TokenTransferTransitionActionV0 { + TokenTransferTransitionActionV0 { + base: make_base(), + amount, + recipient_id: recipient, + public_note: public_note.map(|s| s.to_string()), + shared_encrypted_note: shared, + private_encrypted_note: private_, + } + } + + #[test] + fn v0_amount_and_recipient_return_stored_values() { + let v0 = make_action_v0(1_000, Identifier::new([2u8; 32]), None, None, None); + assert_eq!(v0.amount(), 1_000); + assert_eq!(v0.recipient_id(), Identifier::new([2u8; 32])); + } + + #[test] + fn v0_public_note_accessors_work() { + let v0 = make_action_v0(1, Identifier::new([2u8; 32]), Some("tx"), None, None); + assert_eq!(v0.public_note(), Some(&"tx".to_string())); + assert_eq!(v0.public_note_owned(), Some("tx".to_string())); + } + + #[test] + fn v0_set_public_note_updates_field() { + let mut v0 = make_action_v0(1, Identifier::new([2u8; 32]), None, None, None); + v0.set_public_note(Some("added".to_string())); + assert_eq!(v0.public_note(), Some(&"added".to_string())); + v0.set_public_note(None); + assert!(v0.public_note().is_none()); + } + + #[test] + fn v0_shared_encrypted_note_accessors_work() { + let shared = sample_shared(); + let v0 = make_action_v0( + 1, + Identifier::new([2u8; 32]), + None, + Some(shared.clone()), + None, + ); + assert_eq!(v0.shared_encrypted_note(), Some(&shared)); + assert_eq!(v0.shared_encrypted_note_owned(), Some(shared)); + } + + #[test] + fn v0_set_shared_encrypted_note_updates_and_clears() { + let mut v0 = make_action_v0(1, Identifier::new([2u8; 32]), None, None, None); + assert!(v0.shared_encrypted_note().is_none()); + let shared = sample_shared(); + v0.set_shared_encrypted_note(Some(shared.clone())); + assert_eq!(v0.shared_encrypted_note(), Some(&shared)); + v0.set_shared_encrypted_note(None); + assert!(v0.shared_encrypted_note().is_none()); + } + + #[test] + fn v0_private_encrypted_note_accessors_work() { + let private_ = sample_private(); + let v0 = make_action_v0( + 1, + Identifier::new([2u8; 32]), + None, + None, + Some(private_.clone()), + ); + assert_eq!(v0.private_encrypted_note(), Some(&private_)); + assert_eq!(v0.private_encrypted_note_owned(), Some(private_)); + } + + #[test] + fn v0_set_private_encrypted_note_updates_and_clears() { + let mut v0 = make_action_v0(1, Identifier::new([2u8; 32]), None, None, None); + assert!(v0.private_encrypted_note().is_none()); + let private_ = sample_private(); + v0.set_private_encrypted_note(Some(private_.clone())); + assert_eq!(v0.private_encrypted_note(), Some(&private_)); + v0.set_private_encrypted_note(None); + assert!(v0.private_encrypted_note().is_none()); + } + + #[test] + fn v0_notes_ref_returns_cloned_triple() { + let shared = sample_shared(); + let private_ = sample_private(); + let v0 = make_action_v0( + 1, + Identifier::new([2u8; 32]), + Some("pub"), + Some(shared.clone()), + Some(private_.clone()), + ); + let (public_n, shared_n, private_n) = v0.notes(); + assert_eq!(public_n, Some("pub".to_string())); + assert_eq!(shared_n, Some(shared)); + assert_eq!(private_n, Some(private_)); + + // Ensure the v0 is still usable after notes() (non-consuming) + assert_eq!(v0.amount(), 1); + } + + #[test] + fn v0_notes_owned_returns_all_three_owned() { + let shared = sample_shared(); + let private_ = sample_private(); + let v0 = make_action_v0( + 1, + Identifier::new([2u8; 32]), + Some("pub"), + Some(shared.clone()), + Some(private_.clone()), + ); + let (public_n, shared_n, private_n) = v0.notes_owned(); + assert_eq!(public_n, Some("pub".to_string())); + assert_eq!(shared_n, Some(shared)); + assert_eq!(private_n, Some(private_)); + } + + #[test] + fn v0_notes_none_triple_when_all_none() { + let v0 = make_action_v0(1, Identifier::new([2u8; 32]), None, None, None); + let (public_n, shared_n, private_n) = v0.notes(); + assert!(public_n.is_none()); + assert!(shared_n.is_none()); + assert!(private_n.is_none()); + } + + #[test] + fn v0_default_base_accessors_delegate() { + let v0 = make_action_v0(1, Identifier::new([2u8; 32]), None, None, None); + assert_eq!(v0.token_id(), Identifier::new([21u8; 32])); + assert_eq!(v0.token_position(), 0); + assert_eq!(v0.identity_contract_nonce(), 11); + let fetch = v0.data_contract_fetch_info(); + assert_eq!(v0.data_contract_id(), fetch.contract.id()); + assert_eq!( + v0.data_contract_fetch_info_ref().contract.id(), + fetch.contract.id() + ); + } + + #[test] + fn v0_base_owned_yields_same_token_id() { + let v0 = make_action_v0(1, Identifier::new([2u8; 32]), None, None, None); + let base = v0.base_owned(); + assert_eq!(base.token_id(), Identifier::new([21u8; 32])); + } + + #[test] + fn enum_from_v0_preserves_fields() { + let shared = sample_shared(); + let private_ = sample_private(); + let v0 = make_action_v0( + 555, + Identifier::new([3u8; 32]), + Some("x"), + Some(shared.clone()), + Some(private_.clone()), + ); + let wrapped: TokenTransferTransitionAction = v0.into(); + + assert_eq!(wrapped.amount(), 555); + assert_eq!(wrapped.recipient_id(), Identifier::new([3u8; 32])); + assert_eq!(wrapped.public_note(), Some(&"x".to_string())); + assert_eq!(wrapped.shared_encrypted_note(), Some(&shared)); + assert_eq!(wrapped.private_encrypted_note(), Some(&private_)); + } + + #[test] + fn enum_setters_mutate_underlying_fields() { + let mut wrapped: TokenTransferTransitionAction = + make_action_v0(1, Identifier::new([1u8; 32]), None, None, None).into(); + + wrapped.set_public_note(Some("pub".to_string())); + assert_eq!(wrapped.public_note(), Some(&"pub".to_string())); + + let shared = sample_shared(); + wrapped.set_shared_encrypted_note(Some(shared.clone())); + assert_eq!(wrapped.shared_encrypted_note(), Some(&shared)); + + let private_ = sample_private(); + wrapped.set_private_encrypted_note(Some(private_.clone())); + assert_eq!(wrapped.private_encrypted_note(), Some(&private_)); + } + + #[test] + fn enum_clearing_setters_yield_none() { + let shared = sample_shared(); + let private_ = sample_private(); + let mut wrapped: TokenTransferTransitionAction = make_action_v0( + 1, + Identifier::new([1u8; 32]), + Some("p"), + Some(shared), + Some(private_), + ) + .into(); + + wrapped.set_public_note(None); + wrapped.set_shared_encrypted_note(None); + wrapped.set_private_encrypted_note(None); + + assert!(wrapped.public_note().is_none()); + assert!(wrapped.shared_encrypted_note().is_none()); + assert!(wrapped.private_encrypted_note().is_none()); + } + + #[test] + fn enum_notes_and_notes_owned_match_inputs() { + let shared = sample_shared(); + let private_ = sample_private(); + let wrapped: TokenTransferTransitionAction = make_action_v0( + 1, + Identifier::new([1u8; 32]), + Some("pn"), + Some(shared.clone()), + Some(private_.clone()), + ) + .into(); + + let (p, s, pr) = wrapped.notes(); + assert_eq!(p, Some("pn".to_string())); + assert_eq!(s, Some(shared.clone())); + assert_eq!(pr, Some(private_.clone())); + + let (p2, s2, pr2) = wrapped.notes_owned(); + assert_eq!(p2, Some("pn".to_string())); + assert_eq!(s2, Some(shared)); + assert_eq!(pr2, Some(private_)); + } + + #[test] + fn enum_public_note_owned_consumes_and_returns_note() { + let wrapped: TokenTransferTransitionAction = + make_action_v0(1, Identifier::new([1u8; 32]), Some("taken"), None, None).into(); + let note = wrapped.public_note_owned(); + assert_eq!(note, Some("taken".to_string())); + } + + #[test] + fn enum_shared_and_private_note_owned_consume_self() { + let shared = sample_shared(); + let wrapped: TokenTransferTransitionAction = make_action_v0( + 1, + Identifier::new([1u8; 32]), + None, + Some(shared.clone()), + None, + ) + .into(); + assert_eq!(wrapped.shared_encrypted_note_owned(), Some(shared)); + + let private_ = sample_private(); + let wrapped2: TokenTransferTransitionAction = make_action_v0( + 1, + Identifier::new([1u8; 32]), + None, + None, + Some(private_.clone()), + ) + .into(); + assert_eq!(wrapped2.private_encrypted_note_owned(), Some(private_)); + } + + #[test] + fn enum_base_and_base_owned_expose_same_token_id() { + let wrapped: TokenTransferTransitionAction = + make_action_v0(1, Identifier::new([1u8; 32]), None, None, None).into(); + let ref_id = wrapped.base().token_id(); + let owned_id = wrapped.base_owned().token_id(); + assert_eq!(ref_id, owned_id); + assert_eq!(ref_id, Identifier::new([21u8; 32])); + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_transition_action_type.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_transition_action_type.rs index d9a06b61de8..dfa1ee4dfb4 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_transition_action_type.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_transition_action_type.rs @@ -28,3 +28,334 @@ impl TokenTransitionActionTypeGetter for TokenTransitionAction { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::drive::contract::DataContractFetchInfo; + use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::{ + TokenBaseTransitionAction, TokenBaseTransitionActionV0, + }; + use crate::state_transition_action::batch::batched_transition::token_transition::token_burn_transition_action::{ + TokenBurnTransitionAction, TokenBurnTransitionActionV0, + }; + use crate::state_transition_action::batch::batched_transition::token_transition::token_claim_transition_action::{ + TokenClaimTransitionAction, TokenClaimTransitionActionV0, + }; + use crate::state_transition_action::batch::batched_transition::token_transition::token_config_update_transition_action::{ + TokenConfigUpdateTransitionAction, TokenConfigUpdateTransitionActionV0, + }; + use crate::state_transition_action::batch::batched_transition::token_transition::token_destroy_frozen_funds_transition_action::{ + TokenDestroyFrozenFundsTransitionAction, TokenDestroyFrozenFundsTransitionActionV0, + }; + use crate::state_transition_action::batch::batched_transition::token_transition::token_direct_purchase_transition_action::{ + TokenDirectPurchaseTransitionAction, TokenDirectPurchaseTransitionActionV0, + }; + use crate::state_transition_action::batch::batched_transition::token_transition::token_emergency_action_transition_action::{ + TokenEmergencyActionTransitionAction, TokenEmergencyActionTransitionActionV0, + }; + use crate::state_transition_action::batch::batched_transition::token_transition::token_freeze_transition_action::{ + TokenFreezeTransitionAction, TokenFreezeTransitionActionV0, + }; + use crate::state_transition_action::batch::batched_transition::token_transition::token_mint_transition_action::{ + TokenMintTransitionAction, TokenMintTransitionActionV0, + }; + use crate::state_transition_action::batch::batched_transition::token_transition::token_set_price_for_direct_purchase_transition_action::{ + TokenSetPriceForDirectPurchaseTransitionAction, TokenSetPriceForDirectPurchaseTransitionActionV0, + }; + use crate::state_transition_action::batch::batched_transition::token_transition::token_transfer_transition_action::{ + TokenTransferTransitionAction, TokenTransferTransitionActionV0, + }; + use crate::state_transition_action::batch::batched_transition::token_transition::token_unfreeze_transition_action::{ + TokenUnfreezeTransitionAction, TokenUnfreezeTransitionActionV0, + }; + use dpp::data_contract::associated_token::token_configuration_item::TokenConfigurationChangeItem; + use dpp::data_contract::associated_token::token_distribution_key::TokenDistributionInfo; + use dpp::identifier::Identifier; + use dpp::tokens::emergency_action::TokenEmergencyAction; + use platform_version::version::PlatformVersion; + use std::sync::Arc; + + fn make_base() -> TokenBaseTransitionAction { + let fetch_info = DataContractFetchInfo::dpns_contract_fixture( + PlatformVersion::latest().protocol_version, + ); + TokenBaseTransitionAction::V0(TokenBaseTransitionActionV0 { + token_id: Identifier::new([1u8; 32]), + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract: Arc::new(fetch_info), + store_in_group: None, + perform_action: true, + }) + } + + #[test] + fn action_type_burn() { + let action = TokenTransitionAction::BurnAction(TokenBurnTransitionAction::V0( + TokenBurnTransitionActionV0 { + base: make_base(), + burn_from_identifier: Identifier::new([2u8; 32]), + burn_amount: 1, + public_note: None, + }, + )); + assert_eq!(action.action_type(), TokenTransitionActionType::Burn); + } + + #[test] + fn action_type_mint() { + let action = TokenTransitionAction::MintAction(TokenMintTransitionAction::V0( + TokenMintTransitionActionV0 { + base: make_base(), + mint_amount: 100, + identity_balance_holder_id: Identifier::new([3u8; 32]), + public_note: None, + }, + )); + assert_eq!(action.action_type(), TokenTransitionActionType::Mint); + } + + #[test] + fn action_type_transfer() { + let action = TokenTransitionAction::TransferAction(TokenTransferTransitionAction::V0( + TokenTransferTransitionActionV0 { + base: make_base(), + amount: 1, + recipient_id: Identifier::new([4u8; 32]), + public_note: None, + shared_encrypted_note: None, + private_encrypted_note: None, + }, + )); + assert_eq!(action.action_type(), TokenTransitionActionType::Transfer); + } + + #[test] + fn action_type_freeze() { + let action = TokenTransitionAction::FreezeAction(TokenFreezeTransitionAction::V0( + TokenFreezeTransitionActionV0 { + base: make_base(), + identity_to_freeze_id: Identifier::new([5u8; 32]), + public_note: None, + }, + )); + assert_eq!(action.action_type(), TokenTransitionActionType::Freeze); + } + + #[test] + fn action_type_unfreeze() { + let action = TokenTransitionAction::UnfreezeAction(TokenUnfreezeTransitionAction::V0( + TokenUnfreezeTransitionActionV0 { + base: make_base(), + frozen_identity_id: Identifier::new([6u8; 32]), + public_note: None, + }, + )); + assert_eq!(action.action_type(), TokenTransitionActionType::Unfreeze); + } + + #[test] + fn action_type_claim() { + let action = TokenTransitionAction::ClaimAction(TokenClaimTransitionAction::V0( + TokenClaimTransitionActionV0 { + base: make_base(), + amount: 42, + distribution_info: TokenDistributionInfo::PreProgrammed( + 0, + Identifier::new([7u8; 32]), + ), + public_note: None, + }, + )); + assert_eq!(action.action_type(), TokenTransitionActionType::Claim); + } + + #[test] + fn action_type_emergency_action() { + let action = TokenTransitionAction::EmergencyActionAction( + TokenEmergencyActionTransitionAction::V0(TokenEmergencyActionTransitionActionV0 { + base: make_base(), + emergency_action: TokenEmergencyAction::Pause, + public_note: None, + }), + ); + assert_eq!( + action.action_type(), + TokenTransitionActionType::EmergencyAction + ); + } + + #[test] + fn action_type_destroy_frozen_funds() { + let action = TokenTransitionAction::DestroyFrozenFundsAction( + TokenDestroyFrozenFundsTransitionAction::V0( + TokenDestroyFrozenFundsTransitionActionV0 { + base: make_base(), + frozen_identity_id: Identifier::new([8u8; 32]), + amount: 9, + public_note: None, + }, + ), + ); + assert_eq!( + action.action_type(), + TokenTransitionActionType::DestroyFrozenFunds + ); + } + + #[test] + fn action_type_config_update() { + let action = TokenTransitionAction::ConfigUpdateAction( + TokenConfigUpdateTransitionAction::V0(TokenConfigUpdateTransitionActionV0 { + base: make_base(), + update_token_configuration_item: + TokenConfigurationChangeItem::TokenConfigurationNoChange, + public_note: None, + }), + ); + assert_eq!( + action.action_type(), + TokenTransitionActionType::ConfigUpdate + ); + } + + #[test] + fn action_type_direct_purchase() { + let action = TokenTransitionAction::DirectPurchaseAction( + TokenDirectPurchaseTransitionAction::V0(TokenDirectPurchaseTransitionActionV0 { + base: make_base(), + token_count: 10, + total_agreed_price: 100, + }), + ); + assert_eq!( + action.action_type(), + TokenTransitionActionType::DirectPurchase + ); + } + + #[test] + fn action_type_set_price_for_direct_purchase() { + let action = TokenTransitionAction::SetPriceForDirectPurchaseAction( + TokenSetPriceForDirectPurchaseTransitionAction::V0( + TokenSetPriceForDirectPurchaseTransitionActionV0 { + base: make_base(), + price: None, + public_note: None, + }, + ), + ); + assert_eq!( + action.action_type(), + TokenTransitionActionType::SetPriceForDirectPurchase + ); + } + + /// All eleven variants map to distinct `TokenTransitionActionType` values. + #[test] + fn every_variant_maps_to_distinct_action_type() { + let all = vec![ + TokenTransitionAction::BurnAction(TokenBurnTransitionAction::V0( + TokenBurnTransitionActionV0 { + base: make_base(), + burn_from_identifier: Identifier::new([2u8; 32]), + burn_amount: 1, + public_note: None, + }, + )), + TokenTransitionAction::MintAction(TokenMintTransitionAction::V0( + TokenMintTransitionActionV0 { + base: make_base(), + mint_amount: 1, + identity_balance_holder_id: Identifier::new([3u8; 32]), + public_note: None, + }, + )), + TokenTransitionAction::TransferAction(TokenTransferTransitionAction::V0( + TokenTransferTransitionActionV0 { + base: make_base(), + amount: 1, + recipient_id: Identifier::new([4u8; 32]), + public_note: None, + shared_encrypted_note: None, + private_encrypted_note: None, + }, + )), + TokenTransitionAction::FreezeAction(TokenFreezeTransitionAction::V0( + TokenFreezeTransitionActionV0 { + base: make_base(), + identity_to_freeze_id: Identifier::new([5u8; 32]), + public_note: None, + }, + )), + TokenTransitionAction::UnfreezeAction(TokenUnfreezeTransitionAction::V0( + TokenUnfreezeTransitionActionV0 { + base: make_base(), + frozen_identity_id: Identifier::new([6u8; 32]), + public_note: None, + }, + )), + TokenTransitionAction::ClaimAction(TokenClaimTransitionAction::V0( + TokenClaimTransitionActionV0 { + base: make_base(), + amount: 1, + distribution_info: TokenDistributionInfo::PreProgrammed( + 0, + Identifier::new([7u8; 32]), + ), + public_note: None, + }, + )), + TokenTransitionAction::EmergencyActionAction(TokenEmergencyActionTransitionAction::V0( + TokenEmergencyActionTransitionActionV0 { + base: make_base(), + emergency_action: TokenEmergencyAction::Resume, + public_note: None, + }, + )), + TokenTransitionAction::DestroyFrozenFundsAction( + TokenDestroyFrozenFundsTransitionAction::V0( + TokenDestroyFrozenFundsTransitionActionV0 { + base: make_base(), + frozen_identity_id: Identifier::new([8u8; 32]), + amount: 1, + public_note: None, + }, + ), + ), + TokenTransitionAction::ConfigUpdateAction(TokenConfigUpdateTransitionAction::V0( + TokenConfigUpdateTransitionActionV0 { + base: make_base(), + update_token_configuration_item: + TokenConfigurationChangeItem::TokenConfigurationNoChange, + public_note: None, + }, + )), + TokenTransitionAction::DirectPurchaseAction(TokenDirectPurchaseTransitionAction::V0( + TokenDirectPurchaseTransitionActionV0 { + base: make_base(), + token_count: 1, + total_agreed_price: 1, + }, + )), + TokenTransitionAction::SetPriceForDirectPurchaseAction( + TokenSetPriceForDirectPurchaseTransitionAction::V0( + TokenSetPriceForDirectPurchaseTransitionActionV0 { + base: make_base(), + price: None, + public_note: None, + }, + ), + ), + ]; + + let types: Vec = all.iter().map(|a| a.action_type()).collect(); + + // All 11 types are unique. + let mut unique = types.clone(); + unique.sort_by_key(|t| format!("{}", t)); + unique.dedup(); + assert_eq!(unique.len(), 11, "every variant has a distinct action_type"); + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_unfreeze_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_unfreeze_transition_action/v0/transformer.rs index 2c4833d9150..d0ab9ea8048 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_unfreeze_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_unfreeze_transition_action/v0/transformer.rs @@ -232,3 +232,135 @@ impl TokenUnfreezeTransitionActionV0 { )) } } + +#[cfg(test)] +mod tests { + use crate::drive::contract::DataContractFetchInfo; + use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::{ + TokenBaseTransitionAction, TokenBaseTransitionActionAccessorsV0, + TokenBaseTransitionActionV0, + }; + use crate::state_transition_action::batch::batched_transition::token_transition::token_unfreeze_transition_action::{ + TokenUnfreezeTransitionAction, TokenUnfreezeTransitionActionAccessorsV0, + TokenUnfreezeTransitionActionV0, + }; + use dpp::data_contract::accessors::v0::DataContractV0Getters; + use dpp::identifier::Identifier; + use platform_version::version::PlatformVersion; + use std::sync::Arc; + + fn make_base() -> TokenBaseTransitionAction { + let fetch_info = DataContractFetchInfo::dpns_contract_fixture( + PlatformVersion::latest().protocol_version, + ); + TokenBaseTransitionAction::V0(TokenBaseTransitionActionV0 { + token_id: Identifier::new([12u8; 32]), + identity_contract_nonce: 5, + token_contract_position: 0, + data_contract: Arc::new(fetch_info), + store_in_group: None, + perform_action: true, + }) + } + + fn make_action_v0(target: Identifier, note: Option<&str>) -> TokenUnfreezeTransitionActionV0 { + TokenUnfreezeTransitionActionV0 { + base: make_base(), + frozen_identity_id: target, + public_note: note.map(|s| s.to_string()), + } + } + + #[test] + fn v0_frozen_identity_id_returns_stored_id() { + let target = Identifier::new([42u8; 32]); + let v0 = make_action_v0(target, None); + assert_eq!(v0.frozen_identity_id(), target); + } + + #[test] + fn v0_set_frozen_identity_id_updates_field() { + let mut v0 = make_action_v0(Identifier::new([1u8; 32]), None); + let replacement = Identifier::new([77u8; 32]); + v0.set_frozen_identity_id(replacement); + assert_eq!(v0.frozen_identity_id(), replacement); + } + + #[test] + fn v0_public_note_accessors_return_reference_and_owned() { + let v0 = make_action_v0(Identifier::new([1u8; 32]), Some("thawed")); + assert_eq!(v0.public_note(), Some(&"thawed".to_string())); + assert_eq!(v0.public_note_owned(), Some("thawed".to_string())); + } + + #[test] + fn v0_set_public_note_round_trip() { + let mut v0 = make_action_v0(Identifier::new([1u8; 32]), None); + assert!(v0.public_note().is_none()); + v0.set_public_note(Some("set".to_string())); + assert_eq!(v0.public_note(), Some(&"set".to_string())); + v0.set_public_note(None); + assert!(v0.public_note().is_none()); + } + + #[test] + fn v0_default_accessors_route_through_base() { + let v0 = make_action_v0(Identifier::new([1u8; 32]), None); + assert_eq!(v0.token_id(), Identifier::new([12u8; 32])); + assert_eq!(v0.token_position(), 0); + let fetch = v0.data_contract_fetch_info(); + assert_eq!(v0.data_contract_id(), fetch.contract.id()); + assert_eq!( + v0.data_contract_fetch_info_ref().contract.id(), + fetch.contract.id() + ); + } + + #[test] + fn v0_base_ref_and_base_owned_are_consistent() { + let v0 = make_action_v0(Identifier::new([1u8; 32]), None); + let id_from_ref = v0.base().token_id(); + let base = v0.base_owned(); + assert_eq!(id_from_ref, base.token_id()); + } + + #[test] + fn enum_from_v0_preserves_identity_and_note() { + let v0 = make_action_v0(Identifier::new([9u8; 32]), Some("via_enum")); + let wrapped: TokenUnfreezeTransitionAction = v0.into(); + assert_eq!(wrapped.frozen_identity_id(), Identifier::new([9u8; 32])); + assert_eq!(wrapped.public_note(), Some(&"via_enum".to_string())); + } + + #[test] + fn enum_setters_mutate_underlying_v0() { + let mut wrapped: TokenUnfreezeTransitionAction = + make_action_v0(Identifier::new([1u8; 32]), None).into(); + + wrapped.set_frozen_identity_id(Identifier::new([33u8; 32])); + assert_eq!(wrapped.frozen_identity_id(), Identifier::new([33u8; 32])); + + wrapped.set_public_note(Some("added".to_string())); + assert_eq!(wrapped.public_note(), Some(&"added".to_string())); + + let owned = wrapped.public_note_owned(); + assert_eq!(owned, Some("added".to_string())); + } + + #[test] + fn enum_base_methods_delegate_to_v0() { + let wrapped: TokenUnfreezeTransitionAction = + make_action_v0(Identifier::new([1u8; 32]), None).into(); + assert_eq!(wrapped.base().token_id(), Identifier::new([12u8; 32])); + let base_owned = wrapped.base_owned(); + assert_eq!(base_owned.token_id(), Identifier::new([12u8; 32])); + } + + #[test] + fn enum_set_public_note_none_clears_existing_note() { + let mut wrapped: TokenUnfreezeTransitionAction = + make_action_v0(Identifier::new([1u8; 32]), Some("wipeme")).into(); + wrapped.set_public_note(None); + assert!(wrapped.public_note().is_none()); + } +}