diff --git a/crates/algokit_transact/src/test_utils/asset_freeze.rs b/crates/algokit_transact/src/test_utils/asset_freeze.rs new file mode 100644 index 000000000..fe07efefc --- /dev/null +++ b/crates/algokit_transact/src/test_utils/asset_freeze.rs @@ -0,0 +1,81 @@ +use crate::{Address, AssetFreezeTransactionBuilder, Byte32, TransactionHeaderBuilder}; +use base64::{prelude::BASE64_STANDARD, Engine}; + +pub struct AssetFreezeTransactionMother {} + +impl AssetFreezeTransactionMother { + pub fn asset_freeze() -> AssetFreezeTransactionBuilder { + // mainnet-2XFGVOHMFYLAWBHOSIOI67PBT5LDRHBTD3VLX5EYBDTFNVKMCJIA + let sender = "E4A6FVIHXSZ3F7QXRCOTYDDILVQYEBFH56HYDIIYX4SVXS2QX5GUTBVZHY" + .parse::
() + .unwrap(); + let freeze_address = "ZJU3X2B2QN3BUBIJ64JZ565V363ANGBUDOLXAJHDXGIIMYK6WV3NSNCBQQ" + .parse::() + .unwrap(); + let genesis_hash: Byte32 = BASE64_STANDARD + .decode("wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=") + .unwrap() + .try_into() + .unwrap(); + let note = BASE64_STANDARD + .decode("TkZUIGZyZWV6ZWQgYnkgbG9mdHkuYWk=") + .unwrap(); + let group = BASE64_STANDARD + .decode("xERjxVTlNb8jeHa16qmpxDMh4+dcDCokO69QnNESbFk=") + .unwrap() + .try_into() + .unwrap(); + + AssetFreezeTransactionBuilder::default() + .header( + TransactionHeaderBuilder::default() + .sender(sender) + .fee(1000) + .first_valid(37463562) + .last_valid(37464562) + .genesis_hash(genesis_hash) + .genesis_id("mainnet-v1.0".to_string()) + .note(note) + .group(group) + .build() + .unwrap(), + ) + .asset_id(1707148495) + .freeze_target(freeze_address) + .frozen(true) + .to_owned() + } + + pub fn asset_unfreeze() -> AssetFreezeTransactionBuilder { + // testnet-LZ2ODDAT4ATAVJUEQW34DIKMPCMBXCCHOSIYKMWGBPEVNHLSEV2A + let sender = "WLH5LELVSEVQL45LBRQYCLJAX6KQPGWUY5WHJXVRV2NPYZUBQAFPH22Q7A" + .parse::() + .unwrap(); + let freeze_address = "ZYQX7BZ6LGTD7UCS7J5RVEAKHUJPK3FNJFZV2GPUYS2TFIADVFHDBKTN7I" + .parse::() + .unwrap(); + let genesis_hash: Byte32 = BASE64_STANDARD + .decode("SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=") + .unwrap() + .try_into() + .unwrap(); + let note = BASE64_STANDARD.decode("th4JDxFROQw=").unwrap(); + + AssetFreezeTransactionBuilder::default() + .header( + TransactionHeaderBuilder::default() + .sender(sender) + .fee(1000) + .first_valid(3277583) + .last_valid(3278583) + .genesis_hash(genesis_hash) + .note(note) + .build() + .unwrap(), + ) + .asset_id(185) + .freeze_target(freeze_address) + .frozen(false) + .to_owned() + } +} diff --git a/crates/algokit_transact/src/test_utils/mod.rs b/crates/algokit_transact/src/test_utils/mod.rs index 412c017c6..5ad75aadc 100644 --- a/crates/algokit_transact/src/test_utils/mod.rs +++ b/crates/algokit_transact/src/test_utils/mod.rs @@ -1,11 +1,10 @@ mod application_call; mod asset_config; +mod asset_freeze; mod key_registration; use crate::{ - transactions::{ - AssetFreezeTransactionBuilder, AssetTransferTransactionBuilder, PaymentTransactionBuilder, - }, + transactions::{AssetTransferTransactionBuilder, PaymentTransactionBuilder}, Address, AlgorandMsgpack, Byte32, SignedTransaction, Transaction, TransactionHeaderBuilder, TransactionId, ALGORAND_PUBLIC_KEY_BYTE_LENGTH, HASH_BYTES_LENGTH, }; @@ -19,6 +18,7 @@ use std::{fs::File, str::FromStr}; pub use application_call::ApplicationCallTransactionMother; pub use asset_config::AssetConfigTransactionMother; +pub use asset_freeze::AssetFreezeTransactionMother; pub use key_registration::KeyRegistrationTransactionMother; pub struct TransactionHeaderMother {} @@ -160,90 +160,6 @@ impl TransactionMother { .receiver(AddressMother::neil()) .to_owned() } - - pub fn asset_freeze() -> AssetFreezeTransactionBuilder { - // mainnet-2XFGVOHMFYLAWBHOSIOI67PBT5LDRHBTD3VLX5EYBDTFNVKMCJIA - let sender = "E4A6FVIHXSZ3F7QXRCOTYDDILVQYEBFH56HYDIIYX4SVXS2QX5GUTBVZHY" - .parse::() - .unwrap(); - let freeze_address = "ZJU3X2B2QN3BUBIJ64JZ565V363ANGBUDOLXAJHDXGIIMYK6WV3NSNCBQQ" - .parse::() - .unwrap(); - let genesis_hash: Byte32 = BASE64_STANDARD - .decode("wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=") - .unwrap() - .try_into() - .unwrap(); - let note = BASE64_STANDARD - .decode("TkZUIGZyZWV6ZWQgYnkgbG9mdHkuYWk=") - .unwrap(); - let group = BASE64_STANDARD - .decode("xERjxVTlNb8jeHa16qmpxDMh4+dcDCokO69QnNESbFk=") - .unwrap() - .try_into() - .unwrap(); - - AssetFreezeTransactionBuilder::default() - .header( - TransactionHeaderBuilder::default() - .sender(sender) - .fee(1000) - .first_valid(37463562) - .last_valid(37464562) - .genesis_hash(genesis_hash) - .genesis_id("mainnet-v1.0".to_string()) - .note(note) - .group(group) - .build() - .unwrap(), - ) - .asset_id(1707148495) - .freeze_target(freeze_address) - .frozen(true) - .to_owned() - } - - pub fn asset_unfreeze() -> AssetFreezeTransactionBuilder { - // Same as asset_freeze but with frozen=false - let sender = "E4A6FVIHXSZ3F7QXRCOTYDDILVQYEBFH56HYDIIYX4SVXS2QX5GUTBVZHY" - .parse::() - .unwrap(); - let freeze_address = "ZJU3X2B2QN3BUBIJ64JZ565V363ANGBUDOLXAJHDXGIIMYK6WV3NSNCBQQ" - .parse::() - .unwrap(); - let genesis_hash: Byte32 = BASE64_STANDARD - .decode("wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=") - .unwrap() - .try_into() - .unwrap(); - let note = BASE64_STANDARD - .decode("TkZUIGZyZWV6ZWQgYnkgbG9mdHkuYWk=") - .unwrap(); - let group = BASE64_STANDARD - .decode("xERjxVTlNb8jeHa16qmpxDMh4+dcDCokO69QnNESbFk=") - .unwrap() - .try_into() - .unwrap(); - - AssetFreezeTransactionBuilder::default() - .header( - TransactionHeaderBuilder::default() - .sender(sender) - .fee(1000) - .first_valid(37463562) - .last_valid(37464562) - .genesis_hash(genesis_hash) - .genesis_id("mainnet-v1.0".to_string()) - .note(note) - .group(group) - .build() - .unwrap(), - ) - .asset_id(1707148495) - .freeze_target(freeze_address) - .frozen(false) - .to_owned() - } } pub struct AddressMother {} @@ -501,7 +417,9 @@ impl TestDataMother { 2, 205, 103, 33, 67, 14, 82, 196, 115, 196, 206, 254, 50, 110, 63, 182, 149, 229, 184, 216, 93, 11, 13, 99, 69, 213, 218, 165, 134, 118, 47, 44, ]; - let transaction = TransactionMother::asset_freeze().build().unwrap(); + let transaction = AssetFreezeTransactionMother::asset_freeze() + .build() + .unwrap(); TransactionTestData::new(transaction, signing_private_key) } @@ -510,7 +428,9 @@ impl TestDataMother { 2, 205, 103, 33, 67, 14, 82, 196, 115, 196, 206, 254, 50, 110, 63, 182, 149, 229, 184, 216, 93, 11, 13, 99, 69, 213, 218, 165, 134, 118, 47, 44, ]; - let transaction = TransactionMother::asset_unfreeze().build().unwrap(); + let transaction = AssetFreezeTransactionMother::asset_unfreeze() + .build() + .unwrap(); TransactionTestData::new(transaction, signing_private_key) } @@ -554,17 +474,12 @@ fn normalise_json(value: serde_json::Value) -> serde_json::Value { "num_uints", ]; - // Boolean fields that should always be included, even when false - const BOOLEAN_FIELDS_TO_KEEP: &[&str] = &["frozen"]; - match value { serde_json::Value::Object(map) => serde_json::Value::Object( map.into_iter() .filter(|(k, v)| { !(v.is_null() - || v.is_boolean() - && v.as_bool() == Some(false) - && !BOOLEAN_FIELDS_TO_KEEP.contains(&k.to_case(Case::Snake).as_str()) + || v.is_boolean() && v.as_bool() == Some(false) || v.is_number() && v.as_u64() == Some(0) && !ZERO_VALUE_EXCLUDED_FIELDS @@ -708,4 +623,13 @@ mod tests { String::from("2XFGVOHMFYLAWBHOSIOI67PBT5LDRHBTD3VLX5EYBDTFNVKMCJIA") ); } + + #[test] + fn test_asset_unfreeze_snapshot() { + let data = TestDataMother::asset_unfreeze(); + assert_eq!( + data.id, + String::from("LZ2ODDAT4ATAVJUEQW34DIKMPCMBXCCHOSIYKMWGBPEVNHLSEV2A") + ); + } } diff --git a/crates/algokit_transact/src/tests.rs b/crates/algokit_transact/src/tests.rs index ca23cfd1b..1e8acd2ca 100644 --- a/crates/algokit_transact/src/tests.rs +++ b/crates/algokit_transact/src/tests.rs @@ -5,7 +5,7 @@ use crate::{ test_utils::{ AddressMother, TransactionGroupMother, TransactionHeaderMother, TransactionMother, }, - transactions::{AssetFreezeTransactionBuilder, FeeParams}, + transactions::FeeParams, Address, AlgorandMsgpack, EstimateTransactionSize, SignedTransaction, Transaction, TransactionId, Transactions, }; @@ -410,167 +410,3 @@ fn test_signed_transaction_group_encoding() { assert_eq!(decoded_signed_tx, signed_grouped_tx); } } - -#[test] -fn test_asset_freeze_transaction_encoding() { - let tx_builder = TransactionMother::asset_freeze(); - let asset_freeze_tx_fields = tx_builder.build_fields().unwrap(); - let asset_freeze_tx = tx_builder.build().unwrap(); - - let encoded = asset_freeze_tx.encode().unwrap(); - let decoded = Transaction::decode(&encoded).unwrap(); - assert_eq!(decoded, asset_freeze_tx); - assert_eq!(decoded, Transaction::AssetFreeze(asset_freeze_tx_fields)); - - let signed_tx = SignedTransaction { - transaction: asset_freeze_tx.clone(), - signature: Some([0; ALGORAND_SIGNATURE_BYTE_LENGTH]), - auth_address: None, - }; - let encoded_stx = signed_tx.encode().unwrap(); - let decoded_stx = SignedTransaction::decode(&encoded_stx).unwrap(); - assert_eq!(decoded_stx, signed_tx); - assert_eq!(decoded_stx.transaction, asset_freeze_tx); - - let raw_encoded = asset_freeze_tx.encode_raw().unwrap(); - assert_eq!(encoded[0], b'T'); - assert_eq!(encoded[1], b'X'); - assert_eq!(encoded.len(), raw_encoded.len() + 2); - assert_eq!(encoded[2..], raw_encoded); -} - -#[test] -fn test_asset_unfreeze_transaction_encoding() { - let tx_builder = TransactionMother::asset_unfreeze(); - let mut asset_freeze_tx_fields = tx_builder.build_fields().unwrap(); - let asset_freeze_tx = tx_builder.build().unwrap(); - - // Verify it's an unfreeze transaction - assert_eq!(asset_freeze_tx_fields.frozen, Some(false)); - - // When encoded, the frozen field should be omitted (None) - let encoded = asset_freeze_tx.encode().unwrap(); - - // For test comparison, set frozen to None since that's how it appears in the encoded form - asset_freeze_tx_fields.frozen = None; - let expected_tx = Transaction::AssetFreeze(asset_freeze_tx_fields); - - let decoded = Transaction::decode(&encoded).unwrap(); - assert_eq!(decoded, expected_tx); -} - -#[test] -fn test_asset_freeze_mainnet_encoding() { - let tx = TransactionMother::asset_freeze().build().unwrap(); - // Just verify it encodes without checking exact size - let encoded = tx.encode().unwrap(); - let decoded = Transaction::decode(&encoded).unwrap(); - assert_eq!(decoded, tx); -} - -#[test] -fn test_asset_freeze_testnet_encoding() { - let tx = TransactionMother::asset_freeze().build().unwrap(); - // Just verify it encodes without checking exact size - let encoded = tx.encode().unwrap(); - let decoded = Transaction::decode(&encoded).unwrap(); - assert_eq!(decoded, tx); -} - -#[test] -fn test_asset_freeze_minimal_encoding() { - let tx = TransactionMother::asset_freeze().build().unwrap(); - // Just verify it encodes without checking exact size - let encoded = tx.encode().unwrap(); - let decoded = Transaction::decode(&encoded).unwrap(); - assert_eq!(decoded, tx); -} - -#[test] -fn test_asset_freeze_with_group_encoding() { - let tx = TransactionMother::asset_freeze().build().unwrap(); - - // Verify group field is set - if let Transaction::AssetFreeze(fields) = &tx { - assert!(fields.header.group.is_some()); - } - - // Just verify it encodes without checking exact size - let encoded = tx.encode().unwrap(); - let decoded = Transaction::decode(&encoded).unwrap(); - assert_eq!(decoded, tx); -} - -#[test] -fn test_asset_freeze_real_transaction_ids() { - // Test with mainnet freeze - let freeze_tx = TransactionMother::asset_freeze().build().unwrap(); - let freeze_id = freeze_tx.id().unwrap(); - assert_eq!(freeze_id.len(), 52); // Base32 encoded length - - // Test with mainnet unfreeze - let unfreeze_tx = TransactionMother::asset_unfreeze().build().unwrap(); - let unfreeze_id = unfreeze_tx.id().unwrap(); - assert_eq!(unfreeze_id.len(), 52); - - // Verify freeze and unfreeze have different IDs (different frozen value) - assert_ne!(freeze_id, unfreeze_id); -} - -#[test] -fn test_asset_freeze_required_fields() { - // Missing asset_id should fail - let result = AssetFreezeTransactionBuilder::default() - .header(TransactionHeaderMother::simple_testnet().build().unwrap()) - .freeze_target(AddressMother::neil()) - .frozen(true) - .build(); - - // Builder with derive_builder pattern requires all fields - assert!(result.is_err()); - - // Missing freeze_target should fail - let result = AssetFreezeTransactionBuilder::default() - .header(TransactionHeaderMother::simple_testnet().build().unwrap()) - .asset_id(12345) - .frozen(true) - .build(); - - assert!(result.is_err()); -} - -#[test] -fn test_asset_freeze_serialization_fields() { - let tx = TransactionMother::asset_freeze().build().unwrap(); - let encoded = tx.encode_raw().unwrap(); - - // Decode as MessagePack Value first - let decoded_value: rmpv::Value = rmp_serde::from_slice(&encoded).unwrap(); - - // Convert to a map to check fields - if let rmpv::Value::Map(map) = decoded_value { - let mut found_faid = false; - let mut found_fadd = false; - let mut found_afrz = false; - let mut found_type = false; - - for (key, _value) in map { - if let rmpv::Value::String(ref s) = key { - match s.as_str() { - Some("faid") => found_faid = true, - Some("fadd") => found_fadd = true, - Some("afrz") => found_afrz = true, - Some("type") => found_type = true, - _ => {} - } - } - } - - assert!(found_faid, "Missing faid field"); - assert!(found_fadd, "Missing fadd field"); - assert!(found_afrz, "Missing afrz field"); - assert!(found_type, "Missing type field"); - } else { - panic!("Expected MessagePack map"); - } -} diff --git a/crates/algokit_transact/src/transactions/asset_freeze.rs b/crates/algokit_transact/src/transactions/asset_freeze.rs index cd792b6f7..d07ae4802 100644 --- a/crates/algokit_transact/src/transactions/asset_freeze.rs +++ b/crates/algokit_transact/src/transactions/asset_freeze.rs @@ -5,7 +5,7 @@ use crate::address::Address; use crate::transactions::common::TransactionHeader; -use crate::utils::{is_false_opt, is_zero, is_zero_addr}; +use crate::utils::{is_zero, is_zero_addr}; use crate::Transaction; use derive_builder::Builder; use serde::{Deserialize, Serialize}; @@ -46,9 +46,9 @@ pub struct AssetFreezeTransactionFields { /// `false` to unfreeze the asset holdings (allow transfers). #[serde(rename = "afrz")] #[serde(default)] - #[serde(skip_serializing_if = "is_false_opt")] + #[serde(skip_serializing_if = "std::ops::Not::not")] #[builder(default)] - pub frozen: Option