diff --git a/packages/rs-drive/src/drive/identity/balance/update.rs b/packages/rs-drive/src/drive/identity/balance/update.rs index aed30f9b248..aceadf267eb 100644 --- a/packages/rs-drive/src/drive/identity/balance/update.rs +++ b/packages/rs-drive/src/drive/identity/balance/update.rs @@ -932,4 +932,74 @@ mod tests { )); } } + + mod remove_from_identity_balance_errors { + use super::*; + use crate::error::identity::IdentityError; + use dpp::block::block_info::BlockInfo; + use dpp::version::PlatformVersion; + + #[test] + fn should_fail_to_remove_more_than_balance() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let identity = create_test_identity(&drive, [0; 32], Some(15), None, platform_version) + .expect("expected to create an identity"); + + // Identity starts with balance from create_test_identity (which is 0 after creation + // since create_test_identity sets balance to 0). Let's add some balance first. + let added_balance = 100; + + drive + .add_to_identity_balance( + identity.id().to_buffer(), + added_balance, + &BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("expected to add balance"); + + // Now try to remove more than available + let result = drive.remove_from_identity_balance( + identity.id().to_buffer(), + added_balance + 1, + &BlockInfo::default(), + true, + None, + platform_version, + None, + ); + + assert!(matches!( + result, + Err(Error::Identity(IdentityError::IdentityInsufficientBalance( + _ + ))) + )); + } + + #[test] + fn should_fail_to_remove_from_non_existent_identity() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let result = drive.remove_from_identity_balance( + [0; 32], + 100, + &BlockInfo::default(), + true, + None, + platform_version, + None, + ); + + assert!(matches!( + result, + Err(Error::Drive(DriveError::CorruptedCodeExecution(_))) + )); + } + } } diff --git a/packages/rs-drive/src/drive/identity/fetch/balance/mod.rs b/packages/rs-drive/src/drive/identity/fetch/balance/mod.rs index 742b0346e28..81bf17e6978 100644 --- a/packages/rs-drive/src/drive/identity/fetch/balance/mod.rs +++ b/packages/rs-drive/src/drive/identity/fetch/balance/mod.rs @@ -1,3 +1,237 @@ mod fetch_identity_balance; mod fetch_identity_balance_include_debt; mod fetch_identity_negative_balance; + +#[cfg(feature = "server")] +#[cfg(test)] +mod tests { + use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; + use crate::util::test_helpers::test_utils::identities::create_test_identity; + use dpp::block::block_info::BlockInfo; + use dpp::identity::accessors::IdentityGettersV0; + use dpp::identity::Identity; + use dpp::version::PlatformVersion; + + mod fetch_identity_balance { + use super::*; + + #[test] + fn should_return_none_for_non_existent_identity() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let balance = drive + .fetch_identity_balance([0; 32], None, platform_version) + .expect("should not error"); + + assert!(balance.is_none()); + } + + #[test] + fn should_return_balance_for_existing_identity() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let identity = Identity::random_identity(3, Some(42), platform_version) + .expect("expected a random identity"); + + let expected_balance = identity.balance(); + + drive + .add_new_identity( + identity.clone(), + false, + &BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("expected to add identity"); + + let balance = drive + .fetch_identity_balance(identity.id().to_buffer(), None, platform_version) + .expect("should not error") + .expect("should have balance"); + + assert_eq!(balance, expected_balance); + } + + #[test] + fn should_return_balance_with_costs_estimated() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let identity = Identity::random_identity(3, Some(42), platform_version) + .expect("expected a random identity"); + + drive + .add_new_identity( + identity.clone(), + false, + &BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("expected to add identity"); + + let block_info = BlockInfo::default(); + + // Estimated mode (apply=false) + let (balance, fee_result) = drive + .fetch_identity_balance_with_costs( + identity.id().to_buffer(), + &block_info, + false, + None, + platform_version, + ) + .expect("should return balance with costs"); + + // In estimated (stateless) mode, the balance query does not read + // from the database. It returns Some(0) as a placeholder value + // while computing estimated costs. + assert!(fee_result.processing_fee > 0); + assert_eq!(balance, Some(0)); + } + } + + mod fetch_identity_balance_include_debt { + use super::*; + use crate::fees::op::LowLevelDriveOperation; + + #[test] + fn should_return_none_for_non_existent_identity() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let balance = drive + .fetch_identity_balance_include_debt([0; 32], None, platform_version) + .expect("should not error"); + + assert!(balance.is_none()); + } + + #[test] + fn should_return_positive_balance_for_identity_with_funds() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let identity = create_test_identity(&drive, [0; 32], Some(1), None, platform_version) + .expect("expected an identity"); + + let added_balance = 1000; + + drive + .add_to_identity_balance( + identity.id().to_buffer(), + added_balance, + &BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("should add balance"); + + let balance = drive + .fetch_identity_balance_include_debt( + identity.id().to_buffer(), + None, + platform_version, + ) + .expect("should not error") + .expect("should have balance"); + + assert_eq!(balance, added_balance as i64); + } + + #[test] + fn should_return_negative_balance_for_identity_with_debt() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let identity = create_test_identity(&drive, [0; 32], Some(1), None, platform_version) + .expect("expected an identity"); + + let negative_amount: u64 = 500; + + // Set negative balance (debt) + let batch = vec![drive + .update_identity_negative_credit_operation( + identity.id().to_buffer(), + negative_amount, + platform_version, + ) + .expect("expected operation")]; + + let mut drive_operations: Vec = vec![]; + drive + .apply_batch_low_level_drive_operations( + None, + None, + batch, + &mut drive_operations, + &platform_version.drive, + ) + .expect("should apply batch"); + + let balance = drive + .fetch_identity_balance_include_debt( + identity.id().to_buffer(), + None, + platform_version, + ) + .expect("should not error") + .expect("should have balance"); + + assert_eq!(balance, -(negative_amount as i64)); + } + } + + mod fetch_identity_negative_balance { + use super::*; + + #[test] + fn should_error_for_non_existent_identity() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let mut drive_operations = vec![]; + // For a non-existent identity, the path doesn't exist, so grove_get_raw + // returns an error (PathParentLayerNotFound) since the identity subtree + // doesn't exist at all. + let result = drive.fetch_identity_negative_balance_operations( + [0; 32], + true, + None, + &mut drive_operations, + platform_version, + ); + + assert!(result.is_err()); + } + + #[test] + fn should_return_zero_negative_balance_for_new_identity() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let identity = create_test_identity(&drive, [0; 32], Some(1), None, platform_version) + .expect("expected an identity"); + + let mut drive_operations = vec![]; + let negative_balance = drive + .fetch_identity_negative_balance_operations( + identity.id().to_buffer(), + true, + None, + &mut drive_operations, + platform_version, + ) + .expect("should not error") + .expect("should have negative balance"); + + assert_eq!(negative_balance, 0); + } + } +} diff --git a/packages/rs-drive/src/drive/identity/fetch/mod.rs b/packages/rs-drive/src/drive/identity/fetch/mod.rs index 5426940c949..b83d851790d 100644 --- a/packages/rs-drive/src/drive/identity/fetch/mod.rs +++ b/packages/rs-drive/src/drive/identity/fetch/mod.rs @@ -271,3 +271,234 @@ impl Drive { .collect() } } + +#[cfg(feature = "server")] +#[cfg(test)] +mod tests { + use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; + use dpp::block::block_info::BlockInfo; + use dpp::identity::accessors::IdentityGettersV0; + use dpp::identity::Identity; + use dpp::version::PlatformVersion; + + mod verify_all_identities_exist { + use super::*; + + #[test] + fn should_return_true_when_all_identities_exist() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let identities: Vec = + Identity::random_identities(3, 3, Some(42), platform_version) + .expect("expected random identities"); + + let ids: Vec<[u8; 32]> = identities.iter().map(|i| i.id().to_buffer()).collect(); + + for identity in &identities { + drive + .add_new_identity( + identity.clone(), + false, + &BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("expected to add identity"); + } + + let result = drive + .verify_all_identities_exist(&ids, None, platform_version) + .expect("should not error"); + + assert!(result); + } + + #[test] + fn should_return_false_when_some_identities_missing() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let identity = Identity::random_identity(3, Some(42), platform_version) + .expect("expected a random identity"); + + drive + .add_new_identity( + identity.clone(), + false, + &BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("expected to add identity"); + + // Include a nonexistent identity id + let ids = vec![identity.id().to_buffer(), [0xff; 32]]; + + let result = drive + .verify_all_identities_exist(&ids, None, platform_version) + .expect("should not error"); + + assert!(!result); + } + } + + mod fetch_identities_balances { + use super::*; + + #[test] + fn should_fetch_balances_for_existing_identities() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let identities: Vec = + Identity::random_identities(3, 3, Some(42), platform_version) + .expect("expected random identities"); + + for identity in &identities { + drive + .add_new_identity( + identity.clone(), + false, + &BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("expected to add identity"); + } + + let ids: Vec<[u8; 32]> = identities.iter().map(|i| i.id().to_buffer()).collect(); + + let balances = drive + .fetch_identities_balances(&ids, None, platform_version) + .expect("should fetch balances"); + + assert_eq!(balances.len(), 3); + for identity in &identities { + let balance = balances + .get(&identity.id().to_buffer()) + .expect("should have balance for identity"); + assert_eq!(*balance, identity.balance()); + } + } + } + + mod fetch_optional_identities_balances { + use super::*; + + #[test] + fn should_return_none_for_non_existent_identities() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let identity = Identity::random_identity(3, Some(42), platform_version) + .expect("expected a random identity"); + + drive + .add_new_identity( + identity.clone(), + false, + &BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("expected to add identity"); + + let ids = vec![identity.id().to_buffer(), [0xff; 32]]; + + let balances = drive + .fetch_optional_identities_balances(&ids, None, platform_version) + .expect("should fetch optional balances"); + + assert_eq!(balances.len(), 2); + assert_eq!( + *balances.get(&identity.id().to_buffer()).unwrap(), + Some(identity.balance()) + ); + assert_eq!(*balances.get(&[0xff; 32]).unwrap(), None); + } + } + + mod fetch_many_identity_balances_by_range { + use super::*; + use std::collections::BTreeMap; + + #[test] + fn should_fetch_balances_by_range_ascending() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let identities: Vec = + Identity::random_identities(5, 3, Some(42), platform_version) + .expect("expected random identities"); + + for identity in &identities { + drive + .add_new_identity( + identity.clone(), + false, + &BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("expected to add identity"); + } + + let balances: BTreeMap<[u8; 32], u64> = drive + .fetch_many_identity_balances_by_range::>( + None, + true, + 10, + None, + platform_version, + ) + .expect("should fetch balances by range"); + + assert_eq!(balances.len(), 5); + + // BTreeMap keys are always sorted, confirming ascending order + let keys: Vec<[u8; 32]> = balances.keys().copied().collect(); + assert!(keys.windows(2).all(|w| w[0] <= w[1])); + } + + #[test] + fn should_respect_limit() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let identities: Vec = + Identity::random_identities(5, 3, Some(42), platform_version) + .expect("expected random identities"); + + for identity in &identities { + drive + .add_new_identity( + identity.clone(), + false, + &BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("expected to add identity"); + } + + let balances: BTreeMap<[u8; 32], u64> = drive + .fetch_many_identity_balances_by_range::>( + None, + true, + 2, + None, + platform_version, + ) + .expect("should fetch balances by range"); + + assert_eq!(balances.len(), 2); + } + } +} diff --git a/packages/rs-drive/src/drive/identity/fetch/nonce/mod.rs b/packages/rs-drive/src/drive/identity/fetch/nonce/mod.rs index 6eea4fe165d..51f14fe855c 100644 --- a/packages/rs-drive/src/drive/identity/fetch/nonce/mod.rs +++ b/packages/rs-drive/src/drive/identity/fetch/nonce/mod.rs @@ -1,2 +1,132 @@ mod fetch_identity_nonce; mod prove_identity_nonce; + +#[cfg(feature = "server")] +#[cfg(test)] +mod tests { + use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; + use dpp::block::block_info::BlockInfo; + use dpp::identity::accessors::IdentityGettersV0; + use dpp::identity::Identity; + use dpp::version::PlatformVersion; + + mod fetch_identity_nonce { + use super::*; + + #[test] + fn should_return_none_for_non_existent_identity() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let nonce = drive + .fetch_identity_nonce([0; 32], true, None, platform_version) + .expect("should not error"); + + assert!(nonce.is_none()); + } + + #[test] + fn should_return_initial_nonce_for_new_identity() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let identity = Identity::random_identity(3, Some(42), platform_version) + .expect("expected a random identity"); + + drive + .add_new_identity( + identity.clone(), + false, + &BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("expected to add identity"); + + let nonce = drive + .fetch_identity_nonce(identity.id().to_buffer(), true, None, platform_version) + .expect("should not error") + .expect("should have nonce"); + + // New identity should have nonce 0 + assert_eq!(nonce, 0); + } + + #[test] + fn should_return_updated_nonce_after_merge() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let identity = Identity::random_identity(3, Some(42), platform_version) + .expect("expected a random identity"); + + drive + .add_new_identity( + identity.clone(), + false, + &BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("expected to add identity"); + + // Merge nonce to update it + drive + .merge_identity_nonce( + identity.id().to_buffer(), + 1, + &BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("expected to merge nonce"); + + let nonce = drive + .fetch_identity_nonce(identity.id().to_buffer(), true, None, platform_version) + .expect("should not error") + .expect("should have nonce"); + + // After merging nonce 1, the nonce value should contain 1 + // The nonce encoding includes missing revision bits, so just check it's nonzero + assert!(nonce > 0); + } + + #[test] + fn should_return_nonce_with_fees() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let identity = Identity::random_identity(3, Some(42), platform_version) + .expect("expected a random identity"); + + drive + .add_new_identity( + identity.clone(), + false, + &BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("expected to add identity"); + + let block_info = BlockInfo::default(); + + let (nonce, fee_result) = drive + .fetch_identity_nonce_with_fees( + identity.id().to_buffer(), + &block_info, + true, + None, + platform_version, + ) + .expect("should not error"); + + assert_eq!(nonce, Some(0)); + assert!(fee_result.processing_fee > 0); + } + } +} diff --git a/packages/rs-drive/src/drive/identity/fetch/partial_identity/mod.rs b/packages/rs-drive/src/drive/identity/fetch/partial_identity/mod.rs index 00576a74355..d511142620e 100644 --- a/packages/rs-drive/src/drive/identity/fetch/partial_identity/mod.rs +++ b/packages/rs-drive/src/drive/identity/fetch/partial_identity/mod.rs @@ -3,3 +3,370 @@ mod fetch_identity_balance_with_keys_and_revision; mod fetch_identity_keys; mod fetch_identity_revision_with_keys; mod fetch_identity_with_balance; + +#[cfg(feature = "server")] +#[cfg(test)] +mod tests { + use crate::drive::identity::key::fetch::{IdentityKeysRequest, KeyRequestType}; + use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; + use dpp::block::block_info::BlockInfo; + use dpp::identity::accessors::IdentityGettersV0; + use dpp::identity::Identity; + use dpp::version::PlatformVersion; + + mod fetch_identity_with_balance { + use super::*; + + #[test] + fn should_return_none_for_non_existent_identity() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let result = drive + .fetch_identity_with_balance([0; 32], None, platform_version) + .expect("should not error"); + + assert!(result.is_none()); + } + + #[test] + fn should_return_partial_identity_with_balance() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let identity = Identity::random_identity(3, Some(42), platform_version) + .expect("expected a random identity"); + + let expected_balance = identity.balance(); + + drive + .add_new_identity( + identity.clone(), + false, + &BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("expected to add identity"); + + let partial = drive + .fetch_identity_with_balance(identity.id().to_buffer(), None, platform_version) + .expect("should not error") + .expect("should have partial identity"); + + assert_eq!(partial.id, identity.id().clone()); + assert_eq!(partial.balance, Some(expected_balance)); + assert!(partial.loaded_public_keys.is_empty()); + assert!(partial.revision.is_none()); + } + + #[test] + fn should_return_none_with_cost_when_not_applying() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let (partial, fee_result) = drive + .fetch_identity_with_balance_with_cost([0; 32], false, None, platform_version) + .expect("should not error"); + + assert!(partial.is_none()); + assert!(fee_result.processing_fee > 0); + } + } + + mod fetch_identity_balance_with_keys { + use super::*; + + #[test] + fn should_return_none_for_non_existent_identity() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let key_request = IdentityKeysRequest { + identity_id: [0; 32], + request_type: KeyRequestType::AllKeys, + limit: None, + offset: None, + }; + + let result = drive + .fetch_identity_balance_with_keys(key_request, None, platform_version) + .expect("should not error"); + + assert!(result.is_none()); + } + + #[test] + fn should_return_partial_identity_with_balance_and_keys() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let identity = Identity::random_identity(3, Some(42), platform_version) + .expect("expected a random identity"); + + drive + .add_new_identity( + identity.clone(), + false, + &BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("expected to add identity"); + + let key_request = IdentityKeysRequest { + identity_id: identity.id().to_buffer(), + request_type: KeyRequestType::AllKeys, + limit: None, + offset: None, + }; + + let partial = drive + .fetch_identity_balance_with_keys(key_request, None, platform_version) + .expect("should not error") + .expect("should have partial identity"); + + assert_eq!(partial.id, identity.id().clone()); + assert_eq!(partial.balance, Some(identity.balance())); + assert_eq!(partial.loaded_public_keys.len(), 3); + assert!(partial.revision.is_none()); + } + + #[test] + fn should_return_partial_identity_with_specific_keys() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let identity = Identity::random_identity(5, Some(42), platform_version) + .expect("expected a random identity"); + + drive + .add_new_identity( + identity.clone(), + false, + &BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("expected to add identity"); + + let key_request = IdentityKeysRequest { + identity_id: identity.id().to_buffer(), + request_type: KeyRequestType::SpecificKeys(vec![0, 1]), + limit: Some(2), + offset: None, + }; + + let partial = drive + .fetch_identity_balance_with_keys(key_request, None, platform_version) + .expect("should not error") + .expect("should have partial identity"); + + assert_eq!(partial.loaded_public_keys.len(), 2); + assert!(partial.loaded_public_keys.contains_key(&0)); + assert!(partial.loaded_public_keys.contains_key(&1)); + } + + #[test] + fn should_track_not_found_keys() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let identity = Identity::random_identity(3, Some(42), platform_version) + .expect("expected a random identity"); + + drive + .add_new_identity( + identity.clone(), + false, + &BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("expected to add identity"); + + // Request key id 99 which does not exist + let key_request = IdentityKeysRequest { + identity_id: identity.id().to_buffer(), + request_type: KeyRequestType::SpecificKeys(vec![0, 99]), + limit: Some(2), + offset: None, + }; + + let partial = drive + .fetch_identity_balance_with_keys(key_request, None, platform_version) + .expect("should not error") + .expect("should have partial identity"); + + assert_eq!(partial.loaded_public_keys.len(), 1); + assert!(partial.loaded_public_keys.contains_key(&0)); + assert_eq!(partial.not_found_public_keys.len(), 1); + assert!(partial.not_found_public_keys.contains(&99)); + } + } + + mod fetch_identity_balance_with_keys_and_revision { + use super::*; + + #[test] + fn should_return_none_for_non_existent_identity() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let key_request = IdentityKeysRequest { + identity_id: [0; 32], + request_type: KeyRequestType::AllKeys, + limit: None, + offset: None, + }; + + let result = drive + .fetch_identity_balance_with_keys_and_revision(key_request, None, platform_version) + .expect("should not error"); + + assert!(result.is_none()); + } + + #[test] + fn should_return_partial_identity_with_balance_keys_and_revision() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let identity = Identity::random_identity(3, Some(42), platform_version) + .expect("expected a random identity"); + + drive + .add_new_identity( + identity.clone(), + false, + &BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("expected to add identity"); + + let key_request = IdentityKeysRequest { + identity_id: identity.id().to_buffer(), + request_type: KeyRequestType::AllKeys, + limit: None, + offset: None, + }; + + let partial = drive + .fetch_identity_balance_with_keys_and_revision(key_request, None, platform_version) + .expect("should not error") + .expect("should have partial identity"); + + assert_eq!(partial.id, identity.id().clone()); + assert_eq!(partial.balance, Some(identity.balance())); + assert_eq!(partial.revision, Some(identity.revision())); + assert_eq!(partial.loaded_public_keys.len(), 3); + } + } + + mod fetch_identity_revision_with_keys { + use super::*; + + #[test] + fn should_return_none_for_non_existent_identity() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let key_request = IdentityKeysRequest { + identity_id: [0; 32], + request_type: KeyRequestType::AllKeys, + limit: None, + offset: None, + }; + + let result = drive + .fetch_identity_revision_with_keys(key_request, None, platform_version) + .expect("should not error"); + + assert!(result.is_none()); + } + + #[test] + fn should_return_partial_identity_with_revision_and_keys() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let identity = Identity::random_identity(3, Some(42), platform_version) + .expect("expected a random identity"); + + drive + .add_new_identity( + identity.clone(), + false, + &BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("expected to add identity"); + + let key_request = IdentityKeysRequest { + identity_id: identity.id().to_buffer(), + request_type: KeyRequestType::AllKeys, + limit: None, + offset: None, + }; + + let partial = drive + .fetch_identity_revision_with_keys(key_request, None, platform_version) + .expect("should not error") + .expect("should have partial identity"); + + assert_eq!(partial.id, identity.id().clone()); + assert!(partial.balance.is_none()); + assert_eq!(partial.revision, Some(identity.revision())); + assert_eq!(partial.loaded_public_keys.len(), 3); + } + } + + mod fetch_identity_keys_as_partial_identity { + use super::*; + + #[test] + fn should_return_partial_identity_with_only_keys() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let identity = Identity::random_identity(5, Some(42), platform_version) + .expect("expected a random identity"); + + drive + .add_new_identity( + identity.clone(), + false, + &BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("expected to add identity"); + + let key_request = IdentityKeysRequest { + identity_id: identity.id().to_buffer(), + request_type: KeyRequestType::AllKeys, + limit: None, + offset: None, + }; + + let partial = drive + .fetch_identity_keys_as_partial_identity(key_request, None, platform_version) + .expect("should not error") + .expect("should have partial identity"); + + assert_eq!(partial.id, identity.id().clone()); + assert!(partial.balance.is_none()); + assert!(partial.revision.is_none()); + assert_eq!(partial.loaded_public_keys.len(), 5); + } + } +} diff --git a/packages/rs-drive/src/drive/identity/fetch/revision/mod.rs b/packages/rs-drive/src/drive/identity/fetch/revision/mod.rs index dfb1434a71c..cf9e4cb4388 100644 --- a/packages/rs-drive/src/drive/identity/fetch/revision/mod.rs +++ b/packages/rs-drive/src/drive/identity/fetch/revision/mod.rs @@ -1 +1,133 @@ mod fetch_identity_revision; + +#[cfg(feature = "server")] +#[cfg(test)] +mod tests { + use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; + use dpp::block::block_info::BlockInfo; + use dpp::identity::accessors::IdentityGettersV0; + use dpp::identity::Identity; + use dpp::version::PlatformVersion; + + mod fetch_identity_revision { + use super::*; + + #[test] + fn should_return_none_for_non_existent_identity() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let revision = drive + .fetch_identity_revision([0; 32], true, None, platform_version) + .expect("should not error"); + + assert!(revision.is_none()); + } + + #[test] + fn should_return_initial_revision_for_new_identity() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let identity = Identity::random_identity(3, Some(42), platform_version) + .expect("expected a random identity"); + + let expected_revision = identity.revision(); + + drive + .add_new_identity( + identity.clone(), + false, + &BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("expected to add identity"); + + let revision = drive + .fetch_identity_revision(identity.id().to_buffer(), true, None, platform_version) + .expect("should not error") + .expect("should have revision"); + + assert_eq!(revision, expected_revision); + } + + #[test] + fn should_return_updated_revision_after_update() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let identity = Identity::random_identity(3, Some(42), platform_version) + .expect("expected a random identity"); + + drive + .add_new_identity( + identity.clone(), + false, + &BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("expected to add identity"); + + let new_revision = 5; + let block_info = BlockInfo::default(); + + drive + .update_identity_revision( + identity.id().to_buffer(), + new_revision, + &block_info, + true, + None, + platform_version, + None, + ) + .expect("expected to update revision"); + + let revision = drive + .fetch_identity_revision(identity.id().to_buffer(), true, None, platform_version) + .expect("should not error") + .expect("should have revision"); + + assert_eq!(revision, new_revision); + } + + #[test] + fn should_return_revision_with_fees() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let identity = Identity::random_identity(3, Some(42), platform_version) + .expect("expected a random identity"); + + drive + .add_new_identity( + identity.clone(), + false, + &BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("expected to add identity"); + + let block_info = BlockInfo::default(); + + let (revision, fee_result) = drive + .fetch_identity_revision_with_fees( + identity.id().to_buffer(), + &block_info, + true, + None, + platform_version, + ) + .expect("should not error"); + + assert_eq!(revision, Some(identity.revision())); + assert!(fee_result.processing_fee > 0); + } + } +} diff --git a/packages/rs-drive/src/drive/identity/insert/add_new_identity/mod.rs b/packages/rs-drive/src/drive/identity/insert/add_new_identity/mod.rs index acff3056ac6..793882f2632 100644 --- a/packages/rs-drive/src/drive/identity/insert/add_new_identity/mod.rs +++ b/packages/rs-drive/src/drive/identity/insert/add_new_identity/mod.rs @@ -225,4 +225,99 @@ mod tests { } assert_eq!(fee_result, expected_fee_result); } + + #[test] + fn should_fail_to_insert_duplicate_non_masternode_identity() { + use crate::error::identity::IdentityError; + use crate::error::Error; + + let platform_version = PlatformVersion::latest(); + let drive = setup_drive(None); + + let transaction = drive.grove.start_transaction(); + + drive + .create_initial_state_structure(Some(&transaction), platform_version) + .expect("expected to create root tree successfully"); + + let identity = Identity::random_identity(5, Some(12345), platform_version) + .expect("expected a random identity"); + + // Insert the identity the first time + drive + .add_new_identity( + identity.clone(), + false, + &BlockInfo::default(), + true, + Some(&transaction), + platform_version, + ) + .expect("expected to insert identity"); + + // Inserting the same non-masternode identity again should fail + let result = drive.add_new_identity( + identity, + false, + &BlockInfo::default(), + true, + Some(&transaction), + platform_version, + ); + + assert!(matches!( + result, + Err(Error::Identity(IdentityError::IdentityAlreadyExists(_))) + )); + } + + #[test] + fn should_succeed_reinserting_masternode_identity() { + let platform_version = PlatformVersion::latest(); + let drive = setup_drive(None); + + let transaction = drive.grove.start_transaction(); + + drive + .create_initial_state_structure(Some(&transaction), platform_version) + .expect("expected to create root tree successfully"); + + let identity = Identity::random_identity(5, Some(12345), platform_version) + .expect("expected a random identity"); + + // Insert as masternode identity + drive + .add_new_identity( + identity.clone(), + true, + &BlockInfo::default(), + true, + Some(&transaction), + platform_version, + ) + .expect("expected to insert identity"); + + // Reinserting the same masternode identity should succeed (re-enable keys) + let result = drive.add_new_identity( + identity.clone(), + true, + &BlockInfo::default(), + true, + Some(&transaction), + platform_version, + ); + + assert!(result.is_ok()); + + // Verify keys are still present after reinsertion + let fetched_keys = drive + .fetch_all_identity_keys( + identity.id().to_buffer(), + Some(&transaction), + platform_version, + ) + .expect("expected to fetch keys"); + + assert_eq!(fetched_keys.len(), 5); + } } diff --git a/packages/rs-drive/src/drive/identity/mod.rs b/packages/rs-drive/src/drive/identity/mod.rs index c4bcc1802dd..4c209271c31 100644 --- a/packages/rs-drive/src/drive/identity/mod.rs +++ b/packages/rs-drive/src/drive/identity/mod.rs @@ -417,3 +417,161 @@ impl From for &'static [u8; 1] { } } } + +#[cfg(all(test, any(feature = "server", feature = "verify")))] +mod tests { + use super::*; + + mod identity_root_structure { + use super::*; + + #[test] + fn should_convert_from_valid_u8_values() { + assert!(matches!( + IdentityRootStructure::try_from(192u8), + Ok(IdentityRootStructure::IdentityTreeRevision) + )); + assert!(matches!( + IdentityRootStructure::try_from(64u8), + Ok(IdentityRootStructure::IdentityTreeNonce) + )); + assert!(matches!( + IdentityRootStructure::try_from(128u8), + Ok(IdentityRootStructure::IdentityTreeKeys) + )); + assert!(matches!( + IdentityRootStructure::try_from(160u8), + Ok(IdentityRootStructure::IdentityTreeKeyReferences) + )); + assert!(matches!( + IdentityRootStructure::try_from(96u8), + Ok(IdentityRootStructure::IdentityTreeNegativeCredit) + )); + assert!(matches!( + IdentityRootStructure::try_from(32u8), + Ok(IdentityRootStructure::IdentityContractInfo) + )); + } + + #[test] + fn should_fail_for_invalid_u8_value() { + let result = IdentityRootStructure::try_from(0u8); + assert!(result.is_err()); + let result = IdentityRootStructure::try_from(1u8); + assert!(result.is_err()); + let result = IdentityRootStructure::try_from(255u8); + assert!(result.is_err()); + } + + #[test] + fn should_display_correct_names() { + assert_eq!( + format!("{}", IdentityRootStructure::IdentityTreeRevision), + "Revision" + ); + assert_eq!( + format!("{}", IdentityRootStructure::IdentityTreeNonce), + "Nonce" + ); + assert_eq!( + format!("{}", IdentityRootStructure::IdentityTreeKeys), + "IdentityKeys" + ); + assert_eq!( + format!("{}", IdentityRootStructure::IdentityTreeKeyReferences), + "IdentityKeyReferences" + ); + assert_eq!( + format!("{}", IdentityRootStructure::IdentityTreeNegativeCredit), + "NegativeCredit" + ); + assert_eq!( + format!("{}", IdentityRootStructure::IdentityContractInfo), + "ContractInfo" + ); + } + + #[test] + fn should_convert_to_u8() { + let val: u8 = IdentityRootStructure::IdentityTreeRevision.into(); + assert_eq!(val, 192); + let val: u8 = IdentityRootStructure::IdentityTreeNonce.into(); + assert_eq!(val, 64); + let val: u8 = IdentityRootStructure::IdentityTreeKeys.into(); + assert_eq!(val, 128); + } + + #[test] + fn should_convert_to_u8_array() { + let val: [u8; 1] = IdentityRootStructure::IdentityTreeRevision.into(); + assert_eq!(val, [192]); + let val: [u8; 1] = IdentityRootStructure::IdentityTreeNonce.into(); + assert_eq!(val, [64]); + } + + #[test] + fn should_convert_to_static_u8_array_ref() { + let val: &'static [u8; 1] = IdentityRootStructure::IdentityTreeRevision.into(); + assert_eq!(val, &[192]); + let val: &'static [u8; 1] = IdentityRootStructure::IdentityContractInfo.into(); + assert_eq!(val, &[32]); + } + } + + mod identity_path_functions { + use super::*; + + #[test] + fn should_create_correct_identity_path() { + let id = [1u8; 32]; + let path = identity_path(&id); + assert_eq!(path.len(), 2); + assert_eq!(path[1], &id); + } + + #[test] + fn should_create_correct_identity_path_vec() { + let id = [2u8; 32]; + let path = identity_path_vec(&id); + assert_eq!(path.len(), 2); + assert_eq!(path[1], id.to_vec()); + } + + #[test] + fn should_create_correct_key_tree_path() { + let id = [3u8; 32]; + let path = identity_key_tree_path(&id); + assert_eq!(path.len(), 3); + assert_eq!(path[1], &id); + assert_eq!(path[2], &[IdentityRootStructure::IdentityTreeKeys as u8]); + } + + #[test] + fn should_create_correct_key_tree_path_vec() { + let id = [4u8; 32]; + let path = identity_key_tree_path_vec(&id); + assert_eq!(path.len(), 3); + assert_eq!(path[1], id.to_vec()); + assert_eq!(path[2], vec![IdentityRootStructure::IdentityTreeKeys as u8]); + } + + #[cfg(feature = "server")] + #[test] + fn should_create_correct_contract_info_root_path() { + let id = [5u8; 32]; + let path = identity_contract_info_root_path(&id); + assert_eq!(path.len(), 3); + assert_eq!(path[1], &id); + } + + #[test] + fn should_create_correct_contract_info_group_path() { + let id = [6u8; 32]; + let group_id = [7u8; 32]; + let path = identity_contract_info_group_path(&id, &group_id); + assert_eq!(path.len(), 4); + assert_eq!(path[1], &id); + assert_eq!(path[3], &group_id); + } + } +}