diff --git a/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/validate_fees_of_event/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/validate_fees_of_event/v0/mod.rs index b8810cec48a..cb16c8202a2 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/validate_fees_of_event/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/validate_fees_of_event/v0/mod.rs @@ -36,7 +36,7 @@ where /// /// * This function may return an `Error::Execution` if the identity balance is not found. /// * This function may return an `Error::Drive` if there's an issue with applying drive operations. - pub(in crate::execution) fn validate_fees_of_event_v0( + pub(super) fn validate_fees_of_event_v0( &self, event: &ExecutionEvent, block_info: &BlockInfo, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/direct_selling/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/direct_selling/mod.rs index 26114e2f831..5691b6414c1 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/direct_selling/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/direct_selling/mod.rs @@ -7,6 +7,7 @@ mod token_selling_tests { use super::*; + use crate::error::Error; use dpp::{ dashcore::secp256k1::hashes::hex::{Case, DisplayHex}, prelude::{DataContract, Identity, IdentityPublicKey}, @@ -14,6 +15,7 @@ mod token_selling_tests { }; use drive::verify::RootHash; use simple_signer::signer::SimpleSigner; + #[test] fn test_successful_direct_purchase_single_price() { let platform_version = PlatformVersion::latest(); @@ -332,7 +334,7 @@ mod token_selling_tests { assert_matches!( processing_result.execution_results().as_slice(), - [StateTransitionExecutionResult::PaidConsensusError( + [PaidConsensusError( ConsensusError::StateError(StateError::TokenDirectPurchaseUserPriceTooLow(_)), _ )] @@ -571,6 +573,165 @@ mod token_selling_tests { } } + #[test] + fn test_direct_purchase_from_yourself() { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + + let mut rng = StdRng::seed_from_u64(54321); + // Create an identity that will be both seller and buyer + let (self_trader, self_trader_signer, self_trader_key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(10.0)); + + let single_price = TokenPricingSchedule::SinglePrice(dash_to_credits!(1)); + + let mut identity_contract_nonce: u64 = 2; + let (contract, token_id) = create_token_with_pricing( + platform_version, + &mut platform, + &self_trader, + &self_trader_signer, + &self_trader_key, + Some(single_price.clone()), + &mut identity_contract_nonce, + ); + + // Set the price + let set_price_transition = + BatchTransition::new_token_change_direct_purchase_price_transition( + token_id, + self_trader.id(), + contract.id(), + 0, + Some(single_price.clone()), + None, + None, + &self_trader_key, + identity_contract_nonce, + 0, + &self_trader_signer, + platform_version, + None, + ) + .unwrap(); + + let platform_state = platform.state.load(); + let processing_result = process_test_state_transition( + &mut platform, + set_price_transition, + &platform_state, + platform_version, + ); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution(..)] + ); + + // Check initial token balance (should have some tokens as the owner) + let initial_token_balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + self_trader.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + + // Now purchase tokens from yourself + let purchase_transition = BatchTransition::new_token_direct_purchase_transition( + token_id, + self_trader.id(), // Buyer is the same as seller + contract.id(), + 0, + 5, // Buying 5 tokens + dash_to_credits!(5), // Paying for 5 tokens + &self_trader_key, + identity_contract_nonce + 1, + 0, + &self_trader_signer, + platform_version, + None, + ) + .unwrap(); + + let initial_credit_balance = platform + .drive + .fetch_identity_balance(self_trader.id().to_buffer(), None, platform_version) + .expect("expected to fetch credit balance"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &[purchase_transition + .serialize_to_bytes() + .expect("expected documents batch serialized state transition")], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + platform + .validate_token_aggregated_balance(&transaction, platform_version) + .expect("expected to validate token aggregated balances"); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution(..)] + ); + + // Check token balance after purchase + let final_token_balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + self_trader.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + + // Token balance should increase by 5 + assert_eq!( + final_token_balance, + initial_token_balance.map(|b| b + 5).or(Some(5)) + ); + + // Check credit balance - should only be reduced by fees, not by the purchase price + // (since the money goes back to yourself) + let final_credit_balance = platform + .drive + .fetch_identity_balance(self_trader.id().to_buffer(), None, platform_version) + .expect("expected to fetch credit balance"); + + // The difference should be just the transaction fees + let credit_diff = initial_credit_balance.unwrap() - final_credit_balance.unwrap(); + + // Assert that the difference is much less than the purchase price (just fees) + assert!( + credit_diff < dash_to_credits!(0.01), + "Credit difference should only be transaction fees, but was: {}", + credit_diff + ); + } + // Helper functions // // /\_/\ @@ -596,7 +757,7 @@ mod token_selling_tests { /// Creates a token contract with the given owner identity and configuration, and sets the price. fn create_token_with_pricing( - platform_version: &dpp::version::PlatformVersion, + platform_version: &PlatformVersion, platform: &mut TempPlatform, seller: &Identity, seller_signer: &SimpleSigner, diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_direct_purchase_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_direct_purchase_transition.rs index e1f43cdd198..9c3ceab2581 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_direct_purchase_transition.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_direct_purchase_transition.rs @@ -49,23 +49,26 @@ impl DriveHighLevelBatchOperationConverter for TokenDirectPurchaseTransitionActi allow_saturation: false, })); - ops.push(IdentityOperation( - IdentityOperationType::RemoveFromIdentityBalance { - identity_id: owner_id.to_buffer(), - balance_to_remove: self.total_agreed_price(), - }, - )); - ops.push(IdentityOperation( - IdentityOperationType::AddToIdentityBalance { - identity_id: self - .base() - .data_contract_fetch_info() - .contract - .owner_id() - .to_buffer(), - added_balance: self.total_agreed_price(), - }, - )); + if owner_id != self.base().data_contract_fetch_info().contract.owner_id() { + // We can not send to ourselves + ops.push(IdentityOperation( + IdentityOperationType::RemoveFromIdentityBalance { + identity_id: owner_id.to_buffer(), + balance_to_remove: self.total_agreed_price(), + }, + )); + ops.push(IdentityOperation( + IdentityOperationType::AddToIdentityBalance { + identity_id: self + .base() + .data_contract_fetch_info() + .contract + .owner_id() + .to_buffer(), + added_balance: self.total_agreed_price(), + }, + )); + } let token_configuration = self.base().token_configuration()?; if token_configuration