From 6ee7b0ade1bed5b991b7a488209ce26cf1902e55 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 22 Apr 2026 05:14:35 +0800 Subject: [PATCH] test(dpp): cover state transitions, token configuration, perpetual distribution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 363 new unit tests across 27 files targeting the largest remaining gaps in rs-dpp (token config methods, document batch transitions, identity transitions, shielded transitions, reward distribution types/moments). Per-target breakdown (previous coverage → now exercised): - state_transition/state_transitions/document/batch_transition (79% → covered, 33 tests): action-type TryFrom error messages across document + token enums, resolver match-arm coverage for BatchedTransition and BatchedTransitionRef (14 Document(_)=>None + 11 token => value arms per trait), find_duplicates_by_id fingerprint specificity + three-way collision pushes_pairs quirk, validate_basic_structure DocumentTransitionsAreAbsent + MaxDocumentsTransitionsExceeded + NonceOutOfBounds consensus errors. - data_contract/associated_token/token_configuration/methods (62% → covered, 60 tests across 4 submodules): every enum arm of apply_token_configuration_item (23 tests), authorized_action_takers special-case NoOne returns for TokenConfigurationNoChange + MainControlGroup (15 tests), can_apply_token_configuration_item allow/deny across all rule categories (15 tests), validate_token_configuration_groups_exist GroupPositionDoesNotExistError + MainGroupIsNotDefinedError paths (7 tests). - data_contract/associated_token/token_configuration_item.rs (54% → covered, 24 tests): payload_serialization byte layout for MaxSupply/MainControlGroup BE, Identity 32 bytes, MintingAllowChoosingDestination bool-as-u8, Conventions bincode roundtrip, tag-byte sharing across all 24 AuthorizedActionTakers variants, Display + equality per arm. - data_contract/associated_token/token_configuration/v0 (79% → covered, 38 tests): TokenConfigurationPreset default_* helpers across all 5 TokenConfigurationPresetFeatures variants, default_distribution_rules with direct-pricing on/off, default_most_restrictive, all_used_group_positions Group variant collection + MainGroup flag + main_control_group inclusion. - data_contract/associated_token/token_perpetual_distribution/ reward_distribution_moment (65% → covered, 41 tests): to_u64, same_type, cycle_start (aligned/unaligned/zero-step + type mismatch), Add (all variants + overflow + u16 rhs cap), Add (all variants + overflow + type mismatch), Div (zero rhs, over-u16, ok), Div (zero + type mismatch), PartialEq, to_be_bytes_vec, Display, from_block_info, steps_till edge cases. - data_contract/associated_token/token_perpetual_distribution/ reward_distribution_type (56% → covered, 28 tests): moment_from_bytes 5 decode-error arms + 3 success arms, max_cycle_moment capped-by-current + non-fixed + epoch current-1 saturation, validate_structure_interval_v0 block-too-short Mainnet / Testnet / Regtest + time boundary + not-minute-aligned error paths. - state_transitions/identity/public_key_in_creation (84% → covered, 6 tests): StateTransitionValueConvert dispatch + version errors, canonical_object inserts $version, value_map roundtrip, default_versioned unknown-version, duplicate-keys witness invariant. - state_transitions/identity/identity_topup_from_addresses (69% → covered, 17 tests): full validate_structure branch coverage — no_inputs, witness_count_mismatch, output_is_input, fee_strategy_empty/duplicate/out_of_bounds, input_below_minimum, output_below_minimum, input_sum_less_than_required, overflow. - state_transitions/identity/identity_create_from_addresses (78% → covered, 16 tests): full validate_structure branch coverage — no_inputs, no_public_keys (MissingMasterPublicKey), too_many_public_keys (MaxIdentityPublicKeyLimit), input/witness mismatch, fee_strategy bounds, input/output below minimum, input_sum_less_than_required, overflow_on_input_sum. - state_transitions/identity/identity_create_transition (84% → covered, 7 tests): try_from_inner ChainAssetLockProof identity_id derivation, AssetLockProved::set_asset_lock_proof, accessors, dispatch, versioning. - state_transitions/identity/identity_credit_withdrawal_transition (89% → covered, 10 tests v0+v1): skip_signature removal across to_object / to_cleaned_object / to_canonical_cleaned_object, Pooling::Never vs Standard roundtrip, v1 None-output_script value_map roundtrip. - state_transitions/identity/identity_credit_transfer_transition + identity_credit_transfer_to_addresses (74/78% → covered, 8 tests): skip_signature path removal, modified_data_ids + unique_identifiers, validate_structure NoRecipients / too_many / recipient_below_minimum / single-valid. - state_transitions/shielded/* (68-74% → covered, 25 tests across shield_from_asset_lock / shielded_withdrawal / unshield / shielded_transfer): StateTransitionLike type/version/ modified_ids/unique_identifiers (base64 of asset lock or hex of nullifier), signature + FeatureVersioned + AssetLockProved accessors. - state_transitions/address_funds/address_funding_from_asset_lock (84% → covered, 11 tests): full StateTransitionLike surface, SingleSigned + WitnessSigned setters, UserFeeIncrease get/set, Signable shadow From conversion. All 363 tests pass. No production bugs surfaced. One design quirk noted in find_duplicates_by_id — three-way collisions push (existing, new) for each subsequent collision, producing 4 entries for a triple-duplicate; pinned by test for regression detection. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../apply_token_configuration_item/v0/mod.rs | 438 ++++++++++++++ .../v0/mod.rs | 226 ++++++++ .../v0/mod.rs | 328 +++++++++++ .../v0/mod.rs | 145 +++++ .../token_configuration/v0/mod.rs | 544 ++++++++++++++++++ .../token_configuration_item.rs | 361 ++++++++++++ .../reward_distribution_moment/mod.rs | 530 +++++++++++++++++ .../reward_distribution_type/mod.rs | 337 +++++++++++ .../v0/mod.rs | 159 +++++ .../document_transition_action_type.rs | 51 ++ .../batched_transition/resolvers.rs | 470 +++++++++++++++ .../token_transition_action_type.rs | 49 ++ .../find_duplicates_by_id/v0/mod.rs | 84 +++ .../validate_basic_structure/v0/mod.rs | 204 +++++++ .../v0/mod.rs | 296 ++++++++++ .../identity_create_transition/v0/mod.rs | 125 ++++ .../v0/mod.rs | 77 +++ .../v0/state_transition_validation.rs | 73 +++ .../v0/mod.rs | 50 ++ .../v0/mod.rs | 53 ++ .../v1/mod.rs | 55 ++ .../v0/mod.rs | 310 ++++++++++ .../identity/public_key_in_creation/mod.rs | 104 ++++ .../v0/mod.rs | 93 +++ .../shielded_transfer_transition/v0/mod.rs | 68 +++ .../shielded_withdrawal_transition/v0/mod.rs | 71 +++ .../shielded/unshield_transition/v0/mod.rs | 66 +++ 27 files changed, 5367 insertions(+) diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/apply_token_configuration_item/v0/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/apply_token_configuration_item/v0/mod.rs index 051fec4a633..5e5ff7dcc86 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/apply_token_configuration_item/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/apply_token_configuration_item/v0/mod.rs @@ -154,3 +154,441 @@ impl TokenConfigurationV0 { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::data_contract::associated_token::token_configuration::accessors::v0::{ + TokenConfigurationV0Getters, TokenConfigurationV0Setters, + }; + use crate::data_contract::associated_token::token_configuration_convention::v0::TokenConfigurationConventionV0; + use crate::data_contract::associated_token::token_configuration_convention::TokenConfigurationConvention; + use crate::data_contract::associated_token::token_marketplace_rules::accessors::v0::TokenMarketplaceRulesV0Getters; + use crate::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; + use platform_value::Identifier; + + fn base() -> TokenConfigurationV0 { + TokenConfigurationV0::default_most_restrictive() + } + + // --- no-op --- + + #[test] + fn no_change_leaves_config_untouched() { + let mut c = base(); + let before = c.clone(); + c.apply_token_configuration_item(TokenConfigurationChangeItem::TokenConfigurationNoChange); + assert_eq!(c, before); + } + + // --- conventions --- + + #[test] + fn conventions_item_replaces_conventions() { + let mut c = base(); + let new_conv = TokenConfigurationConvention::V0(TokenConfigurationConventionV0 { + localizations: Default::default(), + decimals: 3, + }); + c.apply_token_configuration_item(TokenConfigurationChangeItem::Conventions( + new_conv.clone(), + )); + assert_eq!(c.conventions(), &new_conv); + } + + #[test] + fn conventions_control_group_sets_authorized_action_takers() { + let mut c = base(); + c.apply_token_configuration_item(TokenConfigurationChangeItem::ConventionsControlGroup( + AuthorizedActionTakers::ContractOwner, + )); + assert_eq!( + c.conventions_change_rules() + .authorized_to_make_change_action_takers(), + &AuthorizedActionTakers::ContractOwner + ); + } + + #[test] + fn conventions_admin_group_sets_admin_action_takers() { + let mut c = base(); + c.apply_token_configuration_item(TokenConfigurationChangeItem::ConventionsAdminGroup( + AuthorizedActionTakers::ContractOwner, + )); + assert_eq!( + c.conventions_change_rules().admin_action_takers(), + &AuthorizedActionTakers::ContractOwner + ); + } + + // --- max_supply --- + + #[test] + fn max_supply_item_sets_value() { + let mut c = base(); + c.apply_token_configuration_item(TokenConfigurationChangeItem::MaxSupply(Some(500))); + assert_eq!(c.max_supply(), Some(500)); + } + + #[test] + fn max_supply_item_sets_none() { + let mut c = base(); + c.set_max_supply(Some(999)); + c.apply_token_configuration_item(TokenConfigurationChangeItem::MaxSupply(None)); + assert_eq!(c.max_supply(), None); + } + + #[test] + fn max_supply_control_group_sets_authorized() { + let mut c = base(); + c.apply_token_configuration_item(TokenConfigurationChangeItem::MaxSupplyControlGroup( + AuthorizedActionTakers::ContractOwner, + )); + assert_eq!( + c.max_supply_change_rules() + .authorized_to_make_change_action_takers(), + &AuthorizedActionTakers::ContractOwner + ); + } + + #[test] + fn max_supply_admin_group_sets_admin() { + let mut c = base(); + c.apply_token_configuration_item(TokenConfigurationChangeItem::MaxSupplyAdminGroup( + AuthorizedActionTakers::ContractOwner, + )); + assert_eq!( + c.max_supply_change_rules().admin_action_takers(), + &AuthorizedActionTakers::ContractOwner + ); + } + + // --- distribution: new tokens destination identity --- + + #[test] + fn new_tokens_destination_identity_sets_value() { + let mut c = base(); + let id = Identifier::from([3u8; 32]); + c.apply_token_configuration_item( + TokenConfigurationChangeItem::NewTokensDestinationIdentity(Some(id)), + ); + assert_eq!( + c.distribution_rules().new_tokens_destination_identity(), + Some(&id) + ); + } + + #[test] + fn new_tokens_destination_identity_control_group_sets_authorized() { + let mut c = base(); + c.apply_token_configuration_item( + TokenConfigurationChangeItem::NewTokensDestinationIdentityControlGroup( + AuthorizedActionTakers::ContractOwner, + ), + ); + assert_eq!( + c.distribution_rules() + .new_tokens_destination_identity_rules() + .authorized_to_make_change_action_takers(), + &AuthorizedActionTakers::ContractOwner + ); + } + + #[test] + fn new_tokens_destination_identity_admin_group_sets_admin() { + let mut c = base(); + c.apply_token_configuration_item( + TokenConfigurationChangeItem::NewTokensDestinationIdentityAdminGroup( + AuthorizedActionTakers::ContractOwner, + ), + ); + assert_eq!( + c.distribution_rules() + .new_tokens_destination_identity_rules() + .admin_action_takers(), + &AuthorizedActionTakers::ContractOwner + ); + } + + #[test] + fn minting_allow_choosing_destination_sets_value() { + let mut c = base(); + c.apply_token_configuration_item( + TokenConfigurationChangeItem::MintingAllowChoosingDestination(false), + ); + assert!(!c.distribution_rules().minting_allow_choosing_destination()); + } + + #[test] + fn minting_allow_choosing_destination_control_group_sets_authorized() { + let mut c = base(); + c.apply_token_configuration_item( + TokenConfigurationChangeItem::MintingAllowChoosingDestinationControlGroup( + AuthorizedActionTakers::ContractOwner, + ), + ); + assert_eq!( + c.distribution_rules() + .minting_allow_choosing_destination_rules() + .authorized_to_make_change_action_takers(), + &AuthorizedActionTakers::ContractOwner + ); + } + + #[test] + fn minting_allow_choosing_destination_admin_group_sets_admin() { + let mut c = base(); + c.apply_token_configuration_item( + TokenConfigurationChangeItem::MintingAllowChoosingDestinationAdminGroup( + AuthorizedActionTakers::ContractOwner, + ), + ); + assert_eq!( + c.distribution_rules() + .minting_allow_choosing_destination_rules() + .admin_action_takers(), + &AuthorizedActionTakers::ContractOwner + ); + } + + #[test] + fn perpetual_distribution_none_clears_it() { + let mut c = base(); + c.apply_token_configuration_item(TokenConfigurationChangeItem::PerpetualDistribution(None)); + assert!(c.distribution_rules().perpetual_distribution().is_none()); + } + + #[test] + fn perpetual_distribution_control_group_sets_authorized() { + let mut c = base(); + c.apply_token_configuration_item( + TokenConfigurationChangeItem::PerpetualDistributionControlGroup( + AuthorizedActionTakers::ContractOwner, + ), + ); + assert_eq!( + c.distribution_rules() + .perpetual_distribution_rules() + .authorized_to_make_change_action_takers(), + &AuthorizedActionTakers::ContractOwner + ); + } + + #[test] + fn perpetual_distribution_admin_group_sets_admin() { + let mut c = base(); + c.apply_token_configuration_item( + TokenConfigurationChangeItem::PerpetualDistributionAdminGroup( + AuthorizedActionTakers::ContractOwner, + ), + ); + assert_eq!( + c.distribution_rules() + .perpetual_distribution_rules() + .admin_action_takers(), + &AuthorizedActionTakers::ContractOwner + ); + } + + // --- manual minting / burning --- + + #[test] + fn manual_minting_sets_authorized() { + let mut c = base(); + c.apply_token_configuration_item(TokenConfigurationChangeItem::ManualMinting( + AuthorizedActionTakers::ContractOwner, + )); + assert_eq!( + c.manual_minting_rules() + .authorized_to_make_change_action_takers(), + &AuthorizedActionTakers::ContractOwner + ); + } + + #[test] + fn manual_minting_admin_group_sets_admin() { + let mut c = base(); + c.apply_token_configuration_item(TokenConfigurationChangeItem::ManualMintingAdminGroup( + AuthorizedActionTakers::ContractOwner, + )); + assert_eq!( + c.manual_minting_rules().admin_action_takers(), + &AuthorizedActionTakers::ContractOwner + ); + } + + #[test] + fn manual_burning_sets_authorized() { + let mut c = base(); + c.apply_token_configuration_item(TokenConfigurationChangeItem::ManualBurning( + AuthorizedActionTakers::ContractOwner, + )); + assert_eq!( + c.manual_burning_rules() + .authorized_to_make_change_action_takers(), + &AuthorizedActionTakers::ContractOwner + ); + } + + #[test] + fn manual_burning_admin_group_sets_admin() { + let mut c = base(); + c.apply_token_configuration_item(TokenConfigurationChangeItem::ManualBurningAdminGroup( + AuthorizedActionTakers::ContractOwner, + )); + assert_eq!( + c.manual_burning_rules().admin_action_takers(), + &AuthorizedActionTakers::ContractOwner + ); + } + + // --- freeze / unfreeze / destroy --- + + #[test] + fn freeze_and_admin_set_expected_fields() { + let mut c = base(); + c.apply_token_configuration_item(TokenConfigurationChangeItem::Freeze( + AuthorizedActionTakers::ContractOwner, + )); + c.apply_token_configuration_item(TokenConfigurationChangeItem::FreezeAdminGroup( + AuthorizedActionTakers::MainGroup, + )); + assert_eq!( + c.freeze_rules().authorized_to_make_change_action_takers(), + &AuthorizedActionTakers::ContractOwner + ); + assert_eq!( + c.freeze_rules().admin_action_takers(), + &AuthorizedActionTakers::MainGroup + ); + } + + #[test] + fn unfreeze_and_admin_set_expected_fields() { + let mut c = base(); + c.apply_token_configuration_item(TokenConfigurationChangeItem::Unfreeze( + AuthorizedActionTakers::ContractOwner, + )); + c.apply_token_configuration_item(TokenConfigurationChangeItem::UnfreezeAdminGroup( + AuthorizedActionTakers::MainGroup, + )); + assert_eq!( + c.unfreeze_rules().authorized_to_make_change_action_takers(), + &AuthorizedActionTakers::ContractOwner + ); + assert_eq!( + c.unfreeze_rules().admin_action_takers(), + &AuthorizedActionTakers::MainGroup + ); + } + + #[test] + fn destroy_frozen_funds_and_admin_set_expected_fields() { + let mut c = base(); + c.apply_token_configuration_item(TokenConfigurationChangeItem::DestroyFrozenFunds( + AuthorizedActionTakers::ContractOwner, + )); + c.apply_token_configuration_item( + TokenConfigurationChangeItem::DestroyFrozenFundsAdminGroup( + AuthorizedActionTakers::MainGroup, + ), + ); + assert_eq!( + c.destroy_frozen_funds_rules() + .authorized_to_make_change_action_takers(), + &AuthorizedActionTakers::ContractOwner + ); + assert_eq!( + c.destroy_frozen_funds_rules().admin_action_takers(), + &AuthorizedActionTakers::MainGroup + ); + } + + // --- emergency action --- + + #[test] + fn emergency_action_and_admin_set_expected_fields() { + let mut c = base(); + c.apply_token_configuration_item(TokenConfigurationChangeItem::EmergencyAction( + AuthorizedActionTakers::ContractOwner, + )); + c.apply_token_configuration_item(TokenConfigurationChangeItem::EmergencyActionAdminGroup( + AuthorizedActionTakers::MainGroup, + )); + assert_eq!( + c.emergency_action_rules() + .authorized_to_make_change_action_takers(), + &AuthorizedActionTakers::ContractOwner + ); + assert_eq!( + c.emergency_action_rules().admin_action_takers(), + &AuthorizedActionTakers::MainGroup + ); + } + + // --- main control group --- + + #[test] + fn main_control_group_sets_value_then_clears() { + let mut c = base(); + c.apply_token_configuration_item(TokenConfigurationChangeItem::MainControlGroup(Some(5))); + assert_eq!(c.main_control_group(), Some(5)); + c.apply_token_configuration_item(TokenConfigurationChangeItem::MainControlGroup(None)); + assert_eq!(c.main_control_group(), None); + } + + // --- marketplace --- + + #[test] + fn marketplace_trade_mode_sets_trade_mode() { + use crate::data_contract::associated_token::token_marketplace_rules::v0::TokenTradeMode; + let mut c = base(); + c.apply_token_configuration_item(TokenConfigurationChangeItem::MarketplaceTradeMode( + TokenTradeMode::NotTradeable, + )); + // Only one variant currently, but this exercises the dispatch arm. + assert_eq!( + c.distribution_rules_mut() + .change_direct_purchase_pricing_rules() + .authorized_to_make_change_action_takers(), + &AuthorizedActionTakers::NoOne + ); + // Primary assertion: confirm trade mode has been set + // (we route via the marketplace rule accessor) + use crate::data_contract::associated_token::token_marketplace_rules::accessors::v0::TokenMarketplaceRulesV0Getters as _; + let _ = c.distribution_rules(); // avoid unused warning if helpers change + // Read trade mode indirectly via reflection: marketplace_rules is pub on V0 + match c.marketplace_rules { + crate::data_contract::associated_token::token_marketplace_rules::TokenMarketplaceRules::V0(ref mp) => { + assert_eq!(mp.trade_mode(), &TokenTradeMode::NotTradeable); + } + } + } + + #[test] + fn marketplace_trade_mode_control_and_admin_set_correctly() { + let mut c = base(); + c.apply_token_configuration_item( + TokenConfigurationChangeItem::MarketplaceTradeModeControlGroup( + AuthorizedActionTakers::ContractOwner, + ), + ); + c.apply_token_configuration_item( + TokenConfigurationChangeItem::MarketplaceTradeModeAdminGroup( + AuthorizedActionTakers::MainGroup, + ), + ); + assert_eq!( + c.marketplace_rules + .trade_mode_change_rules() + .authorized_to_make_change_action_takers(), + &AuthorizedActionTakers::ContractOwner + ); + assert_eq!( + c.marketplace_rules + .trade_mode_change_rules() + .admin_action_takers(), + &AuthorizedActionTakers::MainGroup + ); + } +} diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/authorized_action_takers_for_configuration_item/v0/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/authorized_action_takers_for_configuration_item/v0/mod.rs index 889c2990951..1baae9a924f 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/authorized_action_takers_for_configuration_item/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/authorized_action_takers_for_configuration_item/v0/mod.rs @@ -121,3 +121,229 @@ impl TokenConfigurationV0 { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::data_contract::associated_token::token_configuration::accessors::v0::{ + TokenConfigurationV0Getters, TokenConfigurationV0Setters, + }; + use crate::data_contract::associated_token::token_configuration_convention::v0::TokenConfigurationConventionV0; + use crate::data_contract::associated_token::token_configuration_convention::TokenConfigurationConvention; + use crate::data_contract::associated_token::token_marketplace_rules::v0::TokenTradeMode; + use crate::data_contract::change_control_rules::v0::ChangeControlRulesV0; + use crate::data_contract::change_control_rules::ChangeControlRules; + use platform_value::Identifier; + + fn config_with_all_owner_rules() -> TokenConfigurationV0 { + let mut c = TokenConfigurationV0::default_most_restrictive(); + // Assign each rule's authorized_to_make_change / admin_action_takers to a + // distinguishable value so we can verify dispatch. + let auth = AuthorizedActionTakers::ContractOwner; + let admin = AuthorizedActionTakers::Identity(Identifier::from([9u8; 32])); + let rules = ChangeControlRules::V0(ChangeControlRulesV0 { + authorized_to_make_change: auth, + admin_action_takers: admin, + changing_authorized_action_takers_to_no_one_allowed: true, + changing_admin_action_takers_to_no_one_allowed: true, + self_changing_admin_action_takers_allowed: true, + }); + c.set_conventions_change_rules(rules.clone()); + c.set_max_supply_change_rules(rules.clone()); + c.set_manual_minting_rules(rules.clone()); + c.set_manual_burning_rules(rules.clone()); + c.set_freeze_rules(rules.clone()); + c.set_unfreeze_rules(rules.clone()); + c.set_destroy_frozen_funds_rules(rules.clone()); + c.set_emergency_action_rules(rules.clone()); + c + } + + #[test] + fn no_change_returns_no_one() { + let c = TokenConfigurationV0::default_most_restrictive(); + let result = c.authorized_action_takers_for_configuration_item( + &TokenConfigurationChangeItem::TokenConfigurationNoChange, + ); + assert_eq!(result, AuthorizedActionTakers::NoOne); + } + + #[test] + fn conventions_item_returns_authorized_to_make_change() { + let c = config_with_all_owner_rules(); + let conv = TokenConfigurationConvention::V0(TokenConfigurationConventionV0 { + localizations: Default::default(), + decimals: 8, + }); + let result = c.authorized_action_takers_for_configuration_item( + &TokenConfigurationChangeItem::Conventions(conv), + ); + assert_eq!(result, AuthorizedActionTakers::ContractOwner); + } + + #[test] + fn conventions_control_group_returns_admin_action_takers() { + let c = config_with_all_owner_rules(); + let result = c.authorized_action_takers_for_configuration_item( + &TokenConfigurationChangeItem::ConventionsControlGroup(AuthorizedActionTakers::NoOne), + ); + // conventions_change_rules.admin_action_takers = Identity(9) + assert_eq!( + result, + AuthorizedActionTakers::Identity(Identifier::from([9u8; 32])) + ); + } + + #[test] + fn conventions_admin_group_returns_admin_action_takers() { + let c = config_with_all_owner_rules(); + let result = c.authorized_action_takers_for_configuration_item( + &TokenConfigurationChangeItem::ConventionsAdminGroup(AuthorizedActionTakers::NoOne), + ); + assert_eq!( + result, + AuthorizedActionTakers::Identity(Identifier::from([9u8; 32])) + ); + } + + #[test] + fn max_supply_returns_authorized_to_make_change() { + let c = config_with_all_owner_rules(); + let result = c.authorized_action_takers_for_configuration_item( + &TokenConfigurationChangeItem::MaxSupply(Some(42)), + ); + assert_eq!(result, AuthorizedActionTakers::ContractOwner); + } + + #[test] + fn max_supply_control_and_admin_return_admin() { + let c = config_with_all_owner_rules(); + let expected = AuthorizedActionTakers::Identity(Identifier::from([9u8; 32])); + let r1 = c.authorized_action_takers_for_configuration_item( + &TokenConfigurationChangeItem::MaxSupplyControlGroup(AuthorizedActionTakers::NoOne), + ); + let r2 = c.authorized_action_takers_for_configuration_item( + &TokenConfigurationChangeItem::MaxSupplyAdminGroup(AuthorizedActionTakers::NoOne), + ); + assert_eq!(r1, expected); + assert_eq!(r2, expected); + } + + #[test] + fn manual_minting_returns_admin_not_authorized() { + // Note: ManualMinting in authorized_action_takers_for_configuration_item + // returns admin_action_takers (not authorized_to_make_change) per the + // implementation. Ensure that dispatch is correct. + let c = config_with_all_owner_rules(); + let result = c.authorized_action_takers_for_configuration_item( + &TokenConfigurationChangeItem::ManualMinting(AuthorizedActionTakers::NoOne), + ); + assert_eq!( + result, + AuthorizedActionTakers::Identity(Identifier::from([9u8; 32])) + ); + } + + #[test] + fn freeze_returns_admin_action_takers() { + let c = config_with_all_owner_rules(); + let result = c.authorized_action_takers_for_configuration_item( + &TokenConfigurationChangeItem::Freeze(AuthorizedActionTakers::NoOne), + ); + assert_eq!( + result, + AuthorizedActionTakers::Identity(Identifier::from([9u8; 32])) + ); + } + + #[test] + fn destroy_frozen_funds_returns_admin_action_takers() { + let c = config_with_all_owner_rules(); + let result = c.authorized_action_takers_for_configuration_item( + &TokenConfigurationChangeItem::DestroyFrozenFunds(AuthorizedActionTakers::NoOne), + ); + assert_eq!( + result, + AuthorizedActionTakers::Identity(Identifier::from([9u8; 32])) + ); + } + + #[test] + fn main_control_group_always_returns_no_one() { + // Per implementation, MainControlGroup change items always return NoOne, + // regardless of config. This is important because modifying the main + // control group is governed by main_control_group_can_be_modified, not + // by any of the change_control_rules. + let c = config_with_all_owner_rules(); + let result = c.authorized_action_takers_for_configuration_item( + &TokenConfigurationChangeItem::MainControlGroup(Some(3)), + ); + assert_eq!(result, AuthorizedActionTakers::NoOne); + } + + #[test] + fn marketplace_trade_mode_returns_authorized_to_make_change() { + let c = TokenConfigurationV0::default_most_restrictive(); + let result = c.authorized_action_takers_for_configuration_item( + &TokenConfigurationChangeItem::MarketplaceTradeMode(TokenTradeMode::NotTradeable), + ); + // default rules are NoOne + assert_eq!(result, AuthorizedActionTakers::NoOne); + } + + #[test] + fn perpetual_distribution_returns_authorized_to_make_change() { + let c = TokenConfigurationV0::default_most_restrictive(); + let r = c.authorized_action_takers_for_configuration_item( + &TokenConfigurationChangeItem::PerpetualDistribution(None), + ); + // default distribution rules authorized_to_make_change is NoOne + assert_eq!(r, AuthorizedActionTakers::NoOne); + } + + #[test] + fn new_tokens_destination_identity_returns_authorized_to_make_change() { + let c = TokenConfigurationV0::default_most_restrictive(); + let r = c.authorized_action_takers_for_configuration_item( + &TokenConfigurationChangeItem::NewTokensDestinationIdentity(None), + ); + assert_eq!(r, AuthorizedActionTakers::NoOne); + } + + #[test] + fn minting_allow_choosing_destination_returns_authorized_to_make_change() { + let c = TokenConfigurationV0::default_most_restrictive(); + let r = c.authorized_action_takers_for_configuration_item( + &TokenConfigurationChangeItem::MintingAllowChoosingDestination(true), + ); + assert_eq!(r, AuthorizedActionTakers::NoOne); + } + + #[test] + fn unfreeze_and_admin_both_return_admin() { + let c = config_with_all_owner_rules(); + let r1 = c.authorized_action_takers_for_configuration_item( + &TokenConfigurationChangeItem::Unfreeze(AuthorizedActionTakers::NoOne), + ); + let r2 = c.authorized_action_takers_for_configuration_item( + &TokenConfigurationChangeItem::UnfreezeAdminGroup(AuthorizedActionTakers::NoOne), + ); + let expected = AuthorizedActionTakers::Identity(Identifier::from([9u8; 32])); + assert_eq!(r1, expected); + assert_eq!(r2, expected); + } + + #[test] + fn emergency_action_and_admin_both_return_admin() { + let c = config_with_all_owner_rules(); + let r1 = c.authorized_action_takers_for_configuration_item( + &TokenConfigurationChangeItem::EmergencyAction(AuthorizedActionTakers::NoOne), + ); + let r2 = c.authorized_action_takers_for_configuration_item( + &TokenConfigurationChangeItem::EmergencyActionAdminGroup(AuthorizedActionTakers::NoOne), + ); + let expected = AuthorizedActionTakers::Identity(Identifier::from([9u8; 32])); + assert_eq!(r1, expected); + assert_eq!(r2, expected); + } +} diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/can_apply_token_configuration_item/v0/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/can_apply_token_configuration_item/v0/mod.rs index c45fee375e6..202c055cb2a 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/can_apply_token_configuration_item/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/can_apply_token_configuration_item/v0/mod.rs @@ -320,3 +320,331 @@ impl TokenConfigurationV0 { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::data_contract::associated_token::token_configuration::accessors::v0::{ + TokenConfigurationV0Getters, TokenConfigurationV0Setters, + }; + use crate::data_contract::associated_token::token_configuration_convention::v0::TokenConfigurationConventionV0; + use crate::data_contract::associated_token::token_configuration_convention::TokenConfigurationConvention; + use crate::data_contract::associated_token::token_marketplace_rules::v0::TokenTradeMode; + use crate::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; + use crate::data_contract::change_control_rules::v0::ChangeControlRulesV0; + use crate::data_contract::change_control_rules::ChangeControlRules; + + fn config_owner_can_change_everything() -> TokenConfigurationV0 { + let mut c = TokenConfigurationV0::default_most_restrictive(); + let owner_rules = ChangeControlRules::V0(ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::ContractOwner, + admin_action_takers: AuthorizedActionTakers::ContractOwner, + changing_authorized_action_takers_to_no_one_allowed: true, + changing_admin_action_takers_to_no_one_allowed: true, + self_changing_admin_action_takers_allowed: true, + }); + c.set_conventions_change_rules(owner_rules.clone()); + c.set_max_supply_change_rules(owner_rules.clone()); + c.set_manual_minting_rules(owner_rules.clone()); + c.set_manual_burning_rules(owner_rules.clone()); + c.set_freeze_rules(owner_rules.clone()); + c.set_unfreeze_rules(owner_rules.clone()); + c.set_destroy_frozen_funds_rules(owner_rules.clone()); + c.set_emergency_action_rules(owner_rules.clone()); + c.set_main_control_group_can_be_modified(AuthorizedActionTakers::ContractOwner); + c + } + + #[test] + fn no_change_always_returns_false() { + let c = TokenConfigurationV0::default_most_restrictive(); + let owner = Identifier::random(); + let taker = ActionTaker::SingleIdentity(owner); + let can = c.can_apply_token_configuration_item( + &TokenConfigurationChangeItem::TokenConfigurationNoChange, + &owner, + None, + &BTreeMap::new(), + &taker, + ActionGoal::ActionCompletion, + ); + assert!(!can); + } + + #[test] + fn conventions_item_allowed_when_owner_authorized() { + let c = config_owner_can_change_everything(); + let owner = Identifier::random(); + let taker = ActionTaker::SingleIdentity(owner); + let conv = TokenConfigurationConvention::V0(TokenConfigurationConventionV0 { + localizations: Default::default(), + decimals: 4, + }); + let can = c.can_apply_token_configuration_item( + &TokenConfigurationChangeItem::Conventions(conv), + &owner, + None, + &BTreeMap::new(), + &taker, + ActionGoal::ActionCompletion, + ); + assert!(can); + } + + #[test] + fn conventions_item_denied_for_non_owner() { + let c = config_owner_can_change_everything(); + let owner = Identifier::random(); + let non_owner = Identifier::random(); + let taker = ActionTaker::SingleIdentity(non_owner); + let conv = TokenConfigurationConvention::V0(TokenConfigurationConventionV0 { + localizations: Default::default(), + decimals: 4, + }); + let can = c.can_apply_token_configuration_item( + &TokenConfigurationChangeItem::Conventions(conv), + &owner, + None, + &BTreeMap::new(), + &taker, + ActionGoal::ActionCompletion, + ); + assert!(!can); + } + + #[test] + fn conventions_control_group_allowed_when_owner_admin() { + let c = config_owner_can_change_everything(); + let owner = Identifier::random(); + let taker = ActionTaker::SingleIdentity(owner); + let can = c.can_apply_token_configuration_item( + &TokenConfigurationChangeItem::ConventionsControlGroup( + AuthorizedActionTakers::ContractOwner, + ), + &owner, + None, + &BTreeMap::new(), + &taker, + ActionGoal::ActionCompletion, + ); + assert!(can); + } + + #[test] + fn conventions_admin_group_allowed_when_owner_admin() { + let c = config_owner_can_change_everything(); + let owner = Identifier::random(); + let taker = ActionTaker::SingleIdentity(owner); + let can = c.can_apply_token_configuration_item( + &TokenConfigurationChangeItem::ConventionsAdminGroup( + AuthorizedActionTakers::ContractOwner, + ), + &owner, + None, + &BTreeMap::new(), + &taker, + ActionGoal::ActionCompletion, + ); + assert!(can); + } + + #[test] + fn max_supply_denied_for_default_no_one_rules() { + let c = TokenConfigurationV0::default_most_restrictive(); + let owner = Identifier::random(); + let taker = ActionTaker::SingleIdentity(owner); + let can = c.can_apply_token_configuration_item( + &TokenConfigurationChangeItem::MaxSupply(Some(1)), + &owner, + None, + &BTreeMap::new(), + &taker, + ActionGoal::ActionCompletion, + ); + assert!(!can); + } + + #[test] + fn manual_minting_allowed_when_admin_action_takers_match() { + let c = config_owner_can_change_everything(); + let owner = Identifier::random(); + let taker = ActionTaker::SingleIdentity(owner); + let can = c.can_apply_token_configuration_item( + &TokenConfigurationChangeItem::ManualMinting(AuthorizedActionTakers::ContractOwner), + &owner, + None, + &BTreeMap::new(), + &taker, + ActionGoal::ActionCompletion, + ); + assert!(can); + } + + #[test] + fn manual_minting_admin_group_allowed_when_self_change_allowed() { + let c = config_owner_can_change_everything(); + let owner = Identifier::random(); + let taker = ActionTaker::SingleIdentity(owner); + let can = c.can_apply_token_configuration_item( + &TokenConfigurationChangeItem::ManualMintingAdminGroup( + AuthorizedActionTakers::ContractOwner, + ), + &owner, + None, + &BTreeMap::new(), + &taker, + ActionGoal::ActionCompletion, + ); + assert!(can); + } + + #[test] + fn manual_minting_admin_group_denied_when_self_change_not_allowed() { + let mut c = config_owner_can_change_everything(); + let rules = ChangeControlRules::V0(ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::ContractOwner, + admin_action_takers: AuthorizedActionTakers::ContractOwner, + changing_authorized_action_takers_to_no_one_allowed: true, + changing_admin_action_takers_to_no_one_allowed: true, + self_changing_admin_action_takers_allowed: false, + }); + c.set_manual_minting_rules(rules); + let owner = Identifier::random(); + let taker = ActionTaker::SingleIdentity(owner); + let can = c.can_apply_token_configuration_item( + &TokenConfigurationChangeItem::ManualMintingAdminGroup( + AuthorizedActionTakers::ContractOwner, + ), + &owner, + None, + &BTreeMap::new(), + &taker, + ActionGoal::ActionCompletion, + ); + assert!(!can); + } + + #[test] + fn freeze_unfreeze_destroy_emergency_all_allowed_when_owner_admin() { + let c = config_owner_can_change_everything(); + let owner = Identifier::random(); + let taker = ActionTaker::SingleIdentity(owner); + let items = vec![ + TokenConfigurationChangeItem::Freeze(AuthorizedActionTakers::ContractOwner), + TokenConfigurationChangeItem::Unfreeze(AuthorizedActionTakers::ContractOwner), + TokenConfigurationChangeItem::DestroyFrozenFunds(AuthorizedActionTakers::ContractOwner), + TokenConfigurationChangeItem::EmergencyAction(AuthorizedActionTakers::ContractOwner), + ]; + for item in items { + assert!( + c.can_apply_token_configuration_item( + &item, + &owner, + None, + &BTreeMap::new(), + &taker, + ActionGoal::ActionCompletion, + ), + "expected can_apply true for {:?}", + item + ); + } + } + + #[test] + fn main_control_group_allowed_when_owner_can_modify() { + let c = config_owner_can_change_everything(); + let owner = Identifier::random(); + let taker = ActionTaker::SingleIdentity(owner); + let can = c.can_apply_token_configuration_item( + &TokenConfigurationChangeItem::MainControlGroup(Some(5)), + &owner, + None, + &BTreeMap::new(), + &taker, + ActionGoal::ActionCompletion, + ); + assert!(can); + } + + #[test] + fn main_control_group_denied_by_default() { + // default main_control_group_can_be_modified = NoOne + let c = TokenConfigurationV0::default_most_restrictive(); + let owner = Identifier::random(); + let taker = ActionTaker::SingleIdentity(owner); + let can = c.can_apply_token_configuration_item( + &TokenConfigurationChangeItem::MainControlGroup(Some(5)), + &owner, + None, + &BTreeMap::new(), + &taker, + ActionGoal::ActionCompletion, + ); + assert!(!can); + } + + #[test] + fn marketplace_trade_mode_denied_by_default() { + let c = TokenConfigurationV0::default_most_restrictive(); + let owner = Identifier::random(); + let taker = ActionTaker::SingleIdentity(owner); + let can = c.can_apply_token_configuration_item( + &TokenConfigurationChangeItem::MarketplaceTradeMode(TokenTradeMode::NotTradeable), + &owner, + None, + &BTreeMap::new(), + &taker, + ActionGoal::ActionCompletion, + ); + assert!(!can); + } + + #[test] + fn perpetual_distribution_and_its_admin_denied_by_default() { + let c = TokenConfigurationV0::default_most_restrictive(); + let owner = Identifier::random(); + let taker = ActionTaker::SingleIdentity(owner); + assert!(!c.can_apply_token_configuration_item( + &TokenConfigurationChangeItem::PerpetualDistribution(None), + &owner, + None, + &BTreeMap::new(), + &taker, + ActionGoal::ActionCompletion, + )); + assert!(!c.can_apply_token_configuration_item( + &TokenConfigurationChangeItem::PerpetualDistributionAdminGroup( + AuthorizedActionTakers::ContractOwner, + ), + &owner, + None, + &BTreeMap::new(), + &taker, + ActionGoal::ActionCompletion, + )); + } + + #[test] + fn new_tokens_destination_and_minting_choose_destination_denied_by_default() { + let c = TokenConfigurationV0::default_most_restrictive(); + let owner = Identifier::random(); + let taker = ActionTaker::SingleIdentity(owner); + assert!(!c.can_apply_token_configuration_item( + &TokenConfigurationChangeItem::NewTokensDestinationIdentity(None), + &owner, + None, + &BTreeMap::new(), + &taker, + ActionGoal::ActionCompletion, + )); + assert!(!c.can_apply_token_configuration_item( + &TokenConfigurationChangeItem::MintingAllowChoosingDestination(true), + &owner, + None, + &BTreeMap::new(), + &taker, + ActionGoal::ActionCompletion, + )); + } +} diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_groups_exist/v0/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_groups_exist/v0/mod.rs index e20e3a9639d..df150a4d4b1 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_groups_exist/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_groups_exist/v0/mod.rs @@ -48,3 +48,148 @@ impl TokenConfiguration { validation_result } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::consensus::basic::BasicError; + use crate::consensus::ConsensusError; + use crate::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Setters; + use crate::data_contract::associated_token::token_configuration::v0::TokenConfigurationV0; + use crate::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; + use crate::data_contract::change_control_rules::v0::ChangeControlRulesV0; + use crate::data_contract::change_control_rules::ChangeControlRules; + use crate::data_contract::group::v0::GroupV0; + use platform_value::Identifier; + + fn make_group(member_byte: u8) -> Group { + let mut members = std::collections::BTreeMap::new(); + members.insert(Identifier::from([member_byte; 32]), 1u32); + Group::V0(GroupV0 { + members, + required_power: 1, + }) + } + + fn config_with_group_rule(position: GroupContractPosition) -> TokenConfiguration { + let mut v0 = TokenConfigurationV0::default_most_restrictive(); + v0.set_freeze_rules(ChangeControlRules::V0(ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::Group(position), + admin_action_takers: AuthorizedActionTakers::NoOne, + changing_authorized_action_takers_to_no_one_allowed: false, + changing_admin_action_takers_to_no_one_allowed: false, + self_changing_admin_action_takers_allowed: false, + })); + TokenConfiguration::V0(v0) + } + + #[test] + fn empty_config_with_no_groups_validates() { + let config = TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); + let groups: BTreeMap = BTreeMap::new(); + let result = config.validate_token_config_groups_exist_v0(&groups); + assert!( + result.is_valid(), + "expected valid, got errors {:?}", + result.errors + ); + } + + #[test] + fn missing_referenced_group_produces_group_position_error() { + let config = config_with_group_rule(42); + let groups: BTreeMap = BTreeMap::new(); + let result = config.validate_token_config_groups_exist_v0(&groups); + assert!(!result.is_valid()); + assert_eq!(result.errors.len(), 1); + match result.errors.first() { + Some(ConsensusError::BasicError(BasicError::GroupPositionDoesNotExistError(_))) => {} + other => panic!("unexpected error: {:?}", other), + } + } + + #[test] + fn referenced_group_present_validates() { + let config = config_with_group_rule(7); + let mut groups: BTreeMap = BTreeMap::new(); + groups.insert(7, make_group(1)); + let result = config.validate_token_config_groups_exist_v0(&groups); + assert!( + result.is_valid(), + "expected valid, got errors {:?}", + result.errors + ); + } + + #[test] + fn main_group_used_but_main_control_group_not_set_produces_main_group_not_defined() { + let mut v0 = TokenConfigurationV0::default_most_restrictive(); + v0.set_freeze_rules(ChangeControlRules::V0(ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::MainGroup, + admin_action_takers: AuthorizedActionTakers::NoOne, + changing_authorized_action_takers_to_no_one_allowed: false, + changing_admin_action_takers_to_no_one_allowed: false, + self_changing_admin_action_takers_allowed: false, + })); + // Leave main_control_group = None + let config = TokenConfiguration::V0(v0); + let groups: BTreeMap = BTreeMap::new(); + let result = config.validate_token_config_groups_exist_v0(&groups); + assert!(!result.is_valid()); + match result.errors.first() { + Some(ConsensusError::BasicError(BasicError::MainGroupIsNotDefinedError(_))) => {} + other => panic!("unexpected error: {:?}", other), + } + } + + #[test] + fn main_control_group_set_but_not_in_groups_map_errors() { + let mut v0 = TokenConfigurationV0::default_most_restrictive(); + v0.set_main_control_group(Some(99)); + let config = TokenConfiguration::V0(v0); + let groups: BTreeMap = BTreeMap::new(); + let result = config.validate_token_config_groups_exist_v0(&groups); + assert!(!result.is_valid()); + match result.errors.first() { + Some(ConsensusError::BasicError(BasicError::GroupPositionDoesNotExistError(_))) => {} + other => panic!("unexpected error: {:?}", other), + } + } + + #[test] + fn main_control_group_set_and_in_groups_map_validates() { + let mut v0 = TokenConfigurationV0::default_most_restrictive(); + v0.set_main_control_group(Some(3)); + let config = TokenConfiguration::V0(v0); + let mut groups: BTreeMap = BTreeMap::new(); + groups.insert(3, make_group(2)); + let result = config.validate_token_config_groups_exist_v0(&groups); + assert!( + result.is_valid(), + "expected valid, got errors {:?}", + result.errors + ); + } + + #[test] + fn main_group_referenced_and_main_control_group_set_and_present_validates() { + let mut v0 = TokenConfigurationV0::default_most_restrictive(); + v0.set_main_control_group(Some(5)); + v0.set_freeze_rules(ChangeControlRules::V0(ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::MainGroup, + admin_action_takers: AuthorizedActionTakers::NoOne, + changing_authorized_action_takers_to_no_one_allowed: false, + changing_admin_action_takers_to_no_one_allowed: false, + self_changing_admin_action_takers_allowed: false, + })); + let config = TokenConfiguration::V0(v0); + let mut groups: BTreeMap = BTreeMap::new(); + groups.insert(5, make_group(4)); + let result = config.validate_token_config_groups_exist_v0(&groups); + assert!( + result.is_valid(), + "expected valid, got errors {:?}", + result.errors + ); + } +} diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v0/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v0/mod.rs index 3a163be7eea..2d759f669aa 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v0/mod.rs @@ -507,3 +507,547 @@ impl TokenConfigurationV0 { self } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::data_contract::associated_token::token_configuration::accessors::v0::{ + TokenConfigurationV0Getters, TokenConfigurationV0Setters, + }; + use platform_value::Identifier; + + fn preset( + features: TokenConfigurationPresetFeatures, + action_taker: AuthorizedActionTakers, + ) -> TokenConfigurationPreset { + TokenConfigurationPreset { + features, + action_taker, + } + } + + // --- default_main_control_group_can_be_modified --- + + #[test] + fn preset_main_control_group_can_be_modified_most_restrictive_is_no_one() { + let p = preset( + TokenConfigurationPresetFeatures::MostRestrictive, + AuthorizedActionTakers::ContractOwner, + ); + assert_eq!( + p.default_main_control_group_can_be_modified(), + AuthorizedActionTakers::NoOne + ); + } + + #[test] + fn preset_main_control_group_can_be_modified_only_emergency_is_no_one() { + let p = preset( + TokenConfigurationPresetFeatures::WithOnlyEmergencyAction, + AuthorizedActionTakers::ContractOwner, + ); + assert_eq!( + p.default_main_control_group_can_be_modified(), + AuthorizedActionTakers::NoOne + ); + } + + #[test] + fn preset_main_control_group_can_be_modified_minting_burning_is_no_one() { + let p = preset( + TokenConfigurationPresetFeatures::WithMintingAndBurningActions, + AuthorizedActionTakers::ContractOwner, + ); + assert_eq!( + p.default_main_control_group_can_be_modified(), + AuthorizedActionTakers::NoOne + ); + } + + #[test] + fn preset_main_control_group_can_be_modified_advanced_is_no_one() { + let p = preset( + TokenConfigurationPresetFeatures::WithAllAdvancedActions, + AuthorizedActionTakers::ContractOwner, + ); + assert_eq!( + p.default_main_control_group_can_be_modified(), + AuthorizedActionTakers::NoOne + ); + } + + #[test] + fn preset_main_control_group_can_be_modified_extreme_is_action_taker() { + let taker = AuthorizedActionTakers::Identity(Identifier::from([9u8; 32])); + let p = preset(TokenConfigurationPresetFeatures::WithExtremeActions, taker); + assert_eq!(p.default_main_control_group_can_be_modified(), taker); + } + + // --- default_basic_change_control_rules_v0 --- + + #[test] + fn preset_basic_rules_most_restrictive_is_no_one_locked() { + let p = preset( + TokenConfigurationPresetFeatures::MostRestrictive, + AuthorizedActionTakers::ContractOwner, + ); + let rules = p.default_basic_change_control_rules_v0(); + assert_eq!( + rules.authorized_to_make_change, + AuthorizedActionTakers::NoOne + ); + assert_eq!(rules.admin_action_takers, AuthorizedActionTakers::NoOne); + assert!(!rules.changing_authorized_action_takers_to_no_one_allowed); + assert!(!rules.changing_admin_action_takers_to_no_one_allowed); + assert!(!rules.self_changing_admin_action_takers_allowed); + } + + #[test] + fn preset_basic_rules_only_emergency_is_no_one_locked() { + let p = preset( + TokenConfigurationPresetFeatures::WithOnlyEmergencyAction, + AuthorizedActionTakers::ContractOwner, + ); + let rules = p.default_basic_change_control_rules_v0(); + assert_eq!( + rules.authorized_to_make_change, + AuthorizedActionTakers::NoOne + ); + } + + #[test] + fn preset_basic_rules_minting_burning_is_action_taker_self_mutable() { + let taker = AuthorizedActionTakers::ContractOwner; + let p = preset( + TokenConfigurationPresetFeatures::WithMintingAndBurningActions, + taker, + ); + let rules = p.default_basic_change_control_rules_v0(); + assert_eq!(rules.authorized_to_make_change, taker); + assert_eq!(rules.admin_action_takers, taker); + assert!(rules.self_changing_admin_action_takers_allowed); + // but not to no-one + assert!(!rules.changing_authorized_action_takers_to_no_one_allowed); + } + + #[test] + fn preset_basic_rules_advanced_is_action_taker_self_mutable() { + let taker = AuthorizedActionTakers::ContractOwner; + let p = preset( + TokenConfigurationPresetFeatures::WithAllAdvancedActions, + taker, + ); + let rules = p.default_basic_change_control_rules_v0(); + assert_eq!(rules.authorized_to_make_change, taker); + assert!(rules.self_changing_admin_action_takers_allowed); + assert!(!rules.changing_admin_action_takers_to_no_one_allowed); + } + + #[test] + fn preset_basic_rules_extreme_allows_no_one_transitions() { + let taker = AuthorizedActionTakers::ContractOwner; + let p = preset(TokenConfigurationPresetFeatures::WithExtremeActions, taker); + let rules = p.default_basic_change_control_rules_v0(); + assert_eq!(rules.authorized_to_make_change, taker); + assert!(rules.changing_authorized_action_takers_to_no_one_allowed); + assert!(rules.changing_admin_action_takers_to_no_one_allowed); + assert!(rules.self_changing_admin_action_takers_allowed); + } + + // --- default_advanced_change_control_rules_v0 --- + + #[test] + fn preset_advanced_rules_most_restrictive_is_locked() { + let p = preset( + TokenConfigurationPresetFeatures::MostRestrictive, + AuthorizedActionTakers::ContractOwner, + ); + let rules = p.default_advanced_change_control_rules_v0(); + assert_eq!( + rules.authorized_to_make_change, + AuthorizedActionTakers::NoOne + ); + assert!(!rules.self_changing_admin_action_takers_allowed); + } + + #[test] + fn preset_advanced_rules_minting_burning_is_locked() { + let p = preset( + TokenConfigurationPresetFeatures::WithMintingAndBurningActions, + AuthorizedActionTakers::ContractOwner, + ); + // Minting/burning does NOT open up advanced operations -> advanced remains NoOne + let rules = p.default_advanced_change_control_rules_v0(); + assert_eq!( + rules.authorized_to_make_change, + AuthorizedActionTakers::NoOne + ); + assert_eq!(rules.admin_action_takers, AuthorizedActionTakers::NoOne); + } + + #[test] + fn preset_advanced_rules_only_emergency_is_locked() { + let p = preset( + TokenConfigurationPresetFeatures::WithOnlyEmergencyAction, + AuthorizedActionTakers::ContractOwner, + ); + let rules = p.default_advanced_change_control_rules_v0(); + assert_eq!( + rules.authorized_to_make_change, + AuthorizedActionTakers::NoOne + ); + } + + #[test] + fn preset_advanced_rules_advanced_allows_action_taker() { + let taker = AuthorizedActionTakers::ContractOwner; + let p = preset( + TokenConfigurationPresetFeatures::WithAllAdvancedActions, + taker, + ); + let rules = p.default_advanced_change_control_rules_v0(); + assert_eq!(rules.authorized_to_make_change, taker); + assert!(rules.self_changing_admin_action_takers_allowed); + assert!(!rules.changing_authorized_action_takers_to_no_one_allowed); + } + + #[test] + fn preset_advanced_rules_extreme_allows_everything() { + let taker = AuthorizedActionTakers::ContractOwner; + let p = preset(TokenConfigurationPresetFeatures::WithExtremeActions, taker); + let rules = p.default_advanced_change_control_rules_v0(); + assert!(rules.changing_authorized_action_takers_to_no_one_allowed); + assert!(rules.changing_admin_action_takers_to_no_one_allowed); + assert!(rules.self_changing_admin_action_takers_allowed); + } + + // --- default_emergency_action_change_control_rules_v0 --- + + #[test] + fn preset_emergency_rules_most_restrictive_is_no_one() { + let p = preset( + TokenConfigurationPresetFeatures::MostRestrictive, + AuthorizedActionTakers::ContractOwner, + ); + let rules = p.default_emergency_action_change_control_rules_v0(); + assert_eq!( + rules.authorized_to_make_change, + AuthorizedActionTakers::NoOne + ); + } + + #[test] + fn preset_emergency_rules_only_emergency_allows_action_taker() { + let taker = AuthorizedActionTakers::ContractOwner; + let p = preset( + TokenConfigurationPresetFeatures::WithOnlyEmergencyAction, + taker, + ); + let rules = p.default_emergency_action_change_control_rules_v0(); + assert_eq!(rules.authorized_to_make_change, taker); + assert!(rules.self_changing_admin_action_takers_allowed); + } + + #[test] + fn preset_emergency_rules_minting_burning_allows_action_taker() { + let taker = AuthorizedActionTakers::ContractOwner; + let p = preset( + TokenConfigurationPresetFeatures::WithMintingAndBurningActions, + taker, + ); + let rules = p.default_emergency_action_change_control_rules_v0(); + assert_eq!(rules.authorized_to_make_change, taker); + assert!(rules.self_changing_admin_action_takers_allowed); + } + + #[test] + fn preset_emergency_rules_advanced_allows_action_taker() { + let taker = AuthorizedActionTakers::ContractOwner; + let p = preset( + TokenConfigurationPresetFeatures::WithAllAdvancedActions, + taker, + ); + let rules = p.default_emergency_action_change_control_rules_v0(); + assert_eq!(rules.authorized_to_make_change, taker); + } + + #[test] + fn preset_emergency_rules_extreme_allows_no_one_transitions() { + let taker = AuthorizedActionTakers::ContractOwner; + let p = preset(TokenConfigurationPresetFeatures::WithExtremeActions, taker); + let rules = p.default_emergency_action_change_control_rules_v0(); + assert!(rules.changing_authorized_action_takers_to_no_one_allowed); + } + + // --- default_distribution_rules_v0 with/without direct pricing --- + + #[test] + fn preset_distribution_rules_with_direct_pricing_uses_basic_rules() { + let taker = AuthorizedActionTakers::ContractOwner; + let p = preset(TokenConfigurationPresetFeatures::WithExtremeActions, taker); + let rules = p.default_distribution_rules_v0(None, None, true); + // With direct pricing enabled, the rules match basic (extreme -> owner, all permissive) + assert_eq!( + rules + .change_direct_purchase_pricing_rules + .authorized_to_make_change_action_takers(), + &taker + ); + } + + #[test] + fn preset_distribution_rules_without_direct_pricing_locks_it_down() { + let taker = AuthorizedActionTakers::ContractOwner; + let p = preset(TokenConfigurationPresetFeatures::WithExtremeActions, taker); + let rules = p.default_distribution_rules_v0(None, None, false); + // Without direct pricing, change_direct_purchase_pricing_rules is hard-coded to NoOne + assert_eq!( + rules + .change_direct_purchase_pricing_rules + .authorized_to_make_change_action_takers(), + &AuthorizedActionTakers::NoOne + ); + } + + #[test] + fn preset_distribution_rules_minting_choosing_destination_defaults_true() { + let p = preset( + TokenConfigurationPresetFeatures::MostRestrictive, + AuthorizedActionTakers::NoOne, + ); + let rules = p.default_distribution_rules_v0(None, None, false); + assert!(rules.minting_allow_choosing_destination); + assert!(rules.new_tokens_destination_identity.is_none()); + assert!(rules.perpetual_distribution.is_none()); + assert!(rules.pre_programmed_distribution.is_none()); + } + + // --- default_marketplace_rules_v0 --- + + #[test] + fn preset_marketplace_rules_default_is_not_tradeable() { + let p = preset( + TokenConfigurationPresetFeatures::MostRestrictive, + AuthorizedActionTakers::NoOne, + ); + let mp = p.default_marketplace_rules_v0(); + assert_eq!(mp.trade_mode, TokenTradeMode::NotTradeable); + } + + // --- token_configuration_v0 full config --- + + #[test] + fn preset_token_configuration_v0_populates_fields() { + let taker = AuthorizedActionTakers::ContractOwner; + let p = preset(TokenConfigurationPresetFeatures::WithExtremeActions, taker); + let conventions = TokenConfigurationConvention::V0(TokenConfigurationConventionV0 { + localizations: Default::default(), + decimals: 4, + }); + let config = p.token_configuration_v0(conventions, 1_000, Some(5_000), true, true); + assert_eq!(config.base_supply, 1_000); + assert_eq!(config.max_supply, Some(5_000)); + assert_eq!( + config + .manual_minting_rules + .authorized_to_make_change_action_takers(), + &taker + ); + // start_as_paused is fixed false by constructor + assert!(!config.start_as_paused); + assert!(config.allow_transfer_to_frozen_balance); + assert_eq!(config.main_control_group, None); + // extreme => main_control_group_can_be_modified becomes taker + assert_eq!(config.main_control_group_can_be_modified, taker); + // description is none + assert!(config.description.is_none()); + } + + #[test] + fn preset_token_configuration_keeps_all_history_true() { + let p = preset( + TokenConfigurationPresetFeatures::MostRestrictive, + AuthorizedActionTakers::NoOne, + ); + let conventions = TokenConfigurationConvention::V0(TokenConfigurationConventionV0 { + localizations: Default::default(), + decimals: 8, + }); + let cfg = p.token_configuration_v0(conventions, 100, None, true, false); + // keeps_history is TokenKeepsHistoryRules::V0; all fields should be true + match &cfg.keeps_history { + TokenKeepsHistoryRules::V0(v0) => { + assert!(v0.keeps_transfer_history); + assert!(v0.keeps_freezing_history); + assert!(v0.keeps_minting_history); + assert!(v0.keeps_burning_history); + assert!(v0.keeps_direct_pricing_history); + assert!(v0.keeps_direct_purchase_history); + } + } + } + + #[test] + fn preset_token_configuration_keeps_all_history_false() { + let p = preset( + TokenConfigurationPresetFeatures::MostRestrictive, + AuthorizedActionTakers::NoOne, + ); + let conventions = TokenConfigurationConvention::V0(TokenConfigurationConventionV0 { + localizations: Default::default(), + decimals: 8, + }); + let cfg = p.token_configuration_v0(conventions, 100, None, false, false); + match &cfg.keeps_history { + TokenKeepsHistoryRules::V0(v0) => { + assert!(!v0.keeps_transfer_history); + assert!(!v0.keeps_direct_purchase_history); + } + } + } + + // --- default_most_restrictive + with_base_supply chaining --- + + #[test] + fn token_configuration_v0_default_most_restrictive_has_no_max_supply() { + let c = TokenConfigurationV0::default_most_restrictive(); + assert_eq!(c.base_supply, 100_000); + assert!(c.max_supply.is_none()); + assert_eq!( + c.main_control_group_can_be_modified, + AuthorizedActionTakers::NoOne + ); + } + + #[test] + fn token_configuration_v0_with_base_supply_overrides_value() { + let c = TokenConfigurationV0::default_most_restrictive().with_base_supply(42); + assert_eq!(c.base_supply, 42); + } + + // --- Display trait --- + + #[test] + fn display_token_configuration_v0_contains_key_fields() { + let c = TokenConfigurationV0::default_most_restrictive(); + let s = format!("{}", c); + assert!(s.contains("TokenConfigurationV0")); + assert!(s.contains("base_supply")); + assert!(s.contains("main_control_group")); + } + + // --- all_used_group_positions: the interesting branches --- + + #[test] + fn all_used_group_positions_empty_when_no_groups_referenced() { + let c = TokenConfigurationV0::default_most_restrictive(); + let (positions, uses_main) = c.all_used_group_positions(); + assert!(positions.is_empty()); + assert!(!uses_main); + } + + #[test] + fn all_used_group_positions_collects_from_group_variant_in_rules() { + let mut c = TokenConfigurationV0::default_most_restrictive(); + c.freeze_rules = ChangeControlRules::V0(ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::Group(7), + admin_action_takers: AuthorizedActionTakers::Group(9), + changing_authorized_action_takers_to_no_one_allowed: false, + changing_admin_action_takers_to_no_one_allowed: false, + self_changing_admin_action_takers_allowed: false, + }); + let (positions, uses_main) = c.all_used_group_positions(); + assert!(positions.contains(&7)); + assert!(positions.contains(&9)); + assert!(!uses_main); + } + + #[test] + fn all_used_group_positions_flags_main_group_usage() { + let mut c = TokenConfigurationV0::default_most_restrictive(); + c.emergency_action_rules = ChangeControlRules::V0(ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::MainGroup, + admin_action_takers: AuthorizedActionTakers::NoOne, + changing_authorized_action_takers_to_no_one_allowed: false, + changing_admin_action_takers_to_no_one_allowed: false, + self_changing_admin_action_takers_allowed: false, + }); + let (_, uses_main) = c.all_used_group_positions(); + assert!(uses_main); + } + + #[test] + fn all_used_group_positions_includes_main_control_group() { + let mut c = TokenConfigurationV0::default_most_restrictive(); + c.main_control_group = Some(42); + let (positions, _) = c.all_used_group_positions(); + assert!(positions.contains(&42)); + } + + #[test] + fn all_used_group_positions_includes_positions_from_main_control_group_can_be_modified() { + let mut c = TokenConfigurationV0::default_most_restrictive(); + c.main_control_group_can_be_modified = AuthorizedActionTakers::Group(11); + let (positions, _) = c.all_used_group_positions(); + assert!(positions.contains(&11)); + } + + #[test] + fn all_used_group_positions_ignores_contract_owner_and_identity_and_no_one() { + let mut c = TokenConfigurationV0::default_most_restrictive(); + c.manual_minting_rules = ChangeControlRules::V0(ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::ContractOwner, + admin_action_takers: AuthorizedActionTakers::Identity(Identifier::from([1u8; 32])), + changing_authorized_action_takers_to_no_one_allowed: false, + changing_admin_action_takers_to_no_one_allowed: false, + self_changing_admin_action_takers_allowed: false, + }); + let (positions, uses_main) = c.all_used_group_positions(); + assert!(positions.is_empty()); + assert!(!uses_main); + } + + // --- all_change_control_rules --- + + #[test] + fn all_change_control_rules_returns_expected_rule_names() { + let c = TokenConfigurationV0::default_most_restrictive(); + let rules = c.all_change_control_rules(); + let names: Vec<&str> = rules.iter().map(|(name, _)| *name).collect(); + assert!(names.contains(&"max_supply_change_rules")); + assert!(names.contains(&"conventions_change_rules")); + assert!(names.contains(&"manual_minting_rules")); + assert!(names.contains(&"manual_burning_rules")); + assert!(names.contains(&"freeze_rules")); + assert!(names.contains(&"unfreeze_rules")); + assert!(names.contains(&"destroy_frozen_funds_rules")); + assert!(names.contains(&"emergency_action_rules")); + assert!(names.contains(&"trade_mode_change_rules")); + // 13 rules total per the implementation + assert_eq!(rules.len(), 13); + } + + // --- setters exercise the right fields --- + + #[test] + fn setters_set_description_max_supply_base_supply_main_control_group() { + let mut c = TokenConfigurationV0::default_most_restrictive(); + c.set_description(Some("my token".to_string())); + c.set_max_supply(Some(999)); + c.set_base_supply(77); + c.set_main_control_group(Some(3)); + c.set_start_as_paused(true); + c.allow_transfer_to_frozen_balance(false); + c.set_main_control_group_can_be_modified(AuthorizedActionTakers::ContractOwner); + assert_eq!(c.description(), &Some("my token".to_string())); + assert_eq!(c.max_supply(), Some(999)); + assert_eq!(c.base_supply(), 77); + assert_eq!(c.main_control_group(), Some(3)); + assert!(c.start_as_paused()); + assert!(!c.is_allowed_transfer_to_frozen_balance()); + assert_eq!( + c.main_control_group_can_be_modified(), + &AuthorizedActionTakers::ContractOwner + ); + } +} diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration_item.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration_item.rs index e7f0ae5652c..7fd3cf1a207 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration_item.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration_item.rs @@ -400,4 +400,365 @@ mod tests { let variants = all_variants(); assert_eq!(variants.len(), 32); } + + // --- payload_serialization --- + + #[test] + fn payload_serialization_no_change_is_none() { + let item = TokenConfigurationChangeItem::TokenConfigurationNoChange; + assert!(item.payload_serialization().unwrap().is_none()); + } + + #[test] + fn payload_serialization_max_supply_none_is_none() { + let item = TokenConfigurationChangeItem::MaxSupply(None); + assert!(item.payload_serialization().unwrap().is_none()); + } + + #[test] + fn payload_serialization_max_supply_some_encodes_be_bytes() { + let amount: u64 = 0x0102_0304_0506_0708; + let item = TokenConfigurationChangeItem::MaxSupply(Some(amount)); + let bytes = item.payload_serialization().unwrap().unwrap(); + assert_eq!(bytes, amount.to_be_bytes().to_vec()); + assert_eq!(bytes.len(), 8); + } + + #[test] + fn payload_serialization_new_tokens_destination_identity_none_is_none() { + let item = TokenConfigurationChangeItem::NewTokensDestinationIdentity(None); + assert!(item.payload_serialization().unwrap().is_none()); + } + + #[test] + fn payload_serialization_new_tokens_destination_identity_some_is_32_bytes() { + let id = Identifier::from([0x77u8; 32]); + let item = TokenConfigurationChangeItem::NewTokensDestinationIdentity(Some(id)); + let bytes = item.payload_serialization().unwrap().unwrap(); + assert_eq!(bytes.len(), 32); + assert_eq!(bytes, vec![0x77u8; 32]); + } + + #[test] + fn payload_serialization_perpetual_distribution_none_is_none() { + let item = TokenConfigurationChangeItem::PerpetualDistribution(None); + assert!(item.payload_serialization().unwrap().is_none()); + } + + #[test] + fn payload_serialization_main_control_group_none_is_none() { + let item = TokenConfigurationChangeItem::MainControlGroup(None); + assert!(item.payload_serialization().unwrap().is_none()); + } + + #[test] + fn payload_serialization_main_control_group_some_is_two_be_bytes() { + let pos: u16 = 0xABCD; + let item = TokenConfigurationChangeItem::MainControlGroup(Some(pos)); + let bytes = item.payload_serialization().unwrap().unwrap(); + assert_eq!(bytes, pos.to_be_bytes().to_vec()); + assert_eq!(bytes.len(), 2); + } + + #[test] + fn payload_serialization_minting_allow_choosing_destination_true() { + let item = TokenConfigurationChangeItem::MintingAllowChoosingDestination(true); + let bytes = item.payload_serialization().unwrap().unwrap(); + assert_eq!(bytes, vec![1]); + } + + #[test] + fn payload_serialization_minting_allow_choosing_destination_false() { + let item = TokenConfigurationChangeItem::MintingAllowChoosingDestination(false); + let bytes = item.payload_serialization().unwrap().unwrap(); + assert_eq!(bytes, vec![0]); + } + + #[test] + fn payload_serialization_authorized_action_takers_variants_use_to_bytes() { + // The big match arm has 23 variants that all serialize via AuthorizedActionTakers::to_bytes + // Sanity-check a few representative variants produce the expected tag bytes. + let aat = AuthorizedActionTakers::ContractOwner; + let expected = aat.to_bytes(); + + let variants = [ + TokenConfigurationChangeItem::ConventionsControlGroup(aat.clone()), + TokenConfigurationChangeItem::ConventionsAdminGroup(aat.clone()), + TokenConfigurationChangeItem::MaxSupplyControlGroup(aat.clone()), + TokenConfigurationChangeItem::MaxSupplyAdminGroup(aat.clone()), + TokenConfigurationChangeItem::PerpetualDistributionControlGroup(aat.clone()), + TokenConfigurationChangeItem::PerpetualDistributionAdminGroup(aat.clone()), + TokenConfigurationChangeItem::NewTokensDestinationIdentityControlGroup(aat.clone()), + TokenConfigurationChangeItem::NewTokensDestinationIdentityAdminGroup(aat.clone()), + TokenConfigurationChangeItem::MintingAllowChoosingDestinationControlGroup(aat.clone()), + TokenConfigurationChangeItem::MintingAllowChoosingDestinationAdminGroup(aat.clone()), + TokenConfigurationChangeItem::ManualMinting(aat.clone()), + TokenConfigurationChangeItem::ManualMintingAdminGroup(aat.clone()), + TokenConfigurationChangeItem::ManualBurning(aat.clone()), + TokenConfigurationChangeItem::ManualBurningAdminGroup(aat.clone()), + TokenConfigurationChangeItem::Freeze(aat.clone()), + TokenConfigurationChangeItem::FreezeAdminGroup(aat.clone()), + TokenConfigurationChangeItem::Unfreeze(aat.clone()), + TokenConfigurationChangeItem::UnfreezeAdminGroup(aat.clone()), + TokenConfigurationChangeItem::DestroyFrozenFunds(aat.clone()), + TokenConfigurationChangeItem::DestroyFrozenFundsAdminGroup(aat.clone()), + TokenConfigurationChangeItem::EmergencyAction(aat.clone()), + TokenConfigurationChangeItem::EmergencyActionAdminGroup(aat.clone()), + TokenConfigurationChangeItem::MarketplaceTradeModeControlGroup(aat.clone()), + TokenConfigurationChangeItem::MarketplaceTradeModeAdminGroup(aat.clone()), + ]; + for v in &variants { + let bytes = v.payload_serialization().unwrap().unwrap(); + assert_eq!(bytes, expected, "mismatch for variant {:?}", v); + } + } + + #[test] + fn payload_serialization_conventions_roundtrips_via_bincode() { + // We don't assert the exact bytes (bincode-dependent) but we assert + // (1) Some(..) is returned and (2) it decodes back to the same convention. + use crate::data_contract::associated_token::token_configuration_convention::v0::TokenConfigurationConventionV0; + let convention = TokenConfigurationConvention::V0(TokenConfigurationConventionV0 { + localizations: Default::default(), + decimals: 5, + }); + let item = TokenConfigurationChangeItem::Conventions(convention.clone()); + let bytes = item.payload_serialization().unwrap().unwrap(); + assert!(!bytes.is_empty()); + let (decoded, _): (TokenConfigurationConvention, _) = + bincode::decode_from_slice(&bytes, bincode::config::standard()).unwrap(); + assert_eq!(decoded, convention); + } + + #[test] + fn payload_serialization_marketplace_trade_mode_produces_bytes() { + let item = TokenConfigurationChangeItem::MarketplaceTradeMode(TokenTradeMode::NotTradeable); + let bytes = item.payload_serialization().unwrap().unwrap(); + assert!(!bytes.is_empty()); + // Roundtrip decode + let (decoded, _): (TokenTradeMode, _) = + bincode::decode_from_slice(&bytes, bincode::config::standard()).unwrap(); + assert_eq!(decoded, TokenTradeMode::NotTradeable); + } + + // --- Display --- + + #[test] + fn display_no_change() { + let s = format!( + "{}", + TokenConfigurationChangeItem::TokenConfigurationNoChange + ); + assert_eq!(s, "No Change in Token Configuration"); + } + + #[test] + fn display_max_supply_some_vs_none() { + let some = format!("{}", TokenConfigurationChangeItem::MaxSupply(Some(500))); + let none = format!("{}", TokenConfigurationChangeItem::MaxSupply(None)); + assert!(some.contains("500")); + assert!(none.contains("No Limit")); + } + + #[test] + fn display_perpetual_distribution_none_uses_none_marker() { + let s = format!( + "{}", + TokenConfigurationChangeItem::PerpetualDistribution(None) + ); + assert!(s.contains("None")); + } + + #[test] + fn display_new_tokens_destination_identity_none_uses_none_marker() { + let s = format!( + "{}", + TokenConfigurationChangeItem::NewTokensDestinationIdentity(None) + ); + assert!(s.contains("None")); + } + + #[test] + fn display_new_tokens_destination_identity_some_includes_id() { + let id = Identifier::from([3u8; 32]); + let s = format!( + "{}", + TokenConfigurationChangeItem::NewTokensDestinationIdentity(Some(id)) + ); + assert!(s.contains("New Tokens Destination Identity")); + } + + #[test] + fn display_main_control_group_none_uses_none_marker() { + let s = format!("{}", TokenConfigurationChangeItem::MainControlGroup(None)); + assert!(s.contains("None")); + } + + #[test] + fn display_main_control_group_some_contains_position() { + let s = format!( + "{}", + TokenConfigurationChangeItem::MainControlGroup(Some(7)) + ); + assert!(s.contains("7")); + } + + #[test] + fn display_minting_allow_choosing_destination_contains_value() { + let s_true = format!( + "{}", + TokenConfigurationChangeItem::MintingAllowChoosingDestination(true) + ); + let s_false = format!( + "{}", + TokenConfigurationChangeItem::MintingAllowChoosingDestination(false) + ); + assert!(s_true.contains("true")); + assert!(s_false.contains("false")); + } + + #[test] + fn display_marketplace_trade_mode_uses_debug() { + let s = format!( + "{}", + TokenConfigurationChangeItem::MarketplaceTradeMode(TokenTradeMode::NotTradeable) + ); + assert!(s.contains("NotTradeable")); + } + + #[test] + fn display_action_takers_group_variants() { + // Exercise a handful of action-taker-carrying variants to confirm each + // writes its label. + let aat = AuthorizedActionTakers::ContractOwner; + let cases = vec![ + ( + format!( + "{}", + TokenConfigurationChangeItem::ManualMinting(aat.clone()) + ), + "Manual Minting", + ), + ( + format!( + "{}", + TokenConfigurationChangeItem::ManualMintingAdminGroup(aat.clone()) + ), + "Manual Minting Admin Group", + ), + ( + format!( + "{}", + TokenConfigurationChangeItem::ManualBurning(aat.clone()) + ), + "Manual Burning", + ), + ( + format!("{}", TokenConfigurationChangeItem::Freeze(aat.clone())), + "Freeze", + ), + ( + format!("{}", TokenConfigurationChangeItem::Unfreeze(aat.clone())), + "Unfreeze", + ), + ( + format!( + "{}", + TokenConfigurationChangeItem::DestroyFrozenFunds(aat.clone()) + ), + "Destroy Frozen Funds", + ), + ( + format!( + "{}", + TokenConfigurationChangeItem::EmergencyAction(aat.clone()) + ), + "Emergency Action", + ), + ( + format!( + "{}", + TokenConfigurationChangeItem::MarketplaceTradeModeControlGroup(aat.clone()) + ), + "Marketplace Trade Mode Control Group", + ), + ( + format!( + "{}", + TokenConfigurationChangeItem::ConventionsControlGroup(aat.clone()) + ), + "Conventions Control Group", + ), + ]; + for (output, expected_prefix) in cases { + assert!( + output.contains(expected_prefix), + "expected {:?} in {:?}", + expected_prefix, + output + ); + } + } + + // --- Equality --- + + #[test] + fn equality_same_variant_same_data() { + let a = TokenConfigurationChangeItem::MaxSupply(Some(42)); + let b = TokenConfigurationChangeItem::MaxSupply(Some(42)); + assert_eq!(a, b); + } + + #[test] + fn equality_same_variant_different_data_unequal() { + let a = TokenConfigurationChangeItem::MaxSupply(Some(42)); + let b = TokenConfigurationChangeItem::MaxSupply(Some(43)); + assert_ne!(a, b); + } + + #[test] + fn equality_different_variants_unequal() { + let a = TokenConfigurationChangeItem::MaxSupply(None); + let b = TokenConfigurationChangeItem::MainControlGroup(None); + assert_ne!(a, b); + } + + #[test] + fn equality_authorized_action_takers_sensitivity() { + let a = TokenConfigurationChangeItem::ManualMinting(AuthorizedActionTakers::NoOne); + let b = TokenConfigurationChangeItem::ManualMinting(AuthorizedActionTakers::ContractOwner); + assert_ne!(a, b); + } + + #[test] + fn equality_bool_variant_distinguishes_values() { + let a = TokenConfigurationChangeItem::MintingAllowChoosingDestination(true); + let b = TokenConfigurationChangeItem::MintingAllowChoosingDestination(false); + assert_ne!(a, b); + } + + // --- Default + Clone --- + + #[test] + fn default_is_no_change() { + let d: TokenConfigurationChangeItem = Default::default(); + assert_eq!(d, TokenConfigurationChangeItem::TokenConfigurationNoChange); + } + + #[test] + fn clone_preserves_variant_and_data() { + let aat = AuthorizedActionTakers::Identity(Identifier::from([5u8; 32])); + let a = TokenConfigurationChangeItem::Freeze(aat); + let b = a.clone(); + assert_eq!(a, b); + } + + // --- Debug --- + + #[test] + fn debug_trait_contains_variant_name() { + let a = TokenConfigurationChangeItem::ManualMinting(AuthorizedActionTakers::NoOne); + let dbg = format!("{:?}", a); + assert!(dbg.contains("ManualMinting")); + } } diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_moment/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_moment/mod.rs index b84ba184b31..76214135d2e 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_moment/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_moment/mod.rs @@ -681,4 +681,534 @@ mod tests { let result = start.steps_till(&end, &step, true, true).unwrap(); assert_eq!(result, 3); } + + // ----- to_u64 / From ----- + + #[test] + fn test_to_u64_variants() { + assert_eq!(RewardDistributionMoment::BlockBasedMoment(0).to_u64(), 0); + assert_eq!( + RewardDistributionMoment::BlockBasedMoment(u64::MAX).to_u64(), + u64::MAX + ); + assert_eq!( + RewardDistributionMoment::TimeBasedMoment(1_234_567_890).to_u64(), + 1_234_567_890 + ); + assert_eq!( + RewardDistributionMoment::EpochBasedMoment(u16::MAX).to_u64(), + u16::MAX as u64 + ); + } + + #[test] + fn test_from_moment_into_u64() { + let v: u64 = RewardDistributionMoment::EpochBasedMoment(42).into(); + assert_eq!(v, 42); + } + + // ----- same_type ----- + + #[test] + fn test_same_type() { + let b = RewardDistributionMoment::BlockBasedMoment(1); + let b2 = RewardDistributionMoment::BlockBasedMoment(99); + let t = RewardDistributionMoment::TimeBasedMoment(1); + let e = RewardDistributionMoment::EpochBasedMoment(1); + + assert!(b.same_type(&b2)); + assert!(!b.same_type(&t)); + assert!(!b.same_type(&e)); + assert!(!t.same_type(&e)); + assert!(t.same_type(&RewardDistributionMoment::TimeBasedMoment(2))); + assert!(e.same_type(&RewardDistributionMoment::EpochBasedMoment(2))); + } + + // ----- cycle_start ----- + + #[test] + fn test_cycle_start_block_aligned() { + let start = RewardDistributionMoment::BlockBasedMoment(100); + let step = RewardDistributionMoment::BlockBasedMoment(10); + let result = start.cycle_start(step).unwrap(); + assert_eq!(result, RewardDistributionMoment::BlockBasedMoment(100)); + } + + #[test] + fn test_cycle_start_block_unaligned() { + let start = RewardDistributionMoment::BlockBasedMoment(107); + let step = RewardDistributionMoment::BlockBasedMoment(10); + let result = start.cycle_start(step).unwrap(); + assert_eq!(result, RewardDistributionMoment::BlockBasedMoment(100)); + } + + #[test] + fn test_cycle_start_time_unaligned() { + let start = RewardDistributionMoment::TimeBasedMoment(98_765); + let step = RewardDistributionMoment::TimeBasedMoment(1_000); + let result = start.cycle_start(step).unwrap(); + assert_eq!(result, RewardDistributionMoment::TimeBasedMoment(98_000)); + } + + #[test] + fn test_cycle_start_epoch_unaligned() { + let start = RewardDistributionMoment::EpochBasedMoment(17); + let step = RewardDistributionMoment::EpochBasedMoment(5); + let result = start.cycle_start(step).unwrap(); + assert_eq!(result, RewardDistributionMoment::EpochBasedMoment(15)); + } + + #[test] + fn test_cycle_start_zero_step_block() { + let start = RewardDistributionMoment::BlockBasedMoment(100); + let step = RewardDistributionMoment::BlockBasedMoment(0); + let result = start.cycle_start(step); + assert!(matches!( + result, + Err(ProtocolError::InvalidDistributionStep(_)) + )); + } + + #[test] + fn test_cycle_start_zero_step_time() { + let start = RewardDistributionMoment::TimeBasedMoment(100); + let step = RewardDistributionMoment::TimeBasedMoment(0); + let result = start.cycle_start(step); + assert!(matches!( + result, + Err(ProtocolError::InvalidDistributionStep(_)) + )); + } + + #[test] + fn test_cycle_start_zero_step_epoch() { + let start = RewardDistributionMoment::EpochBasedMoment(100); + let step = RewardDistributionMoment::EpochBasedMoment(0); + let result = start.cycle_start(step); + assert!(matches!( + result, + Err(ProtocolError::InvalidDistributionStep(_)) + )); + } + + #[test] + fn test_cycle_start_type_mismatch() { + let start = RewardDistributionMoment::BlockBasedMoment(100); + let step = RewardDistributionMoment::TimeBasedMoment(10); + let result = start.cycle_start(step); + assert!(matches!( + result, + Err(ProtocolError::AddingDifferentTypes(_)) + )); + } + + // ----- Add ----- + + #[test] + fn test_add_u64_block() { + let a = RewardDistributionMoment::BlockBasedMoment(100); + let r = (a + 50).unwrap(); + assert_eq!(r, RewardDistributionMoment::BlockBasedMoment(150)); + } + + #[test] + fn test_add_u64_block_overflow() { + let a = RewardDistributionMoment::BlockBasedMoment(u64::MAX); + let r = a + 1; + assert!(matches!(r, Err(ProtocolError::Overflow(_)))); + } + + #[test] + fn test_add_u64_time() { + let a = RewardDistributionMoment::TimeBasedMoment(500); + let r = (a + 100).unwrap(); + assert_eq!(r, RewardDistributionMoment::TimeBasedMoment(600)); + } + + #[test] + fn test_add_u64_time_overflow() { + let a = RewardDistributionMoment::TimeBasedMoment(u64::MAX); + let r = a + 1; + assert!(matches!(r, Err(ProtocolError::Overflow(_)))); + } + + #[test] + fn test_add_u64_epoch() { + let a = RewardDistributionMoment::EpochBasedMoment(100); + let r = (a + 5).unwrap(); + assert_eq!(r, RewardDistributionMoment::EpochBasedMoment(105)); + } + + #[test] + fn test_add_u64_epoch_rhs_exceeds_u16_max() { + let a = RewardDistributionMoment::EpochBasedMoment(0); + let rhs = u16::MAX as u64 + 1; + let r = a + rhs; + assert!(matches!(r, Err(ProtocolError::Overflow(_)))); + } + + #[test] + fn test_add_u64_epoch_overflow_u16() { + let a = RewardDistributionMoment::EpochBasedMoment(u16::MAX); + let r = a + 1; + assert!(matches!(r, Err(ProtocolError::Overflow(_)))); + } + + // ----- Add ----- + + #[test] + fn test_add_self_block() { + let a = RewardDistributionMoment::BlockBasedMoment(100); + let b = RewardDistributionMoment::BlockBasedMoment(50); + let r = (a + b).unwrap(); + assert_eq!(r, RewardDistributionMoment::BlockBasedMoment(150)); + } + + #[test] + fn test_add_self_block_overflow() { + let a = RewardDistributionMoment::BlockBasedMoment(u64::MAX); + let b = RewardDistributionMoment::BlockBasedMoment(1); + let r = a + b; + assert!(matches!(r, Err(ProtocolError::Overflow(_)))); + } + + #[test] + fn test_add_self_time_overflow() { + let a = RewardDistributionMoment::TimeBasedMoment(u64::MAX); + let b = RewardDistributionMoment::TimeBasedMoment(100); + let r = a + b; + assert!(matches!(r, Err(ProtocolError::Overflow(_)))); + } + + #[test] + fn test_add_self_epoch_overflow() { + let a = RewardDistributionMoment::EpochBasedMoment(u16::MAX); + let b = RewardDistributionMoment::EpochBasedMoment(1); + let r = a + b; + assert!(matches!(r, Err(ProtocolError::Overflow(_)))); + } + + #[test] + fn test_add_self_type_mismatch() { + let a = RewardDistributionMoment::BlockBasedMoment(100); + let b = RewardDistributionMoment::EpochBasedMoment(1); + let r = a + b; + assert!(matches!(r, Err(ProtocolError::AddingDifferentTypes(_)))); + } + + // ----- Div ----- + + #[test] + fn test_div_u64_zero_rhs_block() { + let a = RewardDistributionMoment::BlockBasedMoment(100); + let r = a / 0u64; + assert!(matches!(r, Err(ProtocolError::DivideByZero(_)))); + } + + #[test] + fn test_div_u64_zero_rhs_time() { + let a = RewardDistributionMoment::TimeBasedMoment(100); + let r = a / 0u64; + assert!(matches!(r, Err(ProtocolError::DivideByZero(_)))); + } + + #[test] + fn test_div_u64_zero_rhs_epoch() { + let a = RewardDistributionMoment::EpochBasedMoment(100); + let r = a / 0u64; + assert!(matches!(r, Err(ProtocolError::DivideByZero(_)))); + } + + #[test] + fn test_div_u64_epoch_rhs_over_u16() { + let a = RewardDistributionMoment::EpochBasedMoment(100); + let r = a / (u16::MAX as u64 + 1); + assert!(matches!(r, Err(ProtocolError::Overflow(_)))); + } + + #[test] + fn test_div_u64_block_ok() { + let a = RewardDistributionMoment::BlockBasedMoment(100); + let r = (a / 4u64).unwrap(); + assert_eq!(r, RewardDistributionMoment::BlockBasedMoment(25)); + } + + #[test] + fn test_div_u64_time_ok() { + let a = RewardDistributionMoment::TimeBasedMoment(1000); + let r = (a / 4u64).unwrap(); + assert_eq!(r, RewardDistributionMoment::TimeBasedMoment(250)); + } + + #[test] + fn test_div_u64_epoch_ok() { + let a = RewardDistributionMoment::EpochBasedMoment(100); + let r = (a / 4u64).unwrap(); + assert_eq!(r, RewardDistributionMoment::EpochBasedMoment(25)); + } + + // ----- Div ----- + + #[test] + fn test_div_self_block_ok() { + let a = RewardDistributionMoment::BlockBasedMoment(100); + let b = RewardDistributionMoment::BlockBasedMoment(4); + let r = (a / b).unwrap(); + assert_eq!(r, RewardDistributionMoment::BlockBasedMoment(25)); + } + + #[test] + fn test_div_self_block_zero() { + let a = RewardDistributionMoment::BlockBasedMoment(100); + let b = RewardDistributionMoment::BlockBasedMoment(0); + let r = a / b; + assert!(matches!(r, Err(ProtocolError::DivideByZero(_)))); + } + + #[test] + fn test_div_self_time_zero() { + let a = RewardDistributionMoment::TimeBasedMoment(100); + let b = RewardDistributionMoment::TimeBasedMoment(0); + let r = a / b; + assert!(matches!(r, Err(ProtocolError::DivideByZero(_)))); + } + + #[test] + fn test_div_self_epoch_zero() { + let a = RewardDistributionMoment::EpochBasedMoment(100); + let b = RewardDistributionMoment::EpochBasedMoment(0); + let r = a / b; + assert!(matches!(r, Err(ProtocolError::DivideByZero(_)))); + } + + #[test] + fn test_div_self_type_mismatch() { + let a = RewardDistributionMoment::BlockBasedMoment(100); + let b = RewardDistributionMoment::TimeBasedMoment(10); + let r = a / b; + assert!(matches!(r, Err(ProtocolError::AddingDifferentTypes(_)))); + } + + // ----- PartialEq against scalar types ----- + + #[test] + fn test_partial_eq_u64() { + let v = RewardDistributionMoment::BlockBasedMoment(42); + assert!(v == 42u64); + assert!(v == &42u64); + assert!(!(v == 99u64)); + + let t = RewardDistributionMoment::TimeBasedMoment(42); + assert!(t == 42u64); + + let e = RewardDistributionMoment::EpochBasedMoment(42); + assert!(e == 42u64); + // Out-of-u16 range: should return false + assert!(!(e == (u16::MAX as u64 + 1))); + } + + #[test] + fn test_partial_eq_u32() { + let v = RewardDistributionMoment::BlockBasedMoment(42); + assert!(v == 42u32); + assert!(v == &42u32); + assert!(!(v == 99u32)); + + let t = RewardDistributionMoment::TimeBasedMoment(42); + assert!(t == 42u32); + + let e = RewardDistributionMoment::EpochBasedMoment(42); + assert!(e == 42u32); + } + + #[test] + fn test_partial_eq_u16() { + let v = RewardDistributionMoment::BlockBasedMoment(42); + assert!(v == 42u16); + assert!(v == &42u16); + + let t = RewardDistributionMoment::TimeBasedMoment(42); + assert!(t == 42u16); + + let e = RewardDistributionMoment::EpochBasedMoment(42); + assert!(e == 42u16); + assert!(!(e == 99u16)); + } + + #[test] + fn test_partial_eq_usize() { + let v = RewardDistributionMoment::BlockBasedMoment(42); + assert!(v == 42usize); + assert!(v == &42usize); + + let t = RewardDistributionMoment::TimeBasedMoment(42); + assert!(t == 42usize); + + let e = RewardDistributionMoment::EpochBasedMoment(42); + assert!(e == 42usize); + } + + // ----- to_be_bytes_vec ----- + + #[test] + fn test_to_be_bytes_vec_block() { + let v = RewardDistributionMoment::BlockBasedMoment(0x0102_0304_0506_0708); + assert_eq!( + v.to_be_bytes_vec(), + vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08] + ); + } + + #[test] + fn test_to_be_bytes_vec_time() { + let v = RewardDistributionMoment::TimeBasedMoment(0x0000_0000_0000_FFFF); + assert_eq!( + v.to_be_bytes_vec(), + vec![0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF] + ); + } + + #[test] + fn test_to_be_bytes_vec_epoch() { + let v = RewardDistributionMoment::EpochBasedMoment(0x0102); + assert_eq!(v.to_be_bytes_vec(), vec![0x01, 0x02]); + } + + // ----- Display ----- + + #[test] + fn test_display_impl() { + assert_eq!( + format!("{}", RewardDistributionMoment::BlockBasedMoment(5)), + "BlockBasedMoment(5)" + ); + assert_eq!( + format!("{}", RewardDistributionMoment::TimeBasedMoment(99)), + "TimeBasedMoment(99)" + ); + assert_eq!( + format!("{}", RewardDistributionMoment::EpochBasedMoment(7)), + "EpochBasedMoment(7)" + ); + } + + // ----- from_block_info ----- + + #[test] + fn test_from_block_info_block_based() { + use crate::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; + let block_info = BlockInfo { + height: 123, + time_ms: 456_000, + core_height: 0, + epoch: crate::block::epoch::Epoch::new(7).unwrap(), + }; + let dt = RewardDistributionType::BlockBasedDistribution { + interval: 100, + function: DistributionFunction::FixedAmount { amount: 1 }, + }; + assert_eq!( + RewardDistributionMoment::from_block_info(&block_info, &dt), + RewardDistributionMoment::BlockBasedMoment(123) + ); + } + + #[test] + fn test_from_block_info_time_based() { + use crate::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; + let block_info = BlockInfo { + height: 123, + time_ms: 456_000, + core_height: 0, + epoch: crate::block::epoch::Epoch::new(7).unwrap(), + }; + let dt = RewardDistributionType::TimeBasedDistribution { + interval: 60_000, + function: DistributionFunction::FixedAmount { amount: 1 }, + }; + assert_eq!( + RewardDistributionMoment::from_block_info(&block_info, &dt), + RewardDistributionMoment::TimeBasedMoment(456_000) + ); + } + + #[test] + fn test_from_block_info_epoch_based() { + use crate::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; + let block_info = BlockInfo { + height: 123, + time_ms: 456_000, + core_height: 0, + epoch: crate::block::epoch::Epoch::new(7).unwrap(), + }; + let dt = RewardDistributionType::EpochBasedDistribution { + interval: 1, + function: DistributionFunction::FixedAmount { amount: 1 }, + }; + assert_eq!( + RewardDistributionMoment::from_block_info(&block_info, &dt), + RewardDistributionMoment::EpochBasedMoment(7) + ); + } + + // ----- steps_till more edge cases ----- + + #[test] + fn test_steps_till_type_mismatch_time_vs_epoch() { + let start = RewardDistributionMoment::TimeBasedMoment(10); + let end = RewardDistributionMoment::EpochBasedMoment(50); + let step = RewardDistributionMoment::TimeBasedMoment(5); + + let result = start.steps_till(&end, &step, true, true); + assert!(matches!( + result, + Err(ProtocolError::AddingDifferentTypes(_)) + )); + } + + #[test] + fn test_steps_till_zero_step_time() { + let start = RewardDistributionMoment::TimeBasedMoment(10); + let end = RewardDistributionMoment::TimeBasedMoment(50); + let step = RewardDistributionMoment::TimeBasedMoment(0); + + let result = start.steps_till(&end, &step, true, true); + assert!(matches!( + result, + Err(ProtocolError::InvalidDistributionStep(_)) + )); + } + + #[test] + fn test_steps_till_zero_step_epoch() { + let start = RewardDistributionMoment::EpochBasedMoment(1); + let end = RewardDistributionMoment::EpochBasedMoment(5); + let step = RewardDistributionMoment::EpochBasedMoment(0); + + let result = start.steps_till(&end, &step, true, true); + assert!(matches!( + result, + Err(ProtocolError::InvalidDistributionStep(_)) + )); + } + + #[test] + fn test_steps_till_start_equals_end() { + let start = RewardDistributionMoment::BlockBasedMoment(50); + let end = RewardDistributionMoment::BlockBasedMoment(50); + let step = RewardDistributionMoment::BlockBasedMoment(10); + + let result = start.steps_till(&end, &step, true, true).unwrap(); + assert_eq!(result, 0); + } + + #[test] + fn test_steps_till_epoch_start_greater_than_end() { + let start = RewardDistributionMoment::EpochBasedMoment(10); + let end = RewardDistributionMoment::EpochBasedMoment(5); + let step = RewardDistributionMoment::EpochBasedMoment(1); + + let result = start.steps_till(&end, &step, true, true).unwrap(); + assert_eq!(result, 0); + } } diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/mod.rs index 8e8924388e3..1517b989982 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/mod.rs @@ -199,6 +199,343 @@ impl RewardDistributionType { } } } +#[cfg(test)] +mod tests { + use super::*; + use crate::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; + + fn block_based() -> RewardDistributionType { + RewardDistributionType::BlockBasedDistribution { + interval: 100, + function: DistributionFunction::FixedAmount { amount: 5 }, + } + } + + fn time_based() -> RewardDistributionType { + RewardDistributionType::TimeBasedDistribution { + interval: 60_000, + function: DistributionFunction::FixedAmount { amount: 5 }, + } + } + + fn epoch_based() -> RewardDistributionType { + RewardDistributionType::EpochBasedDistribution { + interval: 1, + function: DistributionFunction::FixedAmount { amount: 5 }, + } + } + + // ----- moment_from_bytes ----- + + #[test] + fn test_moment_from_bytes_block_ok() { + let dt = block_based(); + let bytes = [0u8, 0, 0, 0, 0, 0, 0, 42]; + let result = dt.moment_from_bytes(&bytes).unwrap(); + assert_eq!(result, RewardDistributionMoment::BlockBasedMoment(42)); + } + + #[test] + fn test_moment_from_bytes_block_wrong_len() { + let dt = block_based(); + let bytes = [0u8, 0, 0, 42]; + let result = dt.moment_from_bytes(&bytes); + assert!(matches!(result, Err(ProtocolError::DecodingError(_)))); + } + + #[test] + fn test_moment_from_bytes_block_empty() { + let dt = block_based(); + let result = dt.moment_from_bytes(&[]); + assert!(matches!(result, Err(ProtocolError::DecodingError(_)))); + } + + #[test] + fn test_moment_from_bytes_time_ok() { + let dt = time_based(); + let bytes = [0u8, 0, 0, 0, 0, 0, 0x01, 0x00]; + let result = dt.moment_from_bytes(&bytes).unwrap(); + assert_eq!(result, RewardDistributionMoment::TimeBasedMoment(256)); + } + + #[test] + fn test_moment_from_bytes_time_wrong_len() { + let dt = time_based(); + let bytes = [0u8, 0, 0]; + let result = dt.moment_from_bytes(&bytes); + assert!(matches!(result, Err(ProtocolError::DecodingError(_)))); + } + + #[test] + fn test_moment_from_bytes_epoch_ok() { + let dt = epoch_based(); + let bytes = [0x00, 0x07]; + let result = dt.moment_from_bytes(&bytes).unwrap(); + assert_eq!(result, RewardDistributionMoment::EpochBasedMoment(7)); + } + + #[test] + fn test_moment_from_bytes_epoch_wrong_len_too_short() { + let dt = epoch_based(); + let bytes = [0u8]; + let result = dt.moment_from_bytes(&bytes); + assert!(matches!(result, Err(ProtocolError::DecodingError(_)))); + } + + #[test] + fn test_moment_from_bytes_epoch_wrong_len_too_long() { + let dt = epoch_based(); + let bytes = [0u8, 0, 0, 0]; + let result = dt.moment_from_bytes(&bytes); + assert!(matches!(result, Err(ProtocolError::DecodingError(_)))); + } + + // ----- interval() / function() accessors ----- + + #[test] + fn test_interval_accessor() { + assert_eq!( + block_based().interval(), + RewardDistributionMoment::BlockBasedMoment(100) + ); + assert_eq!( + time_based().interval(), + RewardDistributionMoment::TimeBasedMoment(60_000) + ); + assert_eq!( + epoch_based().interval(), + RewardDistributionMoment::EpochBasedMoment(1) + ); + } + + #[test] + fn test_function_accessor() { + match block_based().function() { + DistributionFunction::FixedAmount { amount } => assert_eq!(*amount, 5), + _ => panic!("unexpected function"), + } + } + + // ----- max_cycle_moment ----- + + #[test] + fn test_max_cycle_moment_block_capped_by_current() { + let dt = block_based(); + let start = RewardDistributionMoment::BlockBasedMoment(1000); + let current = RewardDistributionMoment::BlockBasedMoment(1500); + // max_cycles*interval = 10*100=1000. start+1000=2000 > current=1500 so capped at current + let result = dt.max_cycle_moment(start, current, 10).unwrap(); + assert_eq!(result, RewardDistributionMoment::BlockBasedMoment(1500)); + } + + #[test] + fn test_max_cycle_moment_block_capped_by_max_cycles_non_fixed() { + use crate::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; + let dt = RewardDistributionType::BlockBasedDistribution { + interval: 100, + function: DistributionFunction::Random { min: 1, max: 10 }, + }; + let start = RewardDistributionMoment::BlockBasedMoment(0); + let current = RewardDistributionMoment::BlockBasedMoment(u64::MAX); + // max_cycles = 3 (non-fixed) => 0 + 100*3 = 300 + let result = dt.max_cycle_moment(start, current, 3).unwrap(); + assert_eq!(result, RewardDistributionMoment::BlockBasedMoment(300)); + } + + #[test] + fn test_max_cycle_moment_time_capped_by_current() { + let dt = time_based(); + let start = RewardDistributionMoment::TimeBasedMoment(0); + let current = RewardDistributionMoment::TimeBasedMoment(100_000); + // max_cycles*interval could exceed current; should cap + let result = dt.max_cycle_moment(start, current, 5).unwrap(); + // fixed amount cycles = MAX_DISTRIBUTION_CYCLES_PARAM => huge, cap by current + assert_eq!(result, RewardDistributionMoment::TimeBasedMoment(100_000)); + } + + #[test] + fn test_max_cycle_moment_epoch_subtracts_one() { + let dt = epoch_based(); + let start = RewardDistributionMoment::EpochBasedMoment(0); + let current = RewardDistributionMoment::EpochBasedMoment(3); + let result = dt.max_cycle_moment(start, current, 10).unwrap(); + // Since fixed amount: huge max_cycles, saturating_mul on u16 -> u16::MAX; + // min(start + step*max, current - 1) = current - 1 = 2 + assert_eq!(result, RewardDistributionMoment::EpochBasedMoment(2)); + } + + #[test] + fn test_max_cycle_moment_epoch_current_zero_saturates() { + let dt = epoch_based(); + let start = RewardDistributionMoment::EpochBasedMoment(0); + let current = RewardDistributionMoment::EpochBasedMoment(0); + // current - 1 saturates to 0 + let result = dt.max_cycle_moment(start, current, 10).unwrap(); + assert_eq!(result, RewardDistributionMoment::EpochBasedMoment(0)); + } + + #[test] + fn test_max_cycle_moment_type_mismatch() { + let dt = block_based(); + let start = RewardDistributionMoment::BlockBasedMoment(0); + let current = RewardDistributionMoment::TimeBasedMoment(50); + let result = dt.max_cycle_moment(start, current, 10); + assert!(matches!( + result, + Err(ProtocolError::CorruptedCodeExecution(_)) + )); + } + + // ----- Display ----- + + #[test] + fn test_display_block_based() { + let dt = block_based(); + let s = format!("{}", dt); + assert!(s.contains("BlockBasedDistribution")); + assert!(s.contains("100 blocks")); + } + + #[test] + fn test_display_time_based() { + let dt = time_based(); + let s = format!("{}", dt); + assert!(s.contains("TimeBasedDistribution")); + assert!(s.contains("60000 milliseconds")); + } + + #[test] + fn test_display_epoch_based() { + let dt = epoch_based(); + let s = format!("{}", dt); + assert!(s.contains("EpochBasedDistribution")); + assert!(s.contains("1 epochs")); + } + + // ----- validate_structure_interval_v0 ----- + + #[test] + fn test_validate_structure_interval_block_mainnet_too_short() { + use dashcore::Network; + let dt = RewardDistributionType::BlockBasedDistribution { + interval: 50, + function: DistributionFunction::FixedAmount { amount: 1 }, + }; + let result = dt.validate_structure_interval_v0(Network::Mainnet); + assert!(!result.is_valid()); + } + + #[test] + fn test_validate_structure_interval_block_mainnet_ok() { + use dashcore::Network; + let dt = RewardDistributionType::BlockBasedDistribution { + interval: 100, + function: DistributionFunction::FixedAmount { amount: 1 }, + }; + let result = dt.validate_structure_interval_v0(Network::Mainnet); + assert!(result.is_valid(), "errors: {:?}", result.errors); + } + + #[test] + fn test_validate_structure_interval_block_testnet() { + use dashcore::Network; + let dt_ok = RewardDistributionType::BlockBasedDistribution { + interval: 5, + function: DistributionFunction::FixedAmount { amount: 1 }, + }; + assert!(dt_ok + .validate_structure_interval_v0(Network::Testnet) + .is_valid()); + + let dt_bad = RewardDistributionType::BlockBasedDistribution { + interval: 4, + function: DistributionFunction::FixedAmount { amount: 1 }, + }; + assert!(!dt_bad + .validate_structure_interval_v0(Network::Testnet) + .is_valid()); + } + + #[test] + fn test_validate_structure_interval_block_regtest() { + use dashcore::Network; + let dt = RewardDistributionType::BlockBasedDistribution { + interval: 1, + function: DistributionFunction::FixedAmount { amount: 1 }, + }; + assert!(dt + .validate_structure_interval_v0(Network::Regtest) + .is_valid()); + + let dt_zero = RewardDistributionType::BlockBasedDistribution { + interval: 0, + function: DistributionFunction::FixedAmount { amount: 1 }, + }; + assert!(!dt_zero + .validate_structure_interval_v0(Network::Regtest) + .is_valid()); + } + + #[test] + fn test_validate_structure_interval_time_mainnet_too_short() { + use dashcore::Network; + let dt = RewardDistributionType::TimeBasedDistribution { + interval: 60_000, + function: DistributionFunction::FixedAmount { amount: 1 }, + }; + let result = dt.validate_structure_interval_v0(Network::Mainnet); + // Less than 1 hour = 3_600_000 ms. Should fail. + assert!(!result.is_valid()); + } + + #[test] + fn test_validate_structure_interval_time_mainnet_not_minute_aligned() { + use dashcore::Network; + let dt = RewardDistributionType::TimeBasedDistribution { + // 3_600_500 > 3_600_000 but not divisible by 60_000 + interval: 3_600_500, + function: DistributionFunction::FixedAmount { amount: 1 }, + }; + let result = dt.validate_structure_interval_v0(Network::Mainnet); + assert!(!result.is_valid()); + } + + #[test] + fn test_validate_structure_interval_time_mainnet_ok() { + use dashcore::Network; + let dt = RewardDistributionType::TimeBasedDistribution { + interval: 3_600_000, + function: DistributionFunction::FixedAmount { amount: 1 }, + }; + let result = dt.validate_structure_interval_v0(Network::Mainnet); + assert!(result.is_valid(), "errors: {:?}", result.errors); + } + + #[test] + fn test_validate_structure_interval_time_regtest_ok() { + use dashcore::Network; + let dt = RewardDistributionType::TimeBasedDistribution { + interval: 60_000, + function: DistributionFunction::FixedAmount { amount: 1 }, + }; + let result = dt.validate_structure_interval_v0(Network::Regtest); + assert!(result.is_valid(), "errors: {:?}", result.errors); + } + + #[test] + fn test_validate_structure_interval_epoch_always_ok() { + use dashcore::Network; + // Epoch-based validation does no checks; even zero interval passes. + let dt = RewardDistributionType::EpochBasedDistribution { + interval: 0, + function: DistributionFunction::FixedAmount { amount: 1 }, + }; + assert!(dt + .validate_structure_interval_v0(Network::Mainnet) + .is_valid()); + } +} + impl fmt::Display for RewardDistributionType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { diff --git a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/v0/mod.rs index 0f903def887..bb0326d0e2a 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/v0/mod.rs @@ -58,3 +58,162 @@ pub struct AddressFundingFromAssetLockTransitionV0 { #[platform_signable(exclude_from_sig_hash)] pub input_witnesses: Vec, } + +#[cfg(test)] +mod tests { + use super::*; + use crate::address_funds::AddressFundsFeeStrategyStep; + use crate::identity::state_transition::asset_lock_proof::chain::ChainAssetLockProof; + use crate::identity::state_transition::AssetLockProved; + use crate::state_transition::{StateTransitionLike, StateTransitionType}; + use dashcore::OutPoint; + + fn make_transition() -> AddressFundingFromAssetLockTransitionV0 { + let mut inputs = BTreeMap::new(); + inputs.insert(PlatformAddress::P2pkh([1u8; 20]), (0, 1_000_000)); + + let mut outputs = BTreeMap::new(); + outputs.insert(PlatformAddress::P2pkh([2u8; 20]), Some(500_000)); + outputs.insert(PlatformAddress::P2pkh([3u8; 20]), None); + + AddressFundingFromAssetLockTransitionV0 { + asset_lock_proof: AssetLockProof::Chain(ChainAssetLockProof { + core_chain_locked_height: 100, + out_point: OutPoint::from([11u8; 36]), + }), + inputs, + outputs, + fee_strategy: vec![AddressFundsFeeStrategyStep::DeductFromInput(0)], + user_fee_increase: 0, + signature: BinaryData::new(vec![1u8; 65]), + input_witnesses: vec![AddressWitness::P2pkh { + signature: vec![0u8; 65].into(), + }], + } + } + + #[test] + fn test_state_transition_like_type() { + let t = make_transition(); + assert_eq!( + t.state_transition_type(), + StateTransitionType::AddressFundingFromAssetLock + ); + } + + #[test] + fn test_state_transition_like_protocol_version_is_zero() { + let t = make_transition(); + assert_eq!(t.state_transition_protocol_version(), 0); + } + + #[test] + fn test_state_transition_like_modified_ids_empty() { + let t = make_transition(); + assert!(t.modified_data_ids().is_empty()); + } + + #[test] + fn test_state_transition_like_unique_ids_empty() { + let t = make_transition(); + assert!(t.unique_identifiers().is_empty()); + } + + #[test] + fn test_asset_lock_proved_accessor() { + let t = make_transition(); + let proof = t.asset_lock_proof(); + // It's an asset lock proof of Chain variant + match proof { + AssetLockProof::Chain(_) => {} + _ => panic!("expected Chain variant"), + } + } + + #[test] + fn test_set_asset_lock_proof() { + let mut t = make_transition(); + let new_proof = AssetLockProof::Chain(ChainAssetLockProof { + core_chain_locked_height: 200, + out_point: OutPoint::from([99u8; 36]), + }); + t.set_asset_lock_proof(new_proof.clone()).unwrap(); + if let AssetLockProof::Chain(new) = t.asset_lock_proof() { + assert_eq!(new.core_chain_locked_height, 200); + } else { + panic!("expected Chain"); + } + } + + #[test] + fn test_state_transition_single_signed() { + use crate::state_transition::StateTransitionSingleSigned; + let mut t = make_transition(); + assert_eq!(t.signature().as_slice(), &vec![1u8; 65][..]); + t.set_signature(BinaryData::new(vec![9u8; 65])); + assert_eq!(t.signature().as_slice(), &vec![9u8; 65][..]); + t.set_signature_bytes(vec![5u8; 65]); + assert_eq!(t.signature().as_slice(), &vec![5u8; 65][..]); + } + + #[test] + fn test_state_transition_user_fee_increase() { + use crate::state_transition::StateTransitionHasUserFeeIncrease; + let mut t = make_transition(); + assert_eq!(t.user_fee_increase(), 0); + t.set_user_fee_increase(7); + assert_eq!(t.user_fee_increase(), 7); + } + + #[test] + fn test_state_transition_witness_signed() { + use crate::state_transition::StateTransitionWitnessSigned; + let mut t = make_transition(); + assert_eq!(t.inputs().len(), 1); + assert_eq!(t.witnesses().len(), 1); + + t.inputs_mut().clear(); + assert!(t.inputs().is_empty()); + + let mut new_inputs = BTreeMap::new(); + new_inputs.insert(PlatformAddress::P2pkh([4u8; 20]), (1, 500_000)); + t.set_inputs(new_inputs); + assert_eq!(t.inputs().len(), 1); + + t.set_witnesses(vec![]); + assert!(t.witnesses().is_empty()); + } + + #[test] + fn test_accessors_outputs() { + use crate::state_transition::address_funding_from_asset_lock_transition::v0::AddressFundingFromAssetLockTransitionV0Signable; + let mut t = make_transition(); + assert_eq!(t.outputs.len(), 2); + + let mut new_outputs = BTreeMap::new(); + new_outputs.insert(PlatformAddress::P2pkh([9u8; 20]), None); + t.outputs = new_outputs; + assert_eq!(t.outputs.len(), 1); + + // signable type just to keep the import alive — it's a generated Signable shadow type + let _: AddressFundingFromAssetLockTransitionV0Signable = (&t).into(); + } + + #[test] + fn test_feature_versioned() { + use crate::state_transition::FeatureVersioned; + let t = make_transition(); + assert_eq!(t.feature_version(), 0); + } + + #[test] + fn test_default_impl() { + // Default constructs a transition with empty collections and default asset lock proof. + let t = AddressFundingFromAssetLockTransitionV0::default(); + assert!(t.inputs.is_empty()); + assert!(t.outputs.is_empty()); + assert!(t.fee_strategy.is_empty()); + assert_eq!(t.user_fee_increase, 0); + assert!(t.input_witnesses.is_empty()); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition_action_type.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition_action_type.rs index 2fadc908381..9ee0f6445c8 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition_action_type.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition_action_type.rs @@ -47,3 +47,54 @@ impl TryFrom<&str> for DocumentTransitionActionType { } } } + +#[cfg(test)] +mod tests { + use super::*; + + // The happy paths and basic error case are already covered in + // batch_transition/tests.rs. The tests below add coverage for + // edge cases NOT exercised there. + + #[test] + fn try_from_str_returns_protocol_error_with_unknown_action_substring() { + // Verify the specific error message structure — exercises the + // format!() inside the catch-all arm. + let err = DocumentTransitionActionType::try_from("nonexistent").unwrap_err(); + match err { + ProtocolError::Generic(msg) => { + assert!(msg.contains("unknown action type")); + assert!(msg.contains("nonexistent")); + } + other => panic!("expected ProtocolError::Generic, got {:?}", other), + } + } + + #[test] + fn try_from_str_is_case_sensitive() { + // Only lowercase "create" is valid; capitalized variants must error. + assert!(DocumentTransitionActionType::try_from("Create").is_err()); + assert!(DocumentTransitionActionType::try_from("CREATE").is_err()); + assert!(DocumentTransitionActionType::try_from("DELETE").is_err()); + } + + #[test] + fn try_from_str_errors_for_empty_string() { + assert!(DocumentTransitionActionType::try_from("").is_err()); + } + + #[test] + fn try_from_str_does_not_trim_whitespace() { + // Whitespace variants are not trimmed + assert!(DocumentTransitionActionType::try_from(" create").is_err()); + assert!(DocumentTransitionActionType::try_from("create ").is_err()); + assert!(DocumentTransitionActionType::try_from("\tcreate").is_err()); + } + + #[test] + fn try_from_str_rejects_internal_bump_revision_variant() { + // `IgnoreWhileBumpingRevision` is an internal variant with no string form. + assert!(DocumentTransitionActionType::try_from("ignoreWhileBumpingRevision").is_err()); + assert!(DocumentTransitionActionType::try_from("ignore_while_bumping_revision").is_err()); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/resolvers.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/resolvers.rs index ea182fc3d46..d8dabc14ca0 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/resolvers.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/resolvers.rs @@ -249,3 +249,473 @@ impl BatchTransitionResolversV0 for BatchedTransitionRef<'_> { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::state_transition::batch_transition::batched_transition::document_create_transition::DocumentCreateTransitionV0; + use crate::state_transition::batch_transition::batched_transition::document_delete_transition::DocumentDeleteTransitionV0; + use crate::state_transition::batch_transition::batched_transition::document_purchase_transition::DocumentPurchaseTransitionV0; + use crate::state_transition::batch_transition::batched_transition::document_replace_transition::DocumentReplaceTransitionV0; + use crate::state_transition::batch_transition::batched_transition::document_transfer_transition::DocumentTransferTransitionV0; + use crate::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; + use crate::state_transition::batch_transition::batched_transition::document_update_price_transition::DocumentUpdatePriceTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_burn_transition::v0::TokenBurnTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_claim_transition::v0::TokenClaimTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_config_update_transition::v0::TokenConfigUpdateTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_destroy_frozen_funds_transition::v0::TokenDestroyFrozenFundsTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_direct_purchase_transition::v0::TokenDirectPurchaseTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_emergency_action_transition::v0::TokenEmergencyActionTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_freeze_transition::v0::TokenFreezeTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_mint_transition::v0::TokenMintTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_set_price_for_direct_purchase_transition::v0::TokenSetPriceForDirectPurchaseTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_transfer_transition::v0::TokenTransferTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_transition::TokenTransition; + use crate::state_transition::batch_transition::batched_transition::token_unfreeze_transition::v0::TokenUnfreezeTransitionV0; + use crate::state_transition::batch_transition::batched_transition::DocumentPurchaseTransition; + use crate::state_transition::batch_transition::batched_transition::DocumentTransferTransition; + use crate::state_transition::batch_transition::batched_transition::DocumentUpdatePriceTransition; + use crate::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; + use crate::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; + use crate::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; + use crate::tokens::emergency_action::TokenEmergencyAction; + use platform_value::Identifier; + use std::collections::BTreeMap; + + fn base() -> DocumentBaseTransition { + DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: Identifier::default(), + identity_contract_nonce: 1, + document_type_name: "d".to_string(), + data_contract_id: Identifier::default(), + }) + } + + fn tok_base() -> TokenBaseTransition { + TokenBaseTransition::V0(TokenBaseTransitionV0::default()) + } + + fn mk_doc_create() -> BatchedTransition { + BatchedTransition::Document(DocumentTransition::Create(DocumentCreateTransition::V0( + DocumentCreateTransitionV0 { + base: base(), + entropy: [0u8; 32], + data: BTreeMap::new(), + prefunded_voting_balance: None, + }, + ))) + } + + fn mk_doc_replace() -> BatchedTransition { + BatchedTransition::Document(DocumentTransition::Replace(DocumentReplaceTransition::V0( + DocumentReplaceTransitionV0 { + base: base(), + revision: 1, + data: BTreeMap::new(), + }, + ))) + } + + fn mk_doc_delete() -> BatchedTransition { + BatchedTransition::Document(DocumentTransition::Delete(DocumentDeleteTransition::V0( + DocumentDeleteTransitionV0 { base: base() }, + ))) + } + + fn mk_doc_transfer() -> BatchedTransition { + BatchedTransition::Document(DocumentTransition::Transfer( + DocumentTransferTransition::V0(DocumentTransferTransitionV0 { + base: base(), + revision: 1, + recipient_owner_id: Identifier::default(), + }), + )) + } + + fn mk_doc_purchase() -> BatchedTransition { + BatchedTransition::Document(DocumentTransition::Purchase( + DocumentPurchaseTransition::V0(DocumentPurchaseTransitionV0 { + base: base(), + revision: 1, + price: 100, + }), + )) + } + + fn mk_doc_update_price() -> BatchedTransition { + BatchedTransition::Document(DocumentTransition::UpdatePrice( + DocumentUpdatePriceTransition::V0(DocumentUpdatePriceTransitionV0 { + base: base(), + revision: 1, + price: 100, + }), + )) + } + + fn mk_tok_burn() -> BatchedTransition { + BatchedTransition::Token(TokenTransition::Burn(TokenBurnTransition::V0( + TokenBurnTransitionV0 { + base: tok_base(), + burn_amount: 10, + public_note: None, + }, + ))) + } + + fn mk_tok_mint() -> BatchedTransition { + BatchedTransition::Token(TokenTransition::Mint(TokenMintTransition::V0( + TokenMintTransitionV0 { + base: tok_base(), + issued_to_identity_id: None, + amount: 10, + public_note: None, + }, + ))) + } + + fn mk_tok_transfer() -> BatchedTransition { + BatchedTransition::Token(TokenTransition::Transfer(TokenTransferTransition::V0( + TokenTransferTransitionV0 { + base: tok_base(), + recipient_id: Identifier::default(), + amount: 10, + public_note: None, + shared_encrypted_note: None, + private_encrypted_note: None, + }, + ))) + } + + fn mk_tok_freeze() -> BatchedTransition { + BatchedTransition::Token(TokenTransition::Freeze(TokenFreezeTransition::V0( + TokenFreezeTransitionV0 { + base: tok_base(), + identity_to_freeze_id: Identifier::default(), + public_note: None, + }, + ))) + } + + fn mk_tok_unfreeze() -> BatchedTransition { + BatchedTransition::Token(TokenTransition::Unfreeze(TokenUnfreezeTransition::V0( + TokenUnfreezeTransitionV0 { + base: tok_base(), + frozen_identity_id: Identifier::default(), + public_note: None, + }, + ))) + } + + fn mk_tok_destroy_frozen() -> BatchedTransition { + BatchedTransition::Token(TokenTransition::DestroyFrozenFunds( + TokenDestroyFrozenFundsTransition::V0(TokenDestroyFrozenFundsTransitionV0 { + base: tok_base(), + frozen_identity_id: Identifier::default(), + public_note: None, + }), + )) + } + + fn mk_tok_claim() -> BatchedTransition { + use crate::data_contract::associated_token::token_distribution_key::TokenDistributionType; + BatchedTransition::Token(TokenTransition::Claim(TokenClaimTransition::V0( + TokenClaimTransitionV0 { + base: tok_base(), + distribution_type: TokenDistributionType::PreProgrammed, + public_note: None, + }, + ))) + } + + fn mk_tok_emergency() -> BatchedTransition { + BatchedTransition::Token(TokenTransition::EmergencyAction( + TokenEmergencyActionTransition::V0(TokenEmergencyActionTransitionV0 { + base: tok_base(), + emergency_action: TokenEmergencyAction::Pause, + public_note: None, + }), + )) + } + + fn mk_tok_config_update() -> BatchedTransition { + use crate::data_contract::associated_token::token_configuration_item::TokenConfigurationChangeItem; + BatchedTransition::Token(TokenTransition::ConfigUpdate( + TokenConfigUpdateTransition::V0(TokenConfigUpdateTransitionV0 { + base: tok_base(), + update_token_configuration_item: + TokenConfigurationChangeItem::TokenConfigurationNoChange, + public_note: None, + }), + )) + } + + fn mk_tok_direct_purchase() -> BatchedTransition { + BatchedTransition::Token(TokenTransition::DirectPurchase( + TokenDirectPurchaseTransition::V0(TokenDirectPurchaseTransitionV0 { + base: tok_base(), + token_count: 5, + total_agreed_price: 500, + }), + )) + } + + fn mk_tok_set_price() -> BatchedTransition { + BatchedTransition::Token(TokenTransition::SetPriceForDirectPurchase( + TokenSetPriceForDirectPurchaseTransition::V0( + TokenSetPriceForDirectPurchaseTransitionV0 { + base: tok_base(), + price: None, + public_note: None, + }, + ), + )) + } + + // ----------------------------------------------------------------------- + // BatchedTransition resolvers — Document variant returns Some for + // matching document types, None for mismatched document types, and + // None for all token types. + // ----------------------------------------------------------------------- + + #[test] + fn document_create_resolves_only_to_create() { + let bt = mk_doc_create(); + assert!(bt.as_transition_create().is_some()); + assert!(bt.as_transition_replace().is_none()); + assert!(bt.as_transition_delete().is_none()); + assert!(bt.as_transition_transfer().is_none()); + assert!(bt.as_transition_purchase().is_none()); + // token accessors must return None for document variant + assert!(bt.as_transition_token_burn().is_none()); + assert!(bt.as_transition_token_mint().is_none()); + assert!(bt.as_transition_token_transfer().is_none()); + assert!(bt.as_transition_token_freeze().is_none()); + assert!(bt.as_transition_token_unfreeze().is_none()); + assert!(bt.as_transition_token_destroy_frozen_funds().is_none()); + assert!(bt.as_transition_token_claim().is_none()); + assert!(bt.as_transition_token_emergency_action().is_none()); + assert!(bt.as_transition_token_config_update().is_none()); + assert!(bt.as_transition_token_direct_purchase().is_none()); + assert!(bt + .as_transition_token_set_price_for_direct_purchase() + .is_none()); + } + + #[test] + fn document_replace_resolves_only_to_replace() { + let bt = mk_doc_replace(); + assert!(bt.as_transition_create().is_none()); + assert!(bt.as_transition_replace().is_some()); + assert!(bt.as_transition_delete().is_none()); + assert!(bt.as_transition_transfer().is_none()); + assert!(bt.as_transition_purchase().is_none()); + } + + #[test] + fn document_delete_resolves_only_to_delete() { + let bt = mk_doc_delete(); + assert!(bt.as_transition_delete().is_some()); + assert!(bt.as_transition_create().is_none()); + assert!(bt.as_transition_replace().is_none()); + } + + #[test] + fn document_transfer_resolves_only_to_transfer() { + let bt = mk_doc_transfer(); + assert!(bt.as_transition_transfer().is_some()); + assert!(bt.as_transition_purchase().is_none()); + } + + #[test] + fn document_purchase_resolves_only_to_purchase() { + let bt = mk_doc_purchase(); + assert!(bt.as_transition_purchase().is_some()); + assert!(bt.as_transition_transfer().is_none()); + } + + #[test] + fn document_update_price_resolves_to_no_token_variants() { + // UpdatePrice is a document-side variant with no accessor — all + // document and token accessors must return None. + let bt = mk_doc_update_price(); + assert!(bt.as_transition_create().is_none()); + assert!(bt.as_transition_replace().is_none()); + assert!(bt.as_transition_delete().is_none()); + assert!(bt.as_transition_transfer().is_none()); + assert!(bt.as_transition_purchase().is_none()); + assert!(bt.as_transition_token_burn().is_none()); + } + + // ----------------------------------------------------------------------- + // BatchedTransition resolvers — each token variant resolves to exactly + // one token accessor, all others return None. This covers the 14 token + // match arms that Document(_) => None short-circuits. + // ----------------------------------------------------------------------- + + #[test] + fn token_burn_resolves_only_to_burn() { + let bt = mk_tok_burn(); + assert!(bt.as_transition_token_burn().is_some()); + assert!(bt.as_transition_token_mint().is_none()); + assert!(bt.as_transition_create().is_none()); + } + + #[test] + fn token_mint_resolves_only_to_mint() { + let bt = mk_tok_mint(); + assert!(bt.as_transition_token_mint().is_some()); + assert!(bt.as_transition_token_burn().is_none()); + } + + #[test] + fn token_transfer_resolves_only_to_token_transfer() { + let bt = mk_tok_transfer(); + assert!(bt.as_transition_token_transfer().is_some()); + // Not to be confused with document transfer + assert!(bt.as_transition_transfer().is_none()); + } + + #[test] + fn token_freeze_resolves_only_to_freeze() { + let bt = mk_tok_freeze(); + assert!(bt.as_transition_token_freeze().is_some()); + assert!(bt.as_transition_token_unfreeze().is_none()); + } + + #[test] + fn token_unfreeze_resolves_only_to_unfreeze() { + let bt = mk_tok_unfreeze(); + assert!(bt.as_transition_token_unfreeze().is_some()); + assert!(bt.as_transition_token_freeze().is_none()); + } + + #[test] + fn token_destroy_frozen_resolves_only_to_destroy_frozen() { + let bt = mk_tok_destroy_frozen(); + assert!(bt.as_transition_token_destroy_frozen_funds().is_some()); + assert!(bt.as_transition_token_freeze().is_none()); + } + + #[test] + fn token_claim_resolves_only_to_claim() { + let bt = mk_tok_claim(); + assert!(bt.as_transition_token_claim().is_some()); + assert!(bt.as_transition_token_mint().is_none()); + } + + #[test] + fn token_emergency_resolves_only_to_emergency() { + let bt = mk_tok_emergency(); + assert!(bt.as_transition_token_emergency_action().is_some()); + assert!(bt.as_transition_token_config_update().is_none()); + } + + #[test] + fn token_config_update_resolves_only_to_config_update() { + let bt = mk_tok_config_update(); + assert!(bt.as_transition_token_config_update().is_some()); + assert!(bt.as_transition_token_emergency_action().is_none()); + } + + #[test] + fn token_direct_purchase_resolves_only_to_direct_purchase() { + let bt = mk_tok_direct_purchase(); + assert!(bt.as_transition_token_direct_purchase().is_some()); + assert!(bt + .as_transition_token_set_price_for_direct_purchase() + .is_none()); + } + + #[test] + fn token_set_price_resolves_only_to_set_price() { + let bt = mk_tok_set_price(); + assert!(bt + .as_transition_token_set_price_for_direct_purchase() + .is_some()); + assert!(bt.as_transition_token_direct_purchase().is_none()); + } + + // ----------------------------------------------------------------------- + // BatchedTransitionRef resolvers — mirror tests, against the Ref impl. + // ----------------------------------------------------------------------- + + #[test] + fn ref_document_create_resolves_only_to_create() { + let bt = mk_doc_create(); + let r = bt.borrow_as_ref(); + assert!(r.as_transition_create().is_some()); + assert!(r.as_transition_replace().is_none()); + assert!(r.as_transition_delete().is_none()); + assert!(r.as_transition_transfer().is_none()); + assert!(r.as_transition_purchase().is_none()); + assert!(r.as_transition_token_burn().is_none()); + assert!(r.as_transition_token_mint().is_none()); + assert!(r.as_transition_token_transfer().is_none()); + assert!(r.as_transition_token_freeze().is_none()); + assert!(r.as_transition_token_unfreeze().is_none()); + assert!(r.as_transition_token_destroy_frozen_funds().is_none()); + assert!(r.as_transition_token_claim().is_none()); + assert!(r.as_transition_token_emergency_action().is_none()); + assert!(r.as_transition_token_config_update().is_none()); + assert!(r.as_transition_token_direct_purchase().is_none()); + assert!(r + .as_transition_token_set_price_for_direct_purchase() + .is_none()); + } + + #[test] + fn ref_all_token_variants_short_circuit_document_accessors() { + // For every token variant, all document-side accessors must be None. + // This exercises the Token(_) => None arms in each document resolver. + for bt in [ + mk_tok_burn(), + mk_tok_mint(), + mk_tok_transfer(), + mk_tok_freeze(), + mk_tok_unfreeze(), + mk_tok_destroy_frozen(), + mk_tok_claim(), + mk_tok_emergency(), + mk_tok_config_update(), + mk_tok_direct_purchase(), + mk_tok_set_price(), + ] { + let r = bt.borrow_as_ref(); + assert!(r.as_transition_create().is_none()); + assert!(r.as_transition_replace().is_none()); + assert!(r.as_transition_delete().is_none()); + assert!(r.as_transition_transfer().is_none()); + assert!(r.as_transition_purchase().is_none()); + } + } + + #[test] + fn ref_all_document_variants_short_circuit_token_accessors() { + // For every document variant, all token-side accessors on the ref + // must be None — covers the Document(_) => None arms on the Ref impl. + for bt in [ + mk_doc_create(), + mk_doc_replace(), + mk_doc_delete(), + mk_doc_transfer(), + mk_doc_purchase(), + mk_doc_update_price(), + ] { + let r = bt.borrow_as_ref(); + assert!(r.as_transition_token_burn().is_none()); + assert!(r.as_transition_token_mint().is_none()); + assert!(r.as_transition_token_transfer().is_none()); + assert!(r.as_transition_token_freeze().is_none()); + assert!(r.as_transition_token_unfreeze().is_none()); + assert!(r.as_transition_token_destroy_frozen_funds().is_none()); + assert!(r.as_transition_token_claim().is_none()); + assert!(r.as_transition_token_emergency_action().is_none()); + assert!(r.as_transition_token_config_update().is_none()); + assert!(r.as_transition_token_direct_purchase().is_none()); + assert!(r + .as_transition_token_set_price_for_direct_purchase() + .is_none()); + } + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition_action_type.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition_action_type.rs index 6215c74acd7..afe766c47f9 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition_action_type.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition_action_type.rs @@ -118,3 +118,52 @@ impl TryFrom<&str> for TokenTransitionActionType { } } } + +#[cfg(test)] +mod tests { + use super::*; + + // The happy paths and basic error case are already covered in + // batch_transition/tests.rs. The tests below add coverage for + // edge cases NOT exercised there. + + #[test] + fn try_from_str_returns_protocol_error_with_unknown_action_substring() { + // Verify the specific error message structure (only is_err() is + // checked elsewhere) — this exercises the format!() in the catch-all. + let err = TokenTransitionActionType::try_from("not_a_real_action").unwrap_err(); + match err { + ProtocolError::Generic(msg) => { + assert!(msg.contains("unknown token transition action type")); + assert!(msg.contains("not_a_real_action")); + } + other => panic!("expected ProtocolError::Generic, got {:?}", other), + } + } + + #[test] + fn try_from_str_errors_for_empty_string() { + assert!(TokenTransitionActionType::try_from("").is_err()); + } + + #[test] + fn try_from_str_does_not_accept_mint_keyword() { + // Known quirk: "issuance" maps to Mint, but "mint" itself is NOT valid. + // Locks in this surprising aliasing behavior. + assert!(TokenTransitionActionType::try_from("mint").is_err()); + } + + #[test] + fn try_from_str_is_case_sensitive_on_basic_variants() { + assert!(TokenTransitionActionType::try_from("Burn").is_err()); + assert!(TokenTransitionActionType::try_from("BURN").is_err()); + assert!(TokenTransitionActionType::try_from("Transfer").is_err()); + } + + #[test] + fn try_from_str_does_not_trim_whitespace() { + assert!(TokenTransitionActionType::try_from(" burn").is_err()); + assert!(TokenTransitionActionType::try_from("burn ").is_err()); + assert!(TokenTransitionActionType::try_from("\tburn").is_err()); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/find_duplicates_by_id/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/find_duplicates_by_id/v0/mod.rs index 27f1f46368a..d5f7fa3255b 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/find_duplicates_by_id/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/find_duplicates_by_id/v0/mod.rs @@ -108,4 +108,88 @@ mod test { assert_eq!(duplicates.len(), 2); } + + fn make_transition_with(id_byte: u8, type_name: &str, nonce: u64) -> DocumentTransition { + DocumentTransition::Create(DocumentCreateTransition::V0(DocumentCreateTransitionV0 { + base: DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: Identifier::new([id_byte; 32]), + identity_contract_nonce: nonce, + document_type_name: type_name.to_string(), + data_contract_id: Identifier::new([0xAA; 32]), + }), + entropy: Default::default(), + data: Default::default(), + prefunded_voting_balance: Default::default(), + })) + } + + #[test] + fn test_empty_input_returns_empty_duplicates() { + let input: Vec<&DocumentTransition> = vec![]; + let duplicates = find_duplicates_by_id(&input); + assert!(duplicates.is_empty()); + } + + #[test] + fn test_single_transition_no_duplicates() { + let t = make_transition_with(1, "doc", 0); + let input = vec![&t]; + let duplicates = find_duplicates_by_id(&input); + assert!(duplicates.is_empty()); + } + + #[test] + fn test_no_duplicates_when_all_distinct() { + let t1 = make_transition_with(1, "a", 0); + let t2 = make_transition_with(2, "a", 1); + let t3 = make_transition_with(3, "b", 2); + let input = vec![&t1, &t2, &t3]; + let duplicates = find_duplicates_by_id(&input); + assert!(duplicates.is_empty()); + } + + #[test] + fn test_same_id_different_type_is_not_a_duplicate() { + // The fingerprint is (document_type, id). If only the id matches but + // the type differs, it should NOT count as a duplicate. + let t1 = make_transition_with(1, "type_a", 0); + let t2 = make_transition_with(1, "type_b", 1); + let input = vec![&t1, &t2]; + let duplicates = find_duplicates_by_id(&input); + assert!(duplicates.is_empty()); + } + + #[test] + fn test_same_type_different_id_is_not_a_duplicate() { + let t1 = make_transition_with(1, "type_a", 0); + let t2 = make_transition_with(2, "type_a", 1); + let input = vec![&t1, &t2]; + let duplicates = find_duplicates_by_id(&input); + assert!(duplicates.is_empty()); + } + + #[test] + fn test_three_way_duplicate_pushes_pairs() { + // Three transitions with the same fingerprint. The current + // implementation pushes the previously-stored entry AND the new + // duplicate every time a collision is detected — so we expect + // (existing, t2) on the second insert, then (existing, t3) on the + // third. That yields four entries total. + let t1 = make_transition_with(7, "doc", 0); + let t2 = make_transition_with(7, "doc", 1); + let t3 = make_transition_with(7, "doc", 2); + let input = vec![&t1, &t2, &t3]; + let duplicates = find_duplicates_by_id(&input); + assert_eq!(duplicates.len(), 4); + } + + #[test] + fn test_fingerprint_only_uses_type_and_id_not_nonce() { + // Differing nonces alone should not prevent a duplicate match. + let t1 = make_transition_with(5, "x", 100); + let t2 = make_transition_with(5, "x", 200); + let input = vec![&t1, &t2]; + let duplicates = find_duplicates_by_id(&input); + assert_eq!(duplicates.len(), 2); + } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs index fbf9159b3e8..126e61d71f5 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs @@ -222,3 +222,207 @@ impl BatchTransition { Ok(result) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::consensus::ConsensusError; + use crate::state_transition::batch_transition::batched_transition::document_create_transition::v0::DocumentCreateTransitionV0; + use crate::state_transition::batch_transition::batched_transition::document_create_transition::DocumentCreateTransition; + use crate::state_transition::batch_transition::batched_transition::document_delete_transition::v0::DocumentDeleteTransitionV0; + use crate::state_transition::batch_transition::batched_transition::document_delete_transition::DocumentDeleteTransition; + use crate::state_transition::batch_transition::batched_transition::BatchedTransition; + use crate::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; + use crate::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; + use crate::state_transition::batch_transition::{ + BatchTransition, BatchTransitionV0, BatchTransitionV1, + }; + use platform_value::BinaryData; + use std::collections::BTreeMap; + + fn make_base(nonce: u64, type_name: &str) -> DocumentBaseTransition { + DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: Identifier::new([1u8; 32]), + identity_contract_nonce: nonce, + document_type_name: type_name.to_string(), + data_contract_id: Identifier::new([0xAA; 32]), + }) + } + + fn make_create(nonce: u64) -> DocumentTransition { + DocumentTransition::Create(DocumentCreateTransition::V0(DocumentCreateTransitionV0 { + base: make_base(nonce, "test_doc"), + entropy: [0u8; 32], + data: BTreeMap::new(), + prefunded_voting_balance: None, + })) + } + + fn make_delete(nonce: u64, id_byte: u8) -> DocumentTransition { + DocumentTransition::Delete(DocumentDeleteTransition::V0(DocumentDeleteTransitionV0 { + base: DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: Identifier::new([id_byte; 32]), + identity_contract_nonce: nonce, + document_type_name: "test_doc".to_string(), + data_contract_id: Identifier::new([0xAA; 32]), + }), + })) + } + + fn make_batch_v0(transitions: Vec) -> BatchTransition { + BatchTransition::V0(BatchTransitionV0 { + owner_id: Identifier::new([0x01; 32]), + transitions, + user_fee_increase: 0, + signature_public_key_id: 0, + signature: BinaryData::default(), + }) + } + + fn make_batch_v1_empty() -> BatchTransition { + BatchTransition::V1(BatchTransitionV1 { + owner_id: Identifier::new([0x02; 32]), + transitions: vec![], + user_fee_increase: 0, + signature_public_key_id: 0, + signature: BinaryData::default(), + }) + } + + // ----------------------------------------------------------------------- + // empty batch — DocumentTransitionsAreAbsentError + // ----------------------------------------------------------------------- + + #[test] + fn validate_base_structure_v0_errors_when_v0_empty() { + let pv = PlatformVersion::latest(); + let batch = make_batch_v0(vec![]); + let result = batch + .validate_base_structure_v0(pv) + .expect("no protocol err"); + assert!(!result.is_valid()); + let errors = result.errors; + assert_eq!(errors.len(), 1); + match &errors[0] { + ConsensusError::BasicError(BasicError::DocumentTransitionsAreAbsentError(_)) => {} + other => panic!( + "expected DocumentTransitionsAreAbsentError, got {:?}", + other + ), + } + } + + #[test] + fn validate_base_structure_v0_errors_when_v1_empty() { + let pv = PlatformVersion::latest(); + let batch = make_batch_v1_empty(); + let result = batch + .validate_base_structure_v0(pv) + .expect("no protocol err"); + assert!(!result.is_valid()); + match &result.errors[0] { + ConsensusError::BasicError(BasicError::DocumentTransitionsAreAbsentError(_)) => {} + other => panic!( + "expected DocumentTransitionsAreAbsentError, got {:?}", + other + ), + } + } + + // ----------------------------------------------------------------------- + // valid single transition with no group / nonce / duplicate issues + // ----------------------------------------------------------------------- + + #[test] + fn validate_base_structure_v0_passes_with_single_valid_transition() { + let pv = PlatformVersion::latest(); + let batch = make_batch_v0(vec![make_create(1)]); + let result = batch + .validate_base_structure_v0(pv) + .expect("no protocol err"); + assert!(result.is_valid(), "expected valid, got {:?}", result.errors); + } + + // ----------------------------------------------------------------------- + // nonce out of bounds — high bits set above 40-bit cap + // ----------------------------------------------------------------------- + + #[test] + fn validate_base_structure_v0_errors_on_nonce_with_high_bits_set() { + let pv = PlatformVersion::latest(); + // MISSING_IDENTITY_REVISIONS_FILTER masks the top bits used to mark + // a missing revision. Setting a value above 40 bits triggers the + // NonceOutOfBoundsError path. + let bad_nonce: u64 = u64::MAX; + let batch = make_batch_v0(vec![make_delete(bad_nonce, 7)]); + let result = batch + .validate_base_structure_v0(pv) + .expect("no protocol err"); + assert!(!result.is_valid()); + let has_nonce_err = result.errors.iter().any(|e| { + matches!( + e, + ConsensusError::BasicError(BasicError::NonceOutOfBoundsError(_)) + ) + }); + assert!( + has_nonce_err, + "expected NonceOutOfBoundsError, got {:?}", + result.errors + ); + } + + // ----------------------------------------------------------------------- + // max-transitions-exceeded — early-return before any other checks + // ----------------------------------------------------------------------- + + #[test] + fn validate_base_structure_v0_errors_when_transitions_exceed_max() { + // The latest platform version caps the batch at a small number of + // transitions. Going over should produce a single + // MaxDocumentsTransitionsExceededError and NO other errors (the + // function early-returns). + let pv = PlatformVersion::latest(); + let max = pv.system_limits.max_transitions_in_documents_batch as usize; + let mut transitions = Vec::with_capacity(max + 1); + // Use distinct ids to avoid duplicate noise — but we never reach the + // duplicate-detection path anyway because we early-return. + for i in 0..(max + 1) { + transitions.push(make_delete(i as u64 + 1, i as u8)); + } + let batch = make_batch_v0(transitions); + let result = batch + .validate_base_structure_v0(pv) + .expect("no protocol err"); + assert!(!result.is_valid()); + assert_eq!(result.errors.len(), 1, "should early-return with one error"); + match &result.errors[0] { + ConsensusError::BasicError(BasicError::MaxDocumentsTransitionsExceededError(_)) => {} + other => panic!( + "expected MaxDocumentsTransitionsExceededError, got {:?}", + other + ), + } + } + + // ----------------------------------------------------------------------- + // V1 batch with only document transitions — same logic, takes the + // BatchedTransitionRef::Document arm of the iterator instead. + // ----------------------------------------------------------------------- + + #[test] + fn validate_base_structure_v0_passes_for_v1_with_documents_only() { + let pv = PlatformVersion::latest(); + let batch = BatchTransition::V1(BatchTransitionV1 { + owner_id: Identifier::new([0x02; 32]), + transitions: vec![BatchedTransition::Document(make_create(1))], + user_fee_increase: 0, + signature_public_key_id: 0, + signature: BinaryData::default(), + }); + let result = batch + .validate_base_structure_v0(pv) + .expect("no protocol err"); + assert!(result.is_valid(), "expected valid, got {:?}", result.errors); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/v0/mod.rs index c376b676f7e..dbc2da51cf0 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/v0/mod.rs @@ -88,3 +88,299 @@ impl TryFrom }) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::address_funds::AddressFundsFeeStrategyStep; + use crate::consensus::basic::BasicError; + use crate::consensus::state::state_error::StateError; + use crate::consensus::ConsensusError; + use crate::identity::{KeyType, Purpose, SecurityLevel}; + use crate::state_transition::public_key_in_creation::v0::IdentityPublicKeyInCreationV0; + use crate::state_transition::StateTransitionStructureValidation; + use platform_value::BinaryData; + use platform_version::version::PlatformVersion; + + fn pv() -> &'static PlatformVersion { + PlatformVersion::latest() + } + + fn make_master_key() -> IdentityPublicKeyInCreation { + IdentityPublicKeyInCreation::V0(IdentityPublicKeyInCreationV0 { + id: 0, + key_type: KeyType::ECDSA_SECP256K1, + purpose: Purpose::AUTHENTICATION, + security_level: SecurityLevel::MASTER, + contract_bounds: None, + read_only: false, + data: BinaryData::new(vec![0u8; 33]), + signature: BinaryData::new(vec![]), + }) + } + + fn make_witness() -> AddressWitness { + AddressWitness::P2pkh { + signature: BinaryData::new(vec![0u8; 65]), + } + } + + fn make_valid() -> IdentityCreateFromAddressesTransitionV0 { + let v = pv(); + let min_input = v.dpp.state_transitions.address_funds.min_input_amount; + let min_funding = v + .dpp + .state_transitions + .address_funds + .min_identity_funding_amount; + let mut inputs = BTreeMap::new(); + inputs.insert( + PlatformAddress::P2pkh([1u8; 20]), + (1u32, min_input.max(min_funding) * 2), + ); + IdentityCreateFromAddressesTransitionV0 { + public_keys: vec![make_master_key()], + inputs, + output: None, + fee_strategy: vec![AddressFundsFeeStrategyStep::DeductFromInput(0)], + user_fee_increase: 0, + input_witnesses: vec![make_witness()], + } + } + + #[test] + fn validate_structure_valid() { + let t = make_valid(); + let r = t.validate_structure(pv()); + assert!(r.is_valid(), "{:?}", r.errors); + } + + #[test] + fn validate_structure_no_inputs() { + let mut t = make_valid(); + t.inputs.clear(); + t.input_witnesses.clear(); + let r = t.validate_structure(pv()); + assert!(matches!( + r.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::TransitionNoInputsError(_) + )] + )); + } + + #[test] + fn validate_structure_no_public_keys() { + let mut t = make_valid(); + t.public_keys.clear(); + let r = t.validate_structure(pv()); + assert!(matches!( + r.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::MissingMasterPublicKeyError(_) + )] + )); + } + + #[test] + fn validate_structure_too_many_public_keys() { + let mut t = make_valid(); + let max = pv() + .dpp + .state_transitions + .identities + .max_public_keys_in_creation as usize; + // Populate > max with distinct data + for i in 0..=max { + t.public_keys.push(IdentityPublicKeyInCreation::V0( + IdentityPublicKeyInCreationV0 { + id: i as u32 + 10, + key_type: KeyType::ECDSA_SECP256K1, + purpose: Purpose::AUTHENTICATION, + security_level: SecurityLevel::HIGH, + contract_bounds: None, + read_only: false, + data: BinaryData::new(vec![i as u8 + 1; 33]), + signature: BinaryData::new(vec![]), + }, + )); + } + let r = t.validate_structure(pv()); + assert!(matches!( + r.errors.as_slice(), + [ConsensusError::StateError( + StateError::MaxIdentityPublicKeyLimitReachedError(_) + )] + )); + } + + #[test] + fn validate_structure_input_witness_mismatch() { + let mut t = make_valid(); + t.input_witnesses.clear(); + let r = t.validate_structure(pv()); + assert!(matches!( + r.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::InputWitnessCountMismatchError(_) + )] + )); + } + + #[test] + fn validate_structure_output_is_input() { + let mut t = make_valid(); + let (addr, _) = t.inputs.iter().next().unwrap(); + t.output = Some((addr.clone(), 500_000)); + let r = t.validate_structure(pv()); + assert!(matches!( + r.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::OutputAddressAlsoInputError(_) + )] + )); + } + + #[test] + fn validate_structure_fee_strategy_empty() { + let mut t = make_valid(); + t.fee_strategy.clear(); + let r = t.validate_structure(pv()); + assert!(matches!( + r.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::FeeStrategyEmptyError(_) + )] + )); + } + + #[test] + fn validate_structure_fee_strategy_duplicate() { + let mut t = make_valid(); + t.fee_strategy = vec![ + AddressFundsFeeStrategyStep::DeductFromInput(0), + AddressFundsFeeStrategyStep::DeductFromInput(0), + ]; + let r = t.validate_structure(pv()); + assert!(matches!( + r.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::FeeStrategyDuplicateError(_) + )] + )); + } + + #[test] + fn validate_structure_fee_strategy_index_out_of_bounds_input() { + let mut t = make_valid(); + t.fee_strategy = vec![AddressFundsFeeStrategyStep::DeductFromInput(42)]; + let r = t.validate_structure(pv()); + assert!(matches!( + r.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::FeeStrategyIndexOutOfBoundsError(_) + )] + )); + } + + #[test] + fn validate_structure_fee_strategy_index_out_of_bounds_output() { + let mut t = make_valid(); + t.fee_strategy = vec![AddressFundsFeeStrategyStep::ReduceOutput(0)]; + let r = t.validate_structure(pv()); + assert!(matches!( + r.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::FeeStrategyIndexOutOfBoundsError(_) + )] + )); + } + + #[test] + fn validate_structure_input_below_minimum() { + let mut t = make_valid(); + let addr = t.inputs.keys().next().cloned().unwrap(); + t.inputs.insert(addr, (1, 1)); + let r = t.validate_structure(pv()); + assert!(matches!( + r.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::InputBelowMinimumError(_) + )] + )); + } + + #[test] + fn validate_structure_output_below_minimum() { + let mut t = make_valid(); + t.output = Some((PlatformAddress::P2pkh([2u8; 20]), 1)); + let r = t.validate_structure(pv()); + assert!(matches!( + r.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::OutputBelowMinimumError(_) + )] + )); + } + + #[test] + fn validate_structure_input_sum_less_than_required() { + let mut t = make_valid(); + let addr = t.inputs.keys().next().cloned().unwrap(); + t.inputs.insert( + addr, + (1, pv().dpp.state_transitions.address_funds.min_input_amount), + ); + let r = t.validate_structure(pv()); + assert!(matches!( + r.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::InputsNotLessThanOutputsError(_) + )] + )); + } + + #[test] + fn validate_structure_overflow_on_input_sum() { + let mut t = make_valid(); + t.inputs.clear(); + t.input_witnesses.clear(); + t.inputs + .insert(PlatformAddress::P2pkh([10u8; 20]), (1, u64::MAX)); + t.inputs + .insert(PlatformAddress::P2pkh([11u8; 20]), (1, u64::MAX)); + t.input_witnesses.push(make_witness()); + t.input_witnesses.push(make_witness()); + let r = t.validate_structure(pv()); + assert!(matches!( + r.errors.as_slice(), + [ConsensusError::BasicError(BasicError::OverflowError(_))] + )); + } + + #[test] + fn try_from_inner_roundtrip() { + let t = make_valid(); + let inner = IdentityCreateFromAddressesTransitionV0Inner { + public_keys: t.public_keys.clone(), + inputs: t.inputs.clone(), + output: t.output.clone(), + fee_strategy: t.fee_strategy.clone(), + user_fee_increase: t.user_fee_increase, + input_witnesses: t.input_witnesses.clone(), + }; + let back = IdentityCreateFromAddressesTransitionV0::try_from(inner).expect("try_from"); + assert_eq!(back, t); + } + + #[test] + fn into_state_transition_wraps_correctly() { + use crate::state_transition::identity_create_from_addresses_transition::IdentityCreateFromAddressesTransition; + let t = make_valid(); + let outer: IdentityCreateFromAddressesTransition = t.into(); + assert!(matches!( + outer, + IdentityCreateFromAddressesTransition::V0(_) + )); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_transition/v0/mod.rs index 2d507a332fb..5108230d276 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_transition/v0/mod.rs @@ -248,4 +248,129 @@ mod test { let obj = t.to_cleaned_object(false).expect("should work"); assert!(obj.is_map()); } + + fn chain_proof() -> AssetLockProof { + use crate::identity::state_transition::asset_lock_proof::chain::ChainAssetLockProof; + AssetLockProof::Chain(ChainAssetLockProof::new(42, [3u8; 36])) + } + + #[test] + fn try_from_inner_populates_identity_id_from_asset_lock_proof() { + let proof = chain_proof(); + let inner = IdentityCreateTransitionV0Inner { + public_keys: vec![], + asset_lock_proof: proof.clone(), + user_fee_increase: 0, + signature: BinaryData::new(vec![]), + }; + let t = IdentityCreateTransitionV0::try_from(inner).expect("try_from"); + let expected = proof.create_identifier().expect("create_identifier"); + assert_eq!(t.identity_id, expected); + } + + #[test] + fn asset_lock_proved_sets_identity_id_and_proof() { + let mut t = IdentityCreateTransitionV0 { + public_keys: vec![], + asset_lock_proof: chain_proof(), + user_fee_increase: 0, + signature: [0u8; 65].to_vec().into(), + identity_id: Identifier::random(), + }; + let original_identity_id = t.identity_id; + // Use a different chain proof (different outpoint) to produce a new identity_id. + use crate::identity::state_transition::asset_lock_proof::chain::ChainAssetLockProof; + let proof = AssetLockProof::Chain(ChainAssetLockProof::new(99, [7u8; 36])); + t.set_asset_lock_proof(proof.clone()) + .expect("set_asset_lock_proof"); + let expected_id = proof.create_identifier().expect("create_identifier"); + assert_eq!(t.identity_id, expected_id); + assert_ne!(original_identity_id, t.identity_id); + assert_eq!(t.asset_lock_proof(), &proof); + } + + #[test] + fn accessors_manipulate_public_keys() { + use crate::identity::{KeyType, Purpose, SecurityLevel}; + use crate::state_transition::public_key_in_creation::v0::IdentityPublicKeyInCreationV0; + use crate::state_transition::public_key_in_creation::IdentityPublicKeyInCreation; + let mut t = make_create_v0(); + assert!(t.public_keys().is_empty()); + + let key = IdentityPublicKeyInCreation::V0(IdentityPublicKeyInCreationV0 { + id: 0, + key_type: KeyType::ECDSA_SECP256K1, + purpose: Purpose::AUTHENTICATION, + security_level: SecurityLevel::MASTER, + contract_bounds: None, + read_only: false, + data: BinaryData::new(vec![0u8; 33]), + signature: BinaryData::new(vec![]), + }); + t.set_public_keys(vec![key.clone()]); + assert_eq!(t.public_keys().len(), 1); + + // Access mutable + let key2 = IdentityPublicKeyInCreation::V0(IdentityPublicKeyInCreationV0 { + id: 1, + key_type: KeyType::ECDSA_SECP256K1, + purpose: Purpose::AUTHENTICATION, + security_level: SecurityLevel::HIGH, + contract_bounds: None, + read_only: false, + data: BinaryData::new(vec![1u8; 33]), + signature: BinaryData::new(vec![]), + }); + let mut more = vec![key2.clone()]; + t.add_public_keys(&mut more); + assert_eq!(t.public_keys().len(), 2); + assert!(more.is_empty(), "add_public_keys drains input Vec"); + } + + #[test] + fn try_from_identity_v0_constructs_from_identity() { + use crate::identity::accessors::IdentityGettersV0; + use crate::identity::{Identity, IdentityV0}; + use std::collections::BTreeMap; + + let identity_v0 = IdentityV0 { + id: Identifier::random(), + public_keys: BTreeMap::new(), + balance: 0, + revision: 0, + }; + let identity: Identity = identity_v0.into(); + let proof = chain_proof(); + let t = IdentityCreateTransitionV0::try_from_identity_v0(&identity, proof.clone()) + .expect("try_from_identity_v0"); + assert!(t.public_keys.is_empty()); + assert_eq!(t.asset_lock_proof, proof); + // identity_id is from the proof, NOT the identity itself. + let expected = proof.create_identifier().expect("create_identifier"); + assert_eq!(t.identity_id, expected); + // Demonstrate: the transition's identity_id may differ from identity.id() + let _ = identity.id(); + } + + #[test] + fn try_from_identity_dispatches_v0_version() { + use crate::identity::{Identity, IdentityV0}; + use crate::version::LATEST_PLATFORM_VERSION; + use std::collections::BTreeMap; + + let identity_v0 = IdentityV0 { + id: Identifier::random(), + public_keys: BTreeMap::new(), + balance: 0, + revision: 0, + }; + let identity: Identity = identity_v0.into(); + let t = IdentityCreateTransitionV0::try_from_identity( + &identity, + chain_proof(), + LATEST_PLATFORM_VERSION, + ) + .expect("try_from_identity"); + assert!(t.public_keys.is_empty()); + } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/v0/mod.rs index 3d90ae7b0c6..6c29c9e005e 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/v0/mod.rs @@ -104,4 +104,81 @@ mod test { test_identity_credit_transfer_to_addresses_transition(transition); } + + fn make_transfer_to_addresses_v0() -> IdentityCreditTransferToAddressesTransitionV0 { + use crate::address_funds::PlatformAddress; + use std::collections::BTreeMap; + let mut recipient_addresses = BTreeMap::new(); + recipient_addresses.insert(PlatformAddress::P2pkh([5u8; 20]), 1_000_000); + recipient_addresses.insert(PlatformAddress::P2pkh([6u8; 20]), 2_000_000); + IdentityCreditTransferToAddressesTransitionV0 { + identity_id: Identifier::random(), + recipient_addresses, + nonce: 7, + user_fee_increase: 3, + signature_public_key_id: 2, + signature: [0u8; 65].to_vec().into(), + } + } + + #[test] + fn state_transition_like_basics() { + use crate::state_transition::{ + StateTransitionHasUserFeeIncrease, StateTransitionLike, StateTransitionOwned, + StateTransitionType, + }; + let mut t = make_transfer_to_addresses_v0(); + assert_eq!( + t.state_transition_type(), + StateTransitionType::IdentityCreditTransferToAddresses + ); + assert_eq!(t.state_transition_protocol_version(), 0); + assert_eq!(t.modified_data_ids(), vec![t.identity_id]); + assert_eq!(t.owner_id(), t.identity_id); + let ids = t.unique_identifiers(); + assert_eq!(ids.len(), 1); + // unique_identifier contains nonce in hex + assert!(ids[0].contains("7")); + assert_eq!(t.user_fee_increase(), 3); + t.set_user_fee_increase(77); + assert_eq!(t.user_fee_increase(), 77); + } + + #[test] + fn identity_signed_requirements() { + use crate::identity::{Purpose, SecurityLevel}; + use crate::state_transition::StateTransitionIdentitySigned; + let mut t = make_transfer_to_addresses_v0(); + assert_eq!(t.signature_public_key_id(), 2); + t.set_signature_public_key_id(42); + assert_eq!(t.signature_public_key_id(), 42); + assert_eq!( + t.security_level_requirement(Purpose::TRANSFER), + vec![SecurityLevel::CRITICAL] + ); + assert_eq!(t.purpose_requirement(), vec![Purpose::TRANSFER]); + } + + #[test] + fn single_signed_behaviour() { + use crate::state_transition::StateTransitionSingleSigned; + use platform_value::BinaryData; + let mut t = make_transfer_to_addresses_v0(); + assert_eq!(t.signature().len(), 65); + t.set_signature(BinaryData::new(vec![1, 2, 3])); + assert_eq!(t.signature().as_slice(), &[1, 2, 3]); + t.set_signature_bytes(vec![4, 5]); + assert_eq!(t.signature().as_slice(), &[4, 5]); + } + + #[test] + fn into_state_transition_wraps_correctly() { + use crate::state_transition::StateTransition; + let t = make_transfer_to_addresses_v0(); + let st: StateTransition = t.into(); + assert!(matches!( + st, + StateTransition::IdentityCreditTransferToAddresses(_) + )); + } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/v0/state_transition_validation.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/v0/state_transition_validation.rs index 22545963a81..65a9093c945 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/v0/state_transition_validation.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/v0/state_transition_validation.rs @@ -99,4 +99,77 @@ mod tests { )] if err.message() == "Recipient addresses sum overflow" ); } + + #[test] + fn should_return_invalid_result_if_no_recipients() { + let platform_version = PlatformVersion::latest(); + let transition = IdentityCreditTransferToAddressesTransitionV0::default(); + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [crate::consensus::ConsensusError::BasicError( + BasicError::TransitionNoOutputsError(_) + )] + ); + } + + #[test] + fn should_return_invalid_result_if_too_many_recipients() { + let platform_version = PlatformVersion::latest(); + let max = platform_version.dpp.state_transitions.max_address_outputs as u16; + let mut recipient_addresses = BTreeMap::new(); + for i in 0..=max { + let mut addr = [0u8; 20]; + addr[0] = (i & 0xff) as u8; + addr[1] = ((i >> 8) & 0xff) as u8; + recipient_addresses.insert(PlatformAddress::P2pkh(addr), 1_000_000); + } + let transition = IdentityCreditTransferToAddressesTransitionV0 { + recipient_addresses, + ..Default::default() + }; + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [crate::consensus::ConsensusError::BasicError( + BasicError::TransitionOverMaxOutputsError(_) + )] + ); + } + + #[test] + fn should_return_invalid_result_if_recipient_below_minimum() { + let platform_version = PlatformVersion::latest(); + let mut recipient_addresses = BTreeMap::new(); + recipient_addresses.insert(PlatformAddress::P2pkh([3u8; 20]), 1); + let transition = IdentityCreditTransferToAddressesTransitionV0 { + recipient_addresses, + ..Default::default() + }; + let result = transition.validate_structure(platform_version); + assert_matches!( + result.errors.as_slice(), + [crate::consensus::ConsensusError::BasicError( + BasicError::OutputBelowMinimumError(_) + )] + ); + } + + #[test] + fn should_pass_with_single_valid_recipient() { + let platform_version = PlatformVersion::latest(); + let min_output = platform_version + .dpp + .state_transitions + .address_funds + .min_output_amount; + let mut recipient_addresses = BTreeMap::new(); + recipient_addresses.insert(PlatformAddress::P2pkh([4u8; 20]), min_output); + let transition = IdentityCreditTransferToAddressesTransitionV0 { + recipient_addresses, + ..Default::default() + }; + let result = transition.validate_structure(platform_version); + assert!(result.is_valid(), "{:?}", result.errors); + } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_transition/v0/mod.rs index 5da1c542f1c..9a62843b924 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_transition/v0/mod.rs @@ -241,4 +241,54 @@ mod test { assert_eq!(transition.nonce, 0); assert_eq!(transition.user_fee_increase, 0); } + + #[test] + fn test_to_cleaned_object_skip_signature_removes_signature() { + use crate::state_transition::StateTransitionValueConvert; + let t = make_transfer_v0(); + let obj = t.to_cleaned_object(true).expect("should work"); + let map = obj.into_btree_string_map().expect("should be map"); + assert!(!map.contains_key("signature")); + } + + #[test] + fn test_to_canonical_cleaned_object_skip_signature_removes_signature() { + use crate::state_transition::StateTransitionValueConvert; + let t = make_transfer_v0(); + let obj = t.to_canonical_cleaned_object(true).expect("should work"); + let map = obj.into_btree_string_map().expect("should be map"); + assert!(!map.contains_key("signature")); + } + + #[test] + fn test_modified_data_ids_and_unique_identifiers() { + use crate::state_transition::StateTransitionLike; + let t = make_transfer_v0(); + let modified = t.modified_data_ids(); + assert_eq!(modified.len(), 2); + assert_eq!(modified[0], t.identity_id); + assert_eq!(modified[1], t.recipient_id); + let ids = t.unique_identifiers(); + assert_eq!(ids.len(), 1); + } + + #[test] + fn test_value_conversion_preserves_fields() { + use crate::state_transition::StateTransitionValueConvert; + use crate::version::LATEST_PLATFORM_VERSION; + let t = make_transfer_v0(); + let obj = t.to_object(false).expect("to_object"); + let map = obj.clone().into_btree_string_map().expect("should be map"); + assert!(map.contains_key("identityId")); + assert!(map.contains_key("recipientId")); + assert!(map.contains_key("amount")); + let restored = + IdentityCreditTransferTransitionV0::from_object(obj, LATEST_PLATFORM_VERSION) + .expect("from_object"); + assert_eq!(t.amount, restored.amount); + assert_eq!(t.identity_id, restored.identity_id); + assert_eq!(t.recipient_id, restored.recipient_id); + assert_eq!(t.nonce, restored.nonce); + assert_eq!(t.user_fee_increase, restored.user_fee_increase); + } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v0/mod.rs index ceff97c9ae2..d4c2316b63e 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v0/mod.rs @@ -421,4 +421,57 @@ mod test { .expect("should work"); assert_eq!(t, restored); } + + #[test] + fn test_to_object_skip_signature_removes_signature() { + use crate::state_transition::StateTransitionValueConvert; + let t = make_withdrawal_v0(); + let obj = t.to_object(true).expect("should work"); + let map = obj.into_btree_string_map().expect("should be a map"); + assert!(!map.contains_key("signature")); + } + + #[test] + fn test_to_cleaned_object_skip_signature() { + use crate::state_transition::StateTransitionValueConvert; + let t = make_withdrawal_v0(); + let obj = t.to_cleaned_object(true).expect("should work"); + let map = obj.into_btree_string_map().expect("should be a map"); + assert!(!map.contains_key("signature")); + } + + #[test] + fn test_to_canonical_cleaned_object_skip_signature() { + use crate::state_transition::StateTransitionValueConvert; + let t = make_withdrawal_v0(); + let obj = t.to_canonical_cleaned_object(true).expect("should work"); + let map = obj.into_btree_string_map().expect("should be a map"); + assert!(!map.contains_key("signature")); + } + + #[test] + fn test_pooling_roundtrip_never() { + use crate::state_transition::StateTransitionValueConvert; + use crate::version::LATEST_PLATFORM_VERSION; + let mut t = make_withdrawal_v0(); + t.pooling = Pooling::Never; + let obj = t.to_object(false).expect("to_object"); + let restored = + super::IdentityCreditWithdrawalTransitionV0::from_object(obj, LATEST_PLATFORM_VERSION) + .expect("from_object"); + assert_eq!(restored.pooling, Pooling::Never); + } + + #[test] + fn test_pooling_roundtrip_standard() { + use crate::state_transition::StateTransitionValueConvert; + use crate::version::LATEST_PLATFORM_VERSION; + let mut t = make_withdrawal_v0(); + t.pooling = Pooling::Standard; + let obj = t.to_object(false).expect("to_object"); + let restored = + super::IdentityCreditWithdrawalTransitionV0::from_object(obj, LATEST_PLATFORM_VERSION) + .expect("from_object"); + assert_eq!(restored.pooling, Pooling::Standard); + } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v1/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v1/mod.rs index 2bba4191747..01204d3b974 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v1/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v1/mod.rs @@ -210,4 +210,59 @@ mod test { let map = obj.into_btree_string_map().expect("should be map"); assert!(!map.contains_key("signature")); } + + #[test] + fn test_to_cleaned_object_skip_signature() { + let t = make_withdrawal_v1(); + let obj = t.to_cleaned_object(true).expect("should work"); + let map = obj.into_btree_string_map().expect("should be map"); + assert!(!map.contains_key("signature")); + } + + #[test] + fn test_to_canonical_cleaned_object_skip_signature() { + let t = make_withdrawal_v1(); + let obj = t.to_canonical_cleaned_object(true).expect("should work"); + let map = obj.into_btree_string_map().expect("should be map"); + assert!(!map.contains_key("signature")); + } + + #[test] + fn test_unique_identifier_includes_nonce() { + let t = make_withdrawal_v1(); + let ids = t.unique_identifiers(); + assert_eq!(ids.len(), 1); + // nonce=10 in hex is "a" + assert!(ids[0].ends_with("-a"), "got: {}", ids[0]); + } + + #[test] + fn test_into_state_transition_wraps_v1() { + use crate::state_transition::identity_credit_withdrawal_transition::IdentityCreditWithdrawalTransition; + use crate::state_transition::StateTransition; + let t = make_withdrawal_v1(); + let outer: IdentityCreditWithdrawalTransition = t.clone().into(); + assert!(matches!(outer, IdentityCreditWithdrawalTransition::V1(_))); + let st: StateTransition = t.into(); + assert!(matches!(st, StateTransition::IdentityCreditWithdrawal(_))); + } + + #[test] + fn test_owner_id_v1() { + let t = make_withdrawal_v1(); + assert_eq!(t.owner_id(), t.identity_id); + } + + #[test] + fn test_value_conversion_script_none_roundtrip_map() { + use crate::version::LATEST_PLATFORM_VERSION; + let t = make_withdrawal_v1_no_script(); + let obj = t.to_object(false).expect("to_object"); + let map = obj.into_btree_string_map().expect("should be map"); + let restored = + IdentityCreditWithdrawalTransitionV1::from_value_map(map, LATEST_PLATFORM_VERSION) + .expect("from_value_map"); + assert!(restored.output_script.is_none()); + assert_eq!(t, restored); + } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_from_addresses_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_from_addresses_transition/v0/mod.rs index aedc54be9f1..506969b696a 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_from_addresses_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_from_addresses_transition/v0/mod.rs @@ -37,3 +37,313 @@ pub struct IdentityTopUpFromAddressesTransitionV0 { #[platform_signable(exclude_from_sig_hash)] pub input_witnesses: Vec, } + +#[cfg(test)] +mod tests { + use super::*; + use crate::address_funds::AddressFundsFeeStrategyStep; + use crate::consensus::basic::BasicError; + use crate::consensus::ConsensusError; + use crate::state_transition::StateTransitionStructureValidation; + use platform_value::Identifier; + use platform_version::version::PlatformVersion; + + fn make_witness() -> AddressWitness { + AddressWitness::P2pkh { + signature: platform_value::BinaryData::new(vec![0u8; 65]), + } + } + + fn make_valid_v0() -> IdentityTopUpFromAddressesTransitionV0 { + // LATEST_PLATFORM_VERSION uses STATE_TRANSITION_VERSIONS_V3 which has + // max_address_inputs = 16. Earlier (v1) state transition versions set it to 0, + // which effectively disables these code paths. + let pv = pv(); + let min_input = pv.dpp.state_transitions.address_funds.min_input_amount; + let min_funding = pv + .dpp + .state_transitions + .address_funds + .min_identity_funding_amount; + let mut inputs = BTreeMap::new(); + inputs.insert( + PlatformAddress::P2pkh([1u8; 20]), + (1u32, min_input.max(min_funding) * 2), + ); + IdentityTopUpFromAddressesTransitionV0 { + inputs, + output: None, + identity_id: Identifier::random(), + fee_strategy: vec![AddressFundsFeeStrategyStep::DeductFromInput(0)], + user_fee_increase: 0, + input_witnesses: vec![make_witness()], + } + } + + fn pv() -> &'static PlatformVersion { + PlatformVersion::latest() + } + + #[test] + fn validate_structure_valid() { + let t = make_valid_v0(); + let result = t.validate_structure(pv()); + assert!(result.is_valid(), "{:?}", result.errors); + } + + #[test] + fn validate_structure_no_inputs() { + let mut t = make_valid_v0(); + t.inputs.clear(); + t.input_witnesses.clear(); + let result = t.validate_structure(pv()); + assert!(matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::TransitionNoInputsError(_) + )] + )); + } + + #[test] + fn validate_structure_input_witness_count_mismatch() { + let mut t = make_valid_v0(); + t.input_witnesses.clear(); + let result = t.validate_structure(pv()); + assert!(matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::InputWitnessCountMismatchError(_) + )] + )); + } + + #[test] + fn validate_structure_output_is_input_address() { + let mut t = make_valid_v0(); + let (addr, _) = t.inputs.iter().next().unwrap(); + t.output = Some((addr.clone(), 500_000)); + let result = t.validate_structure(pv()); + assert!(matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::OutputAddressAlsoInputError(_) + )] + )); + } + + #[test] + fn validate_structure_fee_strategy_empty() { + let mut t = make_valid_v0(); + t.fee_strategy.clear(); + let result = t.validate_structure(pv()); + assert!(matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::FeeStrategyEmptyError(_) + )] + )); + } + + #[test] + fn validate_structure_fee_strategy_duplicate() { + let mut t = make_valid_v0(); + t.fee_strategy = vec![ + AddressFundsFeeStrategyStep::DeductFromInput(0), + AddressFundsFeeStrategyStep::DeductFromInput(0), + ]; + let result = t.validate_structure(pv()); + assert!(matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::FeeStrategyDuplicateError(_) + )] + )); + } + + #[test] + fn validate_structure_fee_strategy_input_out_of_bounds() { + let mut t = make_valid_v0(); + t.fee_strategy = vec![AddressFundsFeeStrategyStep::DeductFromInput(99)]; + let result = t.validate_structure(pv()); + assert!(matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::FeeStrategyIndexOutOfBoundsError(_) + )] + )); + } + + #[test] + fn validate_structure_fee_strategy_reduce_output_out_of_bounds_when_no_output() { + let mut t = make_valid_v0(); + // No output set => ReduceOutput(0) must be out of bounds. + t.fee_strategy = vec![AddressFundsFeeStrategyStep::ReduceOutput(0)]; + let result = t.validate_structure(pv()); + assert!(matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::FeeStrategyIndexOutOfBoundsError(_) + )] + )); + } + + #[test] + fn validate_structure_input_below_minimum() { + let mut t = make_valid_v0(); + let addr = t.inputs.keys().next().cloned().unwrap(); + t.inputs.insert(addr, (1, 1)); // below min_input_amount + let result = t.validate_structure(pv()); + assert!(matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::InputBelowMinimumError(_) + )] + )); + } + + #[test] + fn validate_structure_output_below_minimum() { + let mut t = make_valid_v0(); + // Use a valid output address, but amount below min_output_amount (500_000) + t.output = Some((PlatformAddress::P2pkh([2u8; 20]), 1)); + let result = t.validate_structure(pv()); + assert!(matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::OutputBelowMinimumError(_) + )] + )); + } + + #[test] + fn validate_structure_input_sum_less_than_required() { + let mut t = make_valid_v0(); + // Set input to min_input_amount (well below min_identity_funding_amount=200_000? No, min is 100k, funding is 200k) + let pv = pv(); + let addr = t.inputs.keys().next().cloned().unwrap(); + t.inputs.insert( + addr, + (1, pv.dpp.state_transitions.address_funds.min_input_amount), + ); + let result = t.validate_structure(pv); + assert!(matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::InputsNotLessThanOutputsError(_) + )] + )); + } + + #[test] + fn validate_structure_inputs_overflow_when_summing() { + let mut t = make_valid_v0(); + t.inputs.clear(); + t.input_witnesses.clear(); + // Two u64::MAX inputs will overflow on addition. + t.inputs + .insert(PlatformAddress::P2pkh([10u8; 20]), (1, u64::MAX)); + t.inputs + .insert(PlatformAddress::P2pkh([11u8; 20]), (1, u64::MAX)); + t.input_witnesses.push(make_witness()); + t.input_witnesses.push(make_witness()); + let result = t.validate_structure(pv()); + assert!(matches!( + result.errors.as_slice(), + [ConsensusError::BasicError(BasicError::OverflowError(_))] + )); + } + + #[test] + fn validate_structure_too_many_fee_strategies() { + let mut t = make_valid_v0(); + // max_address_fee_strategies = 4 in state-transitions v3. We need >4 steps. + // Start fresh: expand inputs + witnesses consistently, then install 5 distinct + // fee-strategy steps whose indices are all within bounds. + t.inputs.clear(); + t.input_witnesses.clear(); + for i in 0..5u8 { + t.inputs + .insert(PlatformAddress::P2pkh([20 + i; 20]), (1, 500_000)); + t.input_witnesses.push(make_witness()); + } + t.fee_strategy = vec![ + AddressFundsFeeStrategyStep::DeductFromInput(0), + AddressFundsFeeStrategyStep::DeductFromInput(1), + AddressFundsFeeStrategyStep::DeductFromInput(2), + AddressFundsFeeStrategyStep::DeductFromInput(3), + AddressFundsFeeStrategyStep::DeductFromInput(4), + ]; + let result = t.validate_structure(pv()); + assert!( + matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::FeeStrategyTooManyStepsError(_) + )] + ), + "{:?}", + result.errors + ); + } + + #[test] + fn state_transition_like_basic() { + use crate::state_transition::{ + StateTransitionHasUserFeeIncrease, StateTransitionLike, StateTransitionOwned, + StateTransitionType, + }; + let mut t = make_valid_v0(); + assert_eq!( + t.state_transition_type(), + StateTransitionType::IdentityTopUpFromAddresses + ); + assert_eq!(t.state_transition_protocol_version(), 0); + assert_eq!(t.modified_data_ids(), vec![t.identity_id]); + assert_eq!(t.owner_id(), t.identity_id); + let ids = t.unique_identifiers(); + assert_eq!(ids.len(), 1); + assert!(!ids[0].is_empty()); + assert_eq!(t.user_fee_increase(), 0); + t.set_user_fee_increase(77); + assert_eq!(t.user_fee_increase(), 77); + } + + #[test] + fn witness_signed_accessors() { + use crate::state_transition::StateTransitionWitnessSigned; + let mut t = make_valid_v0(); + let original_inputs = StateTransitionWitnessSigned::inputs(&t).clone(); + assert_eq!(original_inputs.len(), 1); + let mut_ref = StateTransitionWitnessSigned::inputs_mut(&mut t); + mut_ref.clear(); + assert!(StateTransitionWitnessSigned::inputs(&t).is_empty()); + StateTransitionWitnessSigned::set_inputs(&mut t, original_inputs.clone()); + assert_eq!(StateTransitionWitnessSigned::inputs(&t).len(), 1); + let wits = StateTransitionWitnessSigned::witnesses(&t); + assert_eq!(wits.len(), 1); + StateTransitionWitnessSigned::set_witnesses(&mut t, vec![]); + assert_eq!(StateTransitionWitnessSigned::witnesses(&t).len(), 0); + } + + #[test] + fn into_state_transition_wraps_correctly() { + use crate::state_transition::StateTransition; + let t = make_valid_v0(); + let st: StateTransition = t.into(); + assert!(matches!(st, StateTransition::IdentityTopUpFromAddresses(_))); + } + + #[test] + fn default_accessors() { + use crate::state_transition::identity_topup_from_addresses_transition::accessors::IdentityTopUpFromAddressesTransitionAccessorsV0; + let mut t = IdentityTopUpFromAddressesTransitionV0::default(); + assert!(t.inputs.is_empty()); + let new_id = Identifier::random(); + t.set_identity_id(new_id); + assert_eq!(t.identity_id(), &new_id); + assert!(t.output().is_none()); + t.set_output(Some((PlatformAddress::P2pkh([9u8; 20]), 1))); + assert!(t.output().is_some()); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/public_key_in_creation/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/public_key_in_creation/mod.rs index 0d96aa21e4c..66405e80237 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/public_key_in_creation/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/public_key_in_creation/mod.rs @@ -379,4 +379,108 @@ mod test { let hash = key.hash().expect("should hash"); assert_eq!(hash.len(), 20); } + + #[test] + fn test_value_conversion_roundtrip() { + use crate::state_transition::StateTransitionValueConvert; + let key = make_master_key(0); + let v = StateTransitionValueConvert::to_object(&key, false).expect("to_object"); + assert!(v.is_map()); + let restored = ::from_object( + v, + LATEST_PLATFORM_VERSION, + ) + .expect("from_object"); + assert_eq!(key, restored); + } + + #[test] + fn test_value_conversion_unknown_version() { + use crate::state_transition::StateTransitionValueConvert; + use platform_value::Value; + let v = Value::from([("$version", Value::U16(255))]); + let result = ::from_object( + v, + LATEST_PLATFORM_VERSION, + ); + assert!(result.is_err()); + } + + #[test] + fn test_clean_value_unknown_version() { + use crate::state_transition::StateTransitionValueConvert; + use platform_value::Value; + let mut v = Value::from([("$version", Value::U8(255))]); + let result = + ::clean_value(&mut v); + assert!(result.is_err()); + } + + #[test] + fn test_to_canonical_object_inserts_version() { + use crate::state_transition::StateTransitionValueConvert; + let key = make_master_key(0); + let v = StateTransitionValueConvert::to_canonical_object(&key, false) + .expect("to_canonical_object"); + let map = v + .into_btree_string_map() + .expect("canonical object should be a map"); + assert!(map.contains_key("$version")); + } + + #[test] + fn test_to_canonical_cleaned_object_inserts_version() { + use crate::state_transition::StateTransitionValueConvert; + let key = make_master_key(0); + let v = StateTransitionValueConvert::to_canonical_cleaned_object(&key, false) + .expect("to_canonical_cleaned_object"); + let map = v.into_btree_string_map().expect("should be a map"); + assert!(map.contains_key("$version")); + } + + #[test] + fn test_from_value_map_roundtrip() { + use crate::state_transition::StateTransitionValueConvert; + let key = make_master_key(0); + let v = StateTransitionValueConvert::to_object(&key, false).expect("to_object"); + let map = v.into_btree_string_map().expect("should be a map"); + let restored = + ::from_value_map( + map, + LATEST_PLATFORM_VERSION, + ) + .expect("from_value_map"); + assert_eq!(key, restored); + } + + #[test] + fn test_default_versioned_unknown() { + use platform_version::version::PlatformVersion; + // Use a fresh version with an unknown identity_key_structure_version. + let mut pv = PlatformVersion::latest().clone(); + pv.dpp.identity_versions.identity_key_structure_version = 255; + let result = IdentityPublicKeyInCreation::default_versioned(&pv); + assert!(result.is_err()); + } + + #[test] + fn test_hash_duplicate_in_keys_witness() { + let key1 = make_master_key(0); + // Identical data to key1 but different id + let key2 = IdentityPublicKeyInCreation::V0(IdentityPublicKeyInCreationV0 { + id: 1, + key_type: KeyType::ECDSA_SECP256K1, + purpose: Purpose::AUTHENTICATION, + security_level: SecurityLevel::HIGH, + contract_bounds: None, + read_only: false, + data: BinaryData::new(vec![0u8; 33]), + signature: BinaryData::new(vec![]), + }); + let keys = vec![key1, key2]; + let dup_ids = + IdentityPublicKeyInCreation::duplicated_keys_witness(&keys, LATEST_PLATFORM_VERSION) + .expect("witness"); + assert_eq!(dup_ids.len(), 1, "one duplicate expected"); + } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v0/mod.rs index 95ed73a548f..b0215403a9a 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v0/mod.rs @@ -98,4 +98,97 @@ mod tests { test_round_trip(transition); } + + fn make_v0() -> ShieldFromAssetLockTransitionV0 { + ShieldFromAssetLockTransitionV0 { + asset_lock_proof: AssetLockProof::Chain(ChainAssetLockProof { + core_chain_locked_height: 100, + out_point: OutPoint::from([11u8; 36]), + }), + actions: vec![SerializedAction { + nullifier: [1u8; 32], + rk: [2u8; 32], + cmx: [3u8; 32], + encrypted_note: vec![4u8; 216], + cv_net: [5u8; 32], + spend_auth_sig: [6u8; 64], + }], + value_balance: 1000u64, + anchor: [7u8; 32], + proof: vec![8u8; 100], + binding_signature: [9u8; 64], + signature: BinaryData::new(vec![10u8; 65]), + } + } + + #[test] + fn test_state_transition_type() { + use crate::state_transition::{StateTransitionLike, StateTransitionType}; + let t = make_v0(); + assert_eq!( + t.state_transition_type(), + StateTransitionType::ShieldFromAssetLock + ); + } + + #[test] + fn test_state_transition_protocol_version() { + use crate::state_transition::StateTransitionLike; + let t = make_v0(); + assert_eq!(t.state_transition_protocol_version(), 0); + } + + #[test] + fn test_state_transition_modified_data_ids_empty() { + use crate::state_transition::StateTransitionLike; + let t = make_v0(); + assert!(t.modified_data_ids().is_empty()); + } + + #[test] + fn test_state_transition_unique_identifiers_is_base64_asset_lock() { + use crate::state_transition::StateTransitionLike; + let t = make_v0(); + let ids = t.unique_identifiers(); + assert_eq!(ids.len(), 1); + // Should be valid non-empty base64 (not the error fallback) + assert!(!ids[0].is_empty()); + } + + #[test] + fn test_signature_setters() { + use crate::state_transition::StateTransitionSingleSigned; + let mut t = make_v0(); + assert_eq!(t.signature().as_slice(), &vec![10u8; 65][..]); + t.set_signature(BinaryData::new(vec![99u8; 65])); + assert_eq!(t.signature().as_slice(), &vec![99u8; 65][..]); + t.set_signature_bytes(vec![1u8; 65]); + assert_eq!(t.signature().as_slice(), &vec![1u8; 65][..]); + } + + #[test] + fn test_asset_lock_proof_accessor_and_setter() { + use crate::identity::state_transition::AssetLockProved; + let mut t = make_v0(); + match t.asset_lock_proof() { + AssetLockProof::Chain(_) => {} + _ => panic!("expected Chain"), + } + t.set_asset_lock_proof(AssetLockProof::Chain(ChainAssetLockProof { + core_chain_locked_height: 999, + out_point: OutPoint::from([0xAB_u8; 36]), + })) + .unwrap(); + if let AssetLockProof::Chain(c) = t.asset_lock_proof() { + assert_eq!(c.core_chain_locked_height, 999); + } else { + panic!("expected Chain"); + } + } + + #[test] + fn test_feature_versioned() { + use crate::state_transition::FeatureVersioned; + assert_eq!(make_v0().feature_version(), 0); + } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_transfer_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_transfer_transition/v0/mod.rs index c86f1abb600..77606d4c713 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_transfer_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_transfer_transition/v0/mod.rs @@ -80,4 +80,72 @@ mod tests { test_round_trip(transition); } + + fn make_v0(actions: Vec) -> ShieldedTransferTransitionV0 { + ShieldedTransferTransitionV0 { + actions, + value_balance: 1000u64, + anchor: [7u8; 32], + proof: vec![8u8; 100], + binding_signature: [9u8; 64], + } + } + + fn mk_action(nullifier_byte: u8) -> SerializedAction { + SerializedAction { + nullifier: [nullifier_byte; 32], + rk: [2u8; 32], + cmx: [3u8; 32], + encrypted_note: vec![4u8; 216], + cv_net: [5u8; 32], + spend_auth_sig: [6u8; 64], + } + } + + #[test] + fn test_state_transition_type() { + use crate::state_transition::{StateTransitionLike, StateTransitionType}; + let t = make_v0(vec![mk_action(1)]); + assert_eq!( + t.state_transition_type(), + StateTransitionType::ShieldedTransfer + ); + } + + #[test] + fn test_state_transition_protocol_version() { + use crate::state_transition::StateTransitionLike; + let t = make_v0(vec![mk_action(1)]); + assert_eq!(t.state_transition_protocol_version(), 0); + } + + #[test] + fn test_state_transition_modified_data_ids_empty() { + use crate::state_transition::StateTransitionLike; + let t = make_v0(vec![mk_action(1)]); + assert!(t.modified_data_ids().is_empty()); + } + + #[test] + fn test_unique_identifiers_from_nullifiers() { + use crate::state_transition::StateTransitionLike; + let t = make_v0(vec![mk_action(0xAB), mk_action(0xCD)]); + let ids = t.unique_identifiers(); + assert_eq!(ids.len(), 2); + assert_eq!(ids[0], hex::encode([0xABu8; 32])); + assert_eq!(ids[1], hex::encode([0xCDu8; 32])); + } + + #[test] + fn test_unique_identifiers_no_actions() { + use crate::state_transition::StateTransitionLike; + let t = make_v0(vec![]); + assert!(t.unique_identifiers().is_empty()); + } + + #[test] + fn test_feature_versioned() { + use crate::state_transition::FeatureVersioned; + assert_eq!(make_v0(vec![mk_action(1)]).feature_version(), 0); + } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_withdrawal_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_withdrawal_transition/v0/mod.rs index 4f8fb7b937f..4599f854c61 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_withdrawal_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_withdrawal_transition/v0/mod.rs @@ -91,4 +91,75 @@ mod tests { test_round_trip(transition); } + + fn mk_action(nullifier_byte: u8) -> SerializedAction { + SerializedAction { + nullifier: [nullifier_byte; 32], + rk: [2u8; 32], + cmx: [3u8; 32], + encrypted_note: vec![4u8; 216], + cv_net: [5u8; 32], + spend_auth_sig: [6u8; 64], + } + } + + fn make_v0(actions: Vec) -> ShieldedWithdrawalTransitionV0 { + ShieldedWithdrawalTransitionV0 { + actions, + unshielding_amount: 1_000_000u64, + anchor: [7u8; 32], + proof: vec![8u8; 100], + binding_signature: [9u8; 64], + core_fee_per_byte: 2u32, + pooling: Pooling::Never, + output_script: CoreScript::new_p2pkh([11u8; 20]), + } + } + + #[test] + fn test_state_transition_type() { + use crate::state_transition::{StateTransitionLike, StateTransitionType}; + let t = make_v0(vec![mk_action(1)]); + assert_eq!( + t.state_transition_type(), + StateTransitionType::ShieldedWithdrawal + ); + } + + #[test] + fn test_state_transition_protocol_version() { + use crate::state_transition::StateTransitionLike; + let t = make_v0(vec![mk_action(1)]); + assert_eq!(t.state_transition_protocol_version(), 0); + } + + #[test] + fn test_state_transition_modified_data_ids_empty() { + use crate::state_transition::StateTransitionLike; + let t = make_v0(vec![mk_action(1)]); + assert!(t.modified_data_ids().is_empty()); + } + + #[test] + fn test_unique_identifiers_from_nullifiers() { + use crate::state_transition::StateTransitionLike; + let t = make_v0(vec![mk_action(0x7F), mk_action(0x80)]); + let ids = t.unique_identifiers(); + assert_eq!(ids.len(), 2); + assert_eq!(ids[0], hex::encode([0x7Fu8; 32])); + assert_eq!(ids[1], hex::encode([0x80u8; 32])); + } + + #[test] + fn test_unique_identifiers_empty() { + use crate::state_transition::StateTransitionLike; + let t = make_v0(vec![]); + assert!(t.unique_identifiers().is_empty()); + } + + #[test] + fn test_feature_versioned() { + use crate::state_transition::FeatureVersioned; + assert_eq!(make_v0(vec![mk_action(1)]).feature_version(), 0); + } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/unshield_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/unshield_transition/v0/mod.rs index f9cb9d25bf6..e9bffbcc064 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/unshield_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/unshield_transition/v0/mod.rs @@ -87,4 +87,70 @@ mod tests { test_round_trip(transition); } + + fn mk_action(nullifier_byte: u8) -> SerializedAction { + SerializedAction { + nullifier: [nullifier_byte; 32], + rk: [2u8; 32], + cmx: [3u8; 32], + encrypted_note: vec![4u8; 216], + cv_net: [5u8; 32], + spend_auth_sig: [6u8; 64], + } + } + + fn make_v0(actions: Vec) -> UnshieldTransitionV0 { + UnshieldTransitionV0 { + output_address: PlatformAddress::P2pkh([1u8; 20]), + actions, + unshielding_amount: 1000u64, + anchor: [7u8; 32], + proof: vec![8u8; 100], + binding_signature: [9u8; 64], + } + } + + #[test] + fn test_state_transition_type() { + use crate::state_transition::{StateTransitionLike, StateTransitionType}; + let t = make_v0(vec![mk_action(1)]); + assert_eq!(t.state_transition_type(), StateTransitionType::Unshield); + } + + #[test] + fn test_state_transition_protocol_version() { + use crate::state_transition::StateTransitionLike; + let t = make_v0(vec![mk_action(1)]); + assert_eq!(t.state_transition_protocol_version(), 0); + } + + #[test] + fn test_state_transition_modified_data_ids_empty() { + use crate::state_transition::StateTransitionLike; + let t = make_v0(vec![mk_action(1)]); + assert!(t.modified_data_ids().is_empty()); + } + + #[test] + fn test_unique_identifiers_from_nullifiers_multiple() { + use crate::state_transition::StateTransitionLike; + let t = make_v0(vec![mk_action(0x11), mk_action(0x22), mk_action(0x33)]); + let ids = t.unique_identifiers(); + assert_eq!(ids.len(), 3); + assert_eq!(ids[0], hex::encode([0x11u8; 32])); + assert_eq!(ids[2], hex::encode([0x33u8; 32])); + } + + #[test] + fn test_unique_identifiers_empty_actions() { + use crate::state_transition::StateTransitionLike; + let t = make_v0(vec![]); + assert!(t.unique_identifiers().is_empty()); + } + + #[test] + fn test_feature_versioned() { + use crate::state_transition::FeatureVersioned; + assert_eq!(make_v0(vec![mk_action(1)]).feature_version(), 0); + } }