diff --git a/crates/algokit_transact/src/transactions/asset_config.rs b/crates/algokit_transact/src/transactions/asset_config.rs index 4c2ec585c..a8039889c 100644 --- a/crates/algokit_transact/src/transactions/asset_config.rs +++ b/crates/algokit_transact/src/transactions/asset_config.rs @@ -479,14 +479,7 @@ impl Validate for AssetConfigTransactionFields { #[cfg(test)] mod tests { use super::*; - use crate::test_utils::TransactionHeaderMother; - - fn create_test_address() -> Address { - // Use a valid Algorand address for testing - "JB3K6HTAXODO4THESLNYTSG6GQUFNEVIQG7A6ZYVDACR6WA3ZF52TKU5NA" - .parse() - .unwrap() - } + use crate::test_utils::{AccountMother, TransactionHeaderMother}; #[test] fn test_validate_asset_creation_multiple_errors() { @@ -503,7 +496,7 @@ mod tests { unit_name: Some("VERYLONGUNITNAME".to_string()), // More than 8 bytes - ERROR 4 url: Some(long_url), // More than 96 bytes - ERROR 5 metadata_hash: None, - manager: Some(create_test_address()), + manager: Some(AccountMother::neil().address()), reserve: None, freeze: None, clawback: None, @@ -535,10 +528,10 @@ mod tests { unit_name: Some("TA".to_string()), url: Some("https://example.com".to_string()), metadata_hash: Some([1; 32]), - manager: Some(create_test_address()), - reserve: Some(create_test_address()), - freeze: Some(create_test_address()), - clawback: Some(create_test_address()), + manager: Some(AccountMother::neil().address()), + reserve: Some(AccountMother::neil().address()), + freeze: Some(AccountMother::neil().address()), + clawback: Some(AccountMother::neil().address()), }; let result = asset_config.validate(); @@ -551,18 +544,18 @@ mod tests { fn test_validate_valid_asset_reconfigure() { let asset_config = AssetConfigTransactionFields { header: TransactionHeaderMother::example().build().unwrap(), - asset_id: 123, // Existing asset - total: None, // Can't modify - decimals: None, // Can't modify - default_frozen: None, // Can't modify - asset_name: None, // Can't modify - unit_name: None, // Can't modify - url: None, // Can't modify - metadata_hash: None, // Can't modify - manager: Some(create_test_address()), // Can modify - reserve: Some(create_test_address()), // Can modify - freeze: Some(create_test_address()), // Can modify - clawback: Some(create_test_address()), // Can modify + asset_id: 123, // Existing asset + total: None, // Can't modify + decimals: None, // Can't modify + default_frozen: None, // Can't modify + asset_name: None, // Can't modify + unit_name: None, // Can't modify + url: None, // Can't modify + metadata_hash: None, // Can't modify + manager: Some(AccountMother::neil().address()), // Can modify + reserve: Some(AccountMother::neil().address()), // Can modify + freeze: Some(AccountMother::neil().address()), // Can modify + clawback: Some(AccountMother::neil().address()), // Can modify }; let result = asset_config.validate(); @@ -595,15 +588,15 @@ mod tests { fn test_validate_asset_reconfigure_multiple_immutable_field_errors() { let asset_config = AssetConfigTransactionFields { header: TransactionHeaderMother::example().build().unwrap(), - asset_id: 123, // Existing asset - total: Some(2000), // Can't modify total - ERROR 1 - decimals: Some(3), // Can't modify decimals - ERROR 2 - default_frozen: Some(true), // Can't modify default_frozen - ERROR 3 - asset_name: Some("NewName".to_string()), // Can't modify asset_name - ERROR 4 - unit_name: Some("NEW".to_string()), // Can't modify unit_name - ERROR 5 - url: Some("https://newurl.com".to_string()), // Can't modify url - ERROR 6 - metadata_hash: Some([2; 32]), // Can't modify metadata_hash - ERROR 7 - manager: Some(create_test_address()), // This is allowed + asset_id: 123, // Existing asset + total: Some(2000), // Can't modify total - ERROR 1 + decimals: Some(3), // Can't modify decimals - ERROR 2 + default_frozen: Some(true), // Can't modify default_frozen - ERROR 3 + asset_name: Some("NewName".to_string()), // Can't modify asset_name - ERROR 4 + unit_name: Some("NEW".to_string()), // Can't modify unit_name - ERROR 5 + url: Some("https://newurl.com".to_string()), // Can't modify url - ERROR 6 + metadata_hash: Some([2; 32]), // Can't modify metadata_hash - ERROR 7 + manager: Some(AccountMother::neil().address()), // This is allowed reserve: None, freeze: None, clawback: None, diff --git a/crates/algokit_transact/src/transactions/asset_freeze.rs b/crates/algokit_transact/src/transactions/asset_freeze.rs index 3fd7abae2..8814fc1c4 100644 --- a/crates/algokit_transact/src/transactions/asset_freeze.rs +++ b/crates/algokit_transact/src/transactions/asset_freeze.rs @@ -5,6 +5,7 @@ use crate::Transaction; use crate::address::Address; +use crate::traits::Validate; use crate::transactions::common::TransactionHeader; use crate::utils::{is_zero, is_zero_addr}; use derive_builder::Builder; @@ -53,6 +54,90 @@ pub struct AssetFreezeTransactionFields { impl AssetFreezeTransactionBuilder { pub fn build(&self) -> Result { - self.build_fields().map(Transaction::AssetFreeze) + let d = self.build_fields()?; + d.validate().map_err(|errors| { + AssetFreezeTransactionBuilderError::ValidationError(format!( + "Asset freeze validation failed: {}", + errors.join("\n") + )) + })?; + Ok(Transaction::AssetFreeze(d)) + } +} + +impl Validate for AssetFreezeTransactionFields { + fn validate(&self) -> Result<(), Vec> { + let mut errors = Vec::new(); + + if self.asset_id == 0 { + errors.push("Asset ID must not be 0".to_string()); + } + + match errors.is_empty() { + true => Ok(()), + false => Err(errors), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::{AccountMother, TransactionHeaderMother}; + + #[test] + fn test_validate_asset_freeze_zero_asset_id() { + let asset_freeze = AssetFreezeTransactionFields { + header: TransactionHeaderMother::example().build().unwrap(), + asset_id: 0, // Invalid asset ID + freeze_target: AccountMother::neil().address(), + frozen: true, + }; + + let result = asset_freeze.validate(); + assert!(result.is_err()); + let errors = result.unwrap_err(); + assert_eq!(errors.len(), 1); + assert_eq!(errors[0], "Asset ID must not be 0"); + } + + #[test] + fn test_validate_valid_asset_freeze() { + let asset_freeze = AssetFreezeTransactionFields { + header: TransactionHeaderMother::example().build().unwrap(), + asset_id: 123, // Valid asset ID + freeze_target: AccountMother::neil().address(), + frozen: true, + }; + + let result = asset_freeze.validate(); + assert!(result.is_ok()); + } + + #[test] + fn test_build_with_invalid_asset_id() { + let result = AssetFreezeTransactionBuilder::default() + .header(TransactionHeaderMother::example().build().unwrap()) + .asset_id(0) // Invalid asset ID + .freeze_target(AccountMother::neil().address()) + .frozen(true) + .build(); + + assert!(result.is_err()); + let error_message = result.unwrap_err().to_string(); + assert!(error_message.contains("Asset freeze validation failed")); + assert!(error_message.contains("Asset ID must not be 0")); + } + + #[test] + fn test_build_with_valid_asset_id() { + let result = AssetFreezeTransactionBuilder::default() + .header(TransactionHeaderMother::example().build().unwrap()) + .asset_id(123) // Valid asset ID + .freeze_target(AccountMother::neil().address()) + .frozen(true) + .build(); + + assert!(result.is_ok()); } } diff --git a/crates/algokit_transact/src/transactions/asset_transfer.rs b/crates/algokit_transact/src/transactions/asset_transfer.rs index b6ecc3071..c209a7b42 100644 --- a/crates/algokit_transact/src/transactions/asset_transfer.rs +++ b/crates/algokit_transact/src/transactions/asset_transfer.rs @@ -2,6 +2,7 @@ //! //! This module provides functionality for creating and managing asset transfer transactions. +use crate::traits::Validate; use crate::transactions::common::TransactionHeader; use crate::utils::{is_zero, is_zero_addr, is_zero_addr_opt}; use crate::{Address, Transaction}; @@ -77,6 +78,94 @@ pub struct AssetTransferTransactionFields { impl AssetTransferTransactionBuilder { pub fn build(&self) -> Result { - self.build_fields().map(Transaction::AssetTransfer) + let d = self.build_fields()?; + d.validate().map_err(|errors| { + AssetTransferTransactionBuilderError::ValidationError(format!( + "Asset transfer validation failed: {}", + errors.join("\n") + )) + })?; + Ok(Transaction::AssetTransfer(d)) + } +} + +impl Validate for AssetTransferTransactionFields { + fn validate(&self) -> Result<(), Vec> { + let mut errors = Vec::new(); + + if self.asset_id == 0 { + errors.push("Asset ID must not be 0".to_string()); + } + + match errors.is_empty() { + true => Ok(()), + false => Err(errors), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::{AccountMother, TransactionHeaderMother}; + + #[test] + fn test_validate_asset_transfer_zero_asset_id() { + let asset_transfer = AssetTransferTransactionFields { + header: TransactionHeaderMother::example().build().unwrap(), + asset_id: 0, // Invalid asset ID + amount: 1000, + receiver: AccountMother::neil().address(), + asset_sender: None, + close_remainder_to: None, + }; + + let result = asset_transfer.validate(); + assert!(result.is_err()); + let errors = result.unwrap_err(); + assert_eq!(errors.len(), 1); + assert_eq!(errors[0], "Asset ID must not be 0"); + } + + #[test] + fn test_validate_valid_asset_transfer() { + let asset_transfer = AssetTransferTransactionFields { + header: TransactionHeaderMother::example().build().unwrap(), + asset_id: 123, // Valid asset ID + amount: 1000, + receiver: AccountMother::neil().address(), + asset_sender: None, + close_remainder_to: None, + }; + + let result = asset_transfer.validate(); + assert!(result.is_ok()); + } + + #[test] + fn test_build_with_invalid_asset_id() { + let result = AssetTransferTransactionBuilder::default() + .header(TransactionHeaderMother::example().build().unwrap()) + .asset_id(0) // Invalid asset ID + .amount(1000) + .receiver(AccountMother::neil().address()) + .build(); + + assert!(result.is_err()); + let error_message = result.unwrap_err().to_string(); + assert!(error_message.contains("Asset transfer validation failed")); + assert!(error_message.contains("Asset ID must not be 0")); + } + + #[test] + fn test_build_with_valid_asset_id() { + let result = AssetTransferTransactionBuilder::default() + .header(TransactionHeaderMother::example().build().unwrap()) + .asset_id(123) // Valid asset ID + .amount(1000) + .receiver(AccountMother::neil().address()) + .build(); + + assert!(result.is_ok()); } } diff --git a/crates/algokit_utils/src/transactions/asset_transfer.rs b/crates/algokit_utils/src/transactions/asset_transfer.rs index e2bc3ec8e..90328afd4 100644 --- a/crates/algokit_utils/src/transactions/asset_transfer.rs +++ b/crates/algokit_utils/src/transactions/asset_transfer.rs @@ -103,18 +103,13 @@ pub fn build_asset_clawback( #[cfg(test)] mod tests { use super::*; - use algokit_transact::{Address, TransactionHeader}; - use std::str::FromStr; + use algokit_transact::{TransactionHeader, test_utils::AccountMother}; #[test] fn test_asset_opt_out_with_optional_close_remainder_to() { // Use valid test addresses - let sender = - Address::from_str("JB3K6HTAXODO4THESLNYTSG6GQUFNEVIQG7A6ZYVDACR6WA3ZF52TKU5NA") - .unwrap(); - let creator = - Address::from_str("JB3K6HTAXODO4THESLNYTSG6GQUFNEVIQG7A6ZYVDACR6WA3ZF52TKU5NA") - .unwrap(); + let sender = AccountMother::neil().address(); + let creator = AccountMother::neil().address(); // Test with Some(creator) - explicit close_remainder_to let params_with_creator = AssetOptOutParams {